#ay38910 — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #ay38910, aggregated by home.social.
-
🎵 Looking for a chiptune composer for my Pacman tribute for #MSX2
• PSG / AY-3-8910 — Arkos Tracker 3 preferred (we can talk formats)
• The game will be released for FREE
I can pay for the tracks, or credit you in the game, whatever suits you best.
#MSX #chiptune #chipmusic #retrogaming #AY38910 -
So, annoyingly after thinking I might have had a bit of a breakthrough, it turns out I was chasing my tail and it seems like I'm back to square one...
I'm going to have to have a break from this for a bit, as I'm running out of options at the moment.
https://diyelectromusic.com/2026/05/24/ay-3-8912-8910-hardware-emulation-part-3/
-
AY-3-8912/8910 Hardware Emulation – Part 3
This continues my series looking at hardware emulation of the AY-3-8912/8910:
- Part 1: Introduces the AVR-AY projects and available boards and how they go about using an AVR microcontroller, usually an ATMega48, to emulate an AY-3-8910 device. It walks through the functionality of the provided firmware.
- Part 2: My own experimenter PCB based on an overclocked (27MHz) ATMega328P. Unfortunately, when I left things, it wasn’t working.
This post picks up with an attempt to rebuild the firmware from source in an attempt to figure out what is going on.
Spoilers: At the end of it, it still doesn’t work though…
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
ICSP Header
The first thing to do is to add the ICSP header to my AVR-AY. Unfortunately, as I mentioned in Part 2 I got the pinout back to front. But this can be easily accounted for by soldering the ICSP header onto the underside of the board.
I decided this would work best with a right-angled 2×3 pin header, which also means it sits nicely under the bent-over capacitor.
An easy check is to use the Arduino environment to download a blink sketch. As I’m using my USBasp again, I need to select that under “Tools” -> “Programmer”, then it is possible to use “Upload using Programmer” to load a sketch. This can be found under “Sketch” or as shown below, using the shortcut CTRL-SHIFT-U.
All being well, pin 13 should be toggling on and off, albeit at a slighter faster rate than expected – this is running off a 27MHz crystal rather than a 16MHz of course.
Note: LED_BUILTIN = D13 = PB5 = BC2 = ICSP CLK on this board, so that is fairly easy to probe with a scope, or even temporarily add in LED.
AVR Development
In every other case, I’ve never bothered to get into raw AVR development. I’ve found I can do everything I’ve been interested in doing via the Arduino environment.
Unfortunately one thing the Arduino environment won’t do is assembly. Plus it will always add in some bootstrapping code prior to running your setup() routine and it has some housekeeping code inbetween each call to the Arduino loop().
There are a number of options for including some assembler in an Arduino sketch, but it is quite limited and doesn’t scale to an entire program.
One option is to assemble it outside of the Arduino environment and then include it in the final build. But that is all a bit of a faff, so I’m dropping back to a non-Arduino build envirnment, and traditionally that meant using Atmel Studio.
But this is no more. When Microchip acquired Atmel in 2016, this was renamed Microchip Studio. But even that is now deprecated for new designs. But it is still available, so I’m starting with that.
Another option is winavr. I might return to giving this a try later…
LED Flash in Microchip Studio
To check everything is running with Microchip Studio, I opened one of the example projects, filtering for ATmega328, “megaAVR led example – ATmega328P Xplained MINI”. I think this is for one of their own dev boards.
This gives me a whole pile of code, but the only bit I’m interested in is in src/mega_led_example.c. Everything else in src/ASF and src/config I think is related to the dev board and examples, which I’ve mostly ignored.
I changed the provided code, which would flash an LED on a button press, to the following:
#define F_CPU 27000000UL
#include <asf.h>
#include <util/delay.h>
int main (void)
{
/* set board io port */
board_init();
bool button_state = false;
while(1){
_delay_ms(1000);
if(button_state){
LED_Off(LED0);
button_state = true;
}else{
LED_On(LED0);
button_state = false;
}
}
}LED0 and LED_On/Off are defined for the board, and in this case map onto PB5 which is the same as LED_BUILTIN being D13 for an Arduino Uno.
I first played save with F_CPU = 16MHz, but actually once everything was up and running, it seemed quite happy for me to set it to 27MHz for the delay to be accurate.
So, this now builds, but now to download it…
Microchip Studio and USBasp
There are a number of programmers supported within Microchip Studio, but USBasp isn’t one of them. To be able to use it, it has to be added as an external tool.
But before that can happen, you have to make sure you’re in Advanced Mode.
When installing, there is an option of running in Standard or Advanced mode. I choose standard, and consequently couldn’t find most of the things being talked about online in conjunction with USBasp and Microchip/Atmel Studio!
Once changed to Advanced Mode, under “Tools” there is an option for “External Tools”. Once selected, a new tool setup is entered for USBasp.
This has to use the same avrdude setup as described in Part 2, but the filename has to link into the paths that Microchip Studio will use.
- Title: anything you wish
- Command: path\to\avrdude.exe
- Arguments: avrdude -c usbasp -p atmega328p -U flash:w:$(ProjectDir)\Debug\$(TargetName).hex:i
Everything else is left as you see it above. When saved, there is now a new entry under “Tools” which can be used to download directly via USBasp.
Note: debugging is not possible, so the whole “Debug” set of menus is ignored.
At this point I can use “Build” -> “Build Solution” and then “Tools” -> “USBasp-ATmega328P” to build code and get it running.
Microchip Studio Raw Assembler
Now onto the main reason for doing all this – can I build and run the assembler for the AVRAY?
Turns out that is one of the supported options. On creating a new project (curiously, for a C/C++ project there is an option to import an Arduino sketch), choosing assembler gives:
Then I’m asked to choose a MCU, so select ATmega328P and then I am provided with the following:
But actually when I went to get the source, there is already an Atmel Studio project file included (this is the source from: https://github.com/Yevgeniy-Olexandrenko/avr-ay-board/tree/main/firmware/v1.0/sources
On opening this project file, it requires the downloading of an additional pack: Atmega_DFP. I told it to download the specific version requested. But it seems to have ignored me and downloaded the latest one.
In the project properties I had to change the device from ATmega48PA to ATmega328P.
In order to avoid the need for an EEPROM configuration file (which requires the command line avrdude command as described in Part 2) I fixed the code to always assume no serial interface, always parallel interface, and 27MHz operation:
RESET:
; all existing setup code until...
; get byte 0 from EEPROM, check value > 0 or skip USART initialization if value = 0
OutReg EEARH, C00 ; is absent in Atmega48
OutReg EEARL, C00
sbi EECR, b0
InReg r16, EEDR
cp r16, C00
;breq NO_USART
jmp NO_USART ; Always jump to NO_USART
; More code
; In init Timer0
; 219512 Hz internal update clock
ldi r16, (27000000 / (1750000 / 8) - 1)
out OCR0A, r16
; OutReg EEARL, YH ; set EEPROM address 2
; sbi EECR, b0
; InReg r18, EEDR ; load byte 2 from EEPROM to r18
; OutReg OCR0A, r18 ; set PWM speed from byte 2 of EEPROM (affect AY chip frequency)
; After init Timer2
; check for parallel interface enabled in byte 1 of EEPROM
OutReg EEARL, ZH
sbi EECR, b0
InReg r16, EEDR
cp r16, C00
;breq NO_EXT_INT ; Never jump over the parallel init codeThen I used PD1 as a debug OUTPUT pin rather than its usual TX function. Whenever this was required, the following code would be used to set or clear the OUTPUT:
; Set
sbi DDRD, b1
sbi PORTD, b1
; Clear
sbi DDRD, b1
cbi PORTD, b1Using this at the start of the MAIN_LOOP, then for ISR_INT0 and ISR_INT1, I can see that when I manually trigger BC1 or BDIR that the interrupt routines are indeed being called as required. There are two return points in each ISR. They basically have the following structure:
ISR_INT0: Triggered on BC1
IF BDIR also HIGH jump to LATCH_ADDRESS
Handle register read
return from Interrupt
ISR_INT1: Triggered on BDIR
IF BC1 also HIGH jump to LATCH_ADDRESS
Handle register write
return from Interrupt
LATCH_ADDRESS:
Store register address
return from InterruptI can also see that the main loop does utilise most of the CPU time between the PWM sample period.
Note: When looking at pins PB1-PB3 directly (i.e. before the filter circuit) I can see the PWM short pulses occuring, so I know all register initialisation for PWM is happening correctly too.
A note on initialising the interrupts:
; --------------------------------------------------------------------------
; Init external interrupts INT0 and INT1
; --------------------------------------------------------------------------
ldi r16, 0x0F ; fallen edge of INT0, INT1
OutReg EICRA, r16
ldi r16, 0x03
OutReg EIFR, r16
OutReg EIMSK, r16
NO_EXT_INT:The comment states “fallen edge” but the value 0x0F written to EICRA will set up the interrupts on the rising edge. Given that comments elsewhere in the ISRs discuss waiting for the signals to clear, I believe rising edge is correct.
For example, in ISR_INT0, triggered on a rising BC1, it will then wait for BC1 to clear before returning.
At this point, I’m throwing all timings to the wind and adding instructions to instrument the interrupt routines as follows:
ISR_INT0: Triggered on BC1
PD0 = HIGH
IF BDIR also HIGH jump to LATCH_ADDRESS
Handle register read
jump to CLEAR
ISR_INT1: Triggered on BDIR
PD1 = HIGH
IF BC1 also HIGH jump to LATCH_ADDRESS
Handle register write
jump to CLEAR
LATCH_ADDRESS:
PD0 = PD1 = HIGH
Store register address
PD0 = PD1 = LOW
return from InterruptThe idea is to attempt to capture what the AVRAY thinks is going on with respect to BC1/BDIR. Here is a trace of the first access.
Yellow is PD0/BC1 and blue is PD1/BDIR. We can see PD0 going HIGH, hence an INT0 indicating BC1 was detected, followed by PD1 going HIGH which means INT0 has jumped to LATCH_ADDRESS, followed by both going LOW showing it is complete and the return from interrupt.
Then we see BDIR going high with no signal for BC1, so this means INT1 has triggered and a register write is occurring.
Then we see the LATCH_ADDRESS pattern repeat. In fact, including the first one, this repeats 10 times, which corresponds to my test code attempting to initialise 10 registers on startup.
But although the test code is using the same algorithm to latch the address (BC1=BDIR=HIGH) and then a register write (BC1=LOW, BDIR=HIGH) the emulator code isn’t “seeing” any more register writes.
It turns out that the first write is the only one seen by the emulator at all. In the following trace, we can see the ten initialisation writes happening, then a short pause as the Arduino returns from the setup() code and starts the main loop(). Then there is regular scanning of a single AY register in the Arduino’s loop. The first trigger is the only one that shows the write happening at all.
So to me, this implies that INT1 has somehow stopped functioning. I need to add in the traces of the actual BC1 and BDIR signals to be sure they are still happening though, although I see no reason to think they won’t be.
Ok, this is illuminating…
Dark blue is BC1 and purple is BDIR. As we can see here, it goes HIGH and then stays HIGH, well forever. I guess the first write is picked up as the INT0/INT1 interrupts I guess are queued up?
At this point I pulled the AVRAY out whilst it was running, and guess what? BDIR was still HIGH. So looking back over my test code, and I spot this:
#define BDIRLOW {PORTC &= 0xF7;} // A2 LOW
#define BDIRHIGH {PORTC |= 0x04;} // A2 HIGHAnyone? All together now…
Sigh. Yes, that 0xF7 should have been 0xFB. Or better yet, (~0x04).
So now I finally can see that the interrupts are, indeed, working as expected.
Try Again…
So at this point, I went back to the original 328 firmware, EPROM configuration and fuse settings, figuring that my test writes to pins isn’t going to help anything working.
And it still doesn’t work. Big sigh.
At this point I’m going to have to leave it again, because all I’ve really proved is that I can’t be trusted with both sides of a two-part system…
Kevin
#arduino #atmega328 #avr #ay38910 #microchipStudio -
AY-3-8912/8910 Hardware Emulation – Part 2
Having explored how an AVR can be used to emulate the AY-3-8912/8910 in AY-3-8912/8910 Hardware Emulation I wanted to have a go at using the very common ATMega328P to do the same. But rather than wire things up on breadboard, I put together a quick PCB to allow me to do some experiments.
Spoilers: This isn’t working yet!! Read on for where I’ve got to, but I’ll need to come back to this at some point.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to electronics and microcontrollers, see the Getting Started pages.
The Circuit
This is essentially the circuit from here: https://github.com/Yevgeniy-Olexandrenko/avr-ay-board
But with an ATMega328P rather than the suggested ATMega48PA (more about that in part one here: AY-3-8912/8910 Hardware Emulation). I’ve included headers for both the AY-3-8910 and AY-3-8912.
I’ve kept the same usage of ATMega ports, which map onto IO for the ATMega328P as follows:
AY-3-8912SchematicATMega328P PORTATMega328P PinArduino EquivalentD0-D5DA0-DA5PC0-PC523-28A0-A5D6-D7DA6-DA7PD6-PD712-13D6-D7A8A8_CIPOPB418D12BC1BC1PD24D2BC2BC2_SCKPB519D13BDIRBDIRPD35D3CLOCKCLOCKPD46D4RESETRESETPC61RESETTESTN/CIOA7-IOA0N/CCPWM_CPB216D10BPWM_B_COPIPB317D11APWM_APB115D9A9_CFG0PB014D8CFG1PD56D5RXPD02D0TXPD13D1VCCVCCVCC7, 20VCCGNDGNDGND8GNDThe only change is I’m not connecting TEST to anything. I’ll have to see if this becomes an issue later…
The circuit calls for overclocking the ATMega328P to 27MHz (again more discussion on this in the first part).
I’ve included breakout headers for ICSP and a serial header.
- Update: Turns out the header is back to front! See later…
I have also swapped the oscillator to a crystal using a common configuration I’ve seen on many DIY Arduino-style boards using an ATMega328P.
Rather than put together two boards, one for the 8912 and one for the 8910, I’ve also put together a simple adaptor circuit to allow one to plug into the socket for the other.
The schematic essentially just maps the signals from one footprint onto the other.
The only slight complication is that /A9 for the 8910 doesn’t exist on the 8912, so I’ve left in a jumper to provide the option of tying this to GND or VCC should the need arise. This should allow the larger 8910 to be used in the place where a 8912 is required whilst still enabling the 8910.
PCB Design
I did wonder quite how much of a 28 pin DIP could fit inside the footprint of a 28 pin WDIP and still keep through hole components, but I didn’t have to wonder for long to see it wasn’t really going to work.
So as this is basically just for messing around I went with the format as shown. This way, round-profile pin headers can be used on the underside for the AY footprint if the board is to replace a genuine device. Alternatively normal pin headers or sockets can be used on the topside if jumper wires are going to be used to hook this board into another PCB.
The two CFG jumpers are solder bridges (default not connected) on the underside of the board. I’ve also listed the Arduino versions of the AVR pins used on the underside too.
For the converter board, I’ve left the /A9 jumper as standard pin headers. With hindsight I’m not sure when this would be wanted to be tied high, as presumably that would disable the chip. When plugging a 8912 into a 8910 socket, anything expecting an 8910 and using /A9 as part of the addressing scheme would require some additional logic before mapping it onto the smaller 8912.
Errata
The ICSP header is swapped to what is required. This will still work as long as the header is populated on the reverse of the board…
From the top/silkscreen, the header has the following pinout (viewed from the top, ATMega328 on the left).
RESETGNDCLKCOPICIPOVCC (square pad)Bill of Materials
AVR-AY Emulator Board:
- AVR-AY-Board PCB (GitHub link below).
- ATMega329P (28 pin DIP version).
- 1x 1K resistor
- 3x 3K6 resistors
- 1x 10K resistor
- 1x 10uF electrolytic capacitor
- 2x 100nF ceramic capacitors
- 3x 2.2nF ceramic capacitors
- 2x 18pF ceramic capacitors
- 1x 3mm LED (colour to taste)
- 1x 27 MHz crystal (2 pin, low-profile, HC-49 package – see footprints)
- 1x 2-pin momentary push switch (see footprints)
- Optional: 28-way narrow DIP socket
- Optional: round-pin header pins (see photos and discussion)
- Range of pin headers or sockets as required
AY-3-8910 to 12 Converter:
- AY-3-8910-12 Converter PCB (GitHub link below).
- Either 40-pin WDIP socket or 28-pin WDIP socket.
- Round-pin header pins.
AY-3-8910 to 12 Converter Build
This is a relatively straight forward build, but depending on the DIP sockets used it might be necessary to doctor the socket slightly to sit neatly over the soldered pins.
Order of build:
- Round pins for either the 28 pin or 40 pin part of the PCB.
- The 40 pin or 28 pin DIP socket.
If the converter is from a 8912 socket to a 8910 device, then the /A9 jumper or a solder link will have to be configured, presumably connecting it to GND to make it permanently active.
One note of caution – the pins I was using are not particularly robust, so when removing the adaptor from its socket, I managed to break one and had to replace it, which wasn’t easy.
But apart from that, it works!
AVR AY Board
As already mentioned there are a number of configuration options for the 28-pin AY-3-8910 emulation as illustrated below.
The first two are designed for jumper wire connections. The third shows the use of pins which would allow the board, space permitting, to be inserted into a AY-3-8910 socket on an existing board.
These headers will be soldered on last, but it is worth deciding in advance what configuration will be required.
I’ve chosen not to populate the UART and ICSP headers, and am using round pins so that this can hopefully replace an AY-3-8912. But at the moment all my test boards are built for the AY-3-8910, so I’m also having to use the converter, which has led to the following stack of boards!
ATMega328P Programming
The stand-alone ATMega328P chip cannot be programmed directly from the Arduino IDE with this code as it stands. Instead the following are required:
- AVRDude: https://github.com/avrdudes/avrdude
- An AVR Programmer: I’m using a cheap USBasp clone, so one based on this: https://github.com/piit79/USBasp
- Connection to the device via ICSP: I’m using a homemade link, but it should be possible to use the ICSP header on the PCB (however see previous warning about the pinout error!); plug it into a DIP-version of another Arduino Uno board; or possibly even a second Arduino Uno board in “Arduino as ISP” mode.
The firmware should be downloaded from https://github.com/Yevgeniy-Olexandrenko/avr-ay-board.
Update: I ended up using a slightly different firmware and configuration – see later…
I’m using v1.0 of the firmware and have downloaded the binary firmware and the 1.75MHz configuration. I now have the following files:
C:\Kevin\Temp> dir
09/05/2026 15:49 <DIR> .
09/05/2026 15:49 <DIR> ..
09/05/2026 15:49 1,920 avr-psg.hex
09/05/2026 15:40 909,660 avrdude.conf
09/05/2026 15:40 9,604,608 avrdude.exe
09/05/2026 15:40 13,316,096 avrdude.pdb
09/05/2026 15:49 34 config-1.75mhz.hex
C:\Kevin\Temp>To program the ATMega328P requires the following instructions to get the code into flash, the configuration into EEPROM and to set the fuses for the MCU:
C:\Kevin\Temp>avrdude -c USBasp -p ATMega328P -U flash:w:avr-psg.hex:i
Error: cannot set sck period; please check for usbasp firmware update
Error: cannot set sck period; please check for usbasp firmware update
Reading 682 bytes for flash from input file avr-psg.hex
Writing 682 bytes to flash
Writing | ################################################## | 100% 0.43 s
Reading | ################################################## | 100% 0.23 s
682 bytes of flash verified
Avrdude done. Thank you.
C:\Kevin\Temp>avrdude -c USBasp -p ATMega328P -U eeprom:w:config-1.75mhz.hex:i
Error: cannot set sck period; please check for usbasp firmware update
Reading 5 bytes for eeprom from input file config-1.75mhz.hex
Writing 5 bytes to eeprom
Writing | ################################################## | 100% 0.09 s
Reading | ################################################## | 100% 0.01 s
5 bytes of eeprom verified
Avrdude done. Thank you.
C:\Kevin\Test>avrdude -c USBasp -p ATMega328P -U lfuse:w:0xdf:m -U hfuse:w:0xdf:m -U efuse:w:0xfd:m
Error: cannot set sck period; please check for usbasp firmware update
Processing -U lfuse:w:0xdf:m
Reading 1 byte for lfuse from input file 0xdf
Writing 1 byte (0xDF) to lfuse, 1 byte written, 1 verified
Processing -U hfuse:w:0xdf:m
Reading 1 byte for hfuse from input file 0xdf
Writing 1 byte (0xDF) to hfuse, 1 byte written, 1 verified
Processing -U efuse:w:0xfd:m
Reading 1 byte for efuse from input file 0xfd
Writing 1 byte (0xFD) to efuse, 1 byte written, 1 verified
Avrdude done. Thank you.The error “cannot set sck period” is apparently pretty common especially with USBasp clones, and can be ignored.
The three fuse settings come from the original AVR-AY firmware (here) in the readme as follows:
ATMEGA328 ===================================================
avrdude -p m328p -c USBasp -U flash:w:AY_Emul_XXX_Nch_KK_MM.hex -U eeprom:w:Conf_XXX_YYMHz_ZZMhz.hex -U lfuse:w:0xee:m -U hfuse:w:0xdf:m -U efuse:w:0xfd:m
Example:
avrdude -p m328p -c USBasp -U flash:w:AY_Emul_250_2ch_m328_ay.hex -U eeprom:w:Conf_standard_27MHz_1_75Mhz.hex -U lfuse:w:0xee:m -U hfuse:w:0xdf:m -U efuse:w:0xfd:m
=============================================================But where the fuse settings for lfuse = 0xEE, when decoded from the ATMega328P datasheet and an online AVR fuse calculator here: https://eleccelerator.com/fusecalc/fusecalc.php?chip=atmega328p means the following:
0xEE = b1110 1110
CKDIV8 = 1 (unprogrammed)
CLKOUT = 1 (unprogrammed)
SUT = 10
CLKSEL = 1110Which from the AVR fusecalc site maps onto:
- External crystal oscillator
- Frequency 8MHz +
- Start up time 1K
- Powerdown/reset 16
However, following through the settings in the datasheet, it is similar but slightly different, giving:
- Low power crystal oscillator (CLKSEL3:0 = 1111 – 1000)
- Low power operating mode 111 = 8.0-16.0 MHz (with recommended caps 12-22pF)
- CLKSEL0+SUT = 0+10 = Ceramic Resonator BOD enabled, Start up time 1KCK, additional delay 14CK
But I’m not using a ceramic resonator, I’m using a crystal oscillator, so I think I need the mode:
- Crystal oscillator BOD enabled, startup time 16KCK, additional delay 14CK = CLKSEL0+SUT = 1+01
There is no “1KCK” for the crystal oscillator settings, so I’ll have to use 16KCK I guess?
This is what gives me the 0xDF value I’ve used:
0xDF = b1101 1111
CKDIV8 = 1 (unprogrammed)
CLKOUT = 1 (unprogrammed)
SUT = 01
CLKSEL = 1111Actually, this wasn’t working with either 0xEE or 0xDF, so I decide to try to turn off BOD and went with “slow rising power” (CLKSEL0+SUT = 1+11) which gave me the fuses:
lfuse = b1111 1111 = 0xFF
hfuse = 0xDF (as before)
efuse = 0xFF (no brown-out detection)But that isn’t working either…
The AY891x Library and BCn/BCDIR control
Eventually, I remembered reading about the control signals with the library that was designed for use with that test board. It states the following (here):
“While the PSG has tight timing requirements, it is possible to use
digitalWrite()by using all three bus control signals (BDIR, BC1, BC2) and cycling through an extra state when reading and writing the chip registers.”But the emulation is ignoring BC2 (more here), relying instead on the ATMega’s two IO interrupt pins INT0 and INT1 on D2 and D3, to trigger on BDIR and BC1. This means I had to ditch the library and instead switched over to my own PORT IO driver as described here: Arduino and AY-3-8910 – Part 3.
Resetting the fuses as before, unfortunately it still isn’t working.
At this point I loaded on a simple tone() function on one of the IO pins using the Arduino IDE then set the fuses again as above. I can definitely see that the frequency for the tone() output is almost twice as high as when the ATMega328P is plugged into the Arduino, so the 27MHz crystal is certainly having an effect compared to the Uno’s original 16MHz.
I can also see the 105kHz PWM carrier frequency on all three PWM output pins, but the peaks are so narrow, these must be responding to a level 0 output which implies all code is working, but there are simply no register writes getting through.
So, in summary, I think the code on the emulator is running fine – so why isn’t it responding to register writes?
An Epiphany?
At this point I left things for a bit and came back to it all a few days later.
I was chewing over why the register writes didn’t seem to be getting through, so at this point I think the handling of BC1 and BDIR are key. The next things that I tried:
- Getting it all on a breadboard and showing it working with a real AY-3-8910.
- Check I can see BC1 and BDIR and the data lines doing something.
- Replace the real chip with the emulator.
- Repeat the tests for BC1, BDIR and data – all appear ok.
At this point I’m suspecting the interrupts aren’t getting through (remember BC1 and BDIR are triggered off the external interrupts), so I’m contemplating two things:
- Some test code that lights an LED on an external interrupt.
- Porting the whole code across to the Arduino environment to let me build and run it all from there.
But this has got me wondering about build differences for the two devices – what might be different between the ATMega48 and ATMega328P. Would rebuilding from source for the ATMega328P sort it out?
Looking at the original code, I can see there are many different binaries for the different chips. For the repository I’m using there is just one.
Looking in the two chip datasheets, I can see that the INT0 interrupt vector is different – it is 2 (address 0x001) for the 48 and whist it is still 2 for the 328P, the address is 0x0002. From the 48 datasheet:
“Each interrupt vector occupies two instruction words in ATmega168, and one instruction word in ATmega48 and
ATmega88.”From the 328P datasheet:
“Each interrupt vector occupies two instruction words in Atmel ATmega328P.”
Looking at the suggested implementations of the vector tables, they are shown as follows:
// ATMega48 vector table
0x000 rjmp RESET ; Reset Handler
0x001 rjmp EXT_INT0 ; IRQ0 Handler
0x002 rjmp EXT_INT1 ; IRQ1 Handler
// ATMega328P vector table
0x0000 jmp RESET ; Reset Handler
0x0002 jmp EXT_INT0 ; IRQ0 Handler
0x0004 jmp EXT_INT1 ; IRQ1 HandlerSo presumably one is a relative jump (rjmp) which is a single word (16-bit) instruction, vs an absolute jump which is a two-word (32-bit) instruction.
With hindsight is seems obvious I’d need to rebuild for the ATMega328P, but the initial talk of compatibility lulled me into a false sense of security!
There is no build for the 328P in the listed repository, but going back to the original avr-ay source, there are builds for all supported MCUs, each with variants covering:
- 2 or 3 channel sound.
- AY or YM volume tables.
- Additional speaker output on PD1.
There are configuration options for:
- Serial, parallel, or both (“standard”).
- 1.75MHz, 1.78MHz, or 2.0MHz operation.
- 20MHz to 40MHz (including my required 27MHz).
I’ve now downloaded:
avrdude -c USBasp -p ATMega328P -U flash:w:AY_Emul_260_3ch_m328_ay.hex:i
avrdude -c USBasp -p ATMega328P -U eeprom:w:Conf_parallel_27MHz_1_78Mhz.hex:i
avrdude -c USBasp -p ATMega328P -U lfuse:w:0xee:m -Uhfuse:w:0xdf:m -U efuse:w:0xfd:mBut unfortunately it still isn’t working…
I did notice in the original source, a number of conditional assembler code, based on the initial configuration of MCU_TYPE. But most of it is of the form “if MCU_TYPE==0” which is giving alternative code for the ATMega8. There is one piece of code that implies something different for the ATMega48:
; get byte 0 from EEPROM, check value > 0 or skip USART initialization if value = 0
#if MCU_TYPE == 0 || MCU_TYPE > 1
out EEARH,C00 ; is absent in Atmega48
#endif
out EEARL,C00I also note that the vector table is all relative jumps:
.cseg
;------------------------------------------------------
; INTERRUPT VECTORS TABLE
;------------------------------------------------------
.org 0x0000
rjmp _RESET
.org INT0addr
rjmp _INT0_Handler
.org INT1addr
rjmp _INT1_Handler
.org URXCaddr
rjmp _USART_RX_COMPLETEBut as each entry has its own origin statement, one presumes that these are correct for the MCU type.
Double checking the hex records for my chosen firmware, and adding some expansions and annotations, I can see:
// Format:
// :ll aaaa tt dd..dd cc
// ++-- CRC
// +----+----- Data
// ++------------ Record type
// +--+--------------- Address
// ++-------------------- Data length
:02 0000 02 0000 FC // tt=02: Extended (Upper) Segment Base Address
// Vector Table tt=00: Data
:02 0000 00 88C0 B6
:02 0004 00 4AC0 F0
:02 0008 00 56C0 E0
// Start of code/data tt=00; Data
:10 0048 00 50C0000101010202030506090D11161D 29
:10 0058 00 242D0000010101010101020202020303 33
:10 0068 00 0505060607090B0D0F111316191D2024 87
...
:10 02F8 00 0092880010928A0092CF05910D932A95 5A
:04 0308 00 E1F70895 7C
:00 0000 01 FF // tt=01: End of FileSo yes, it would appear that the vectors in the vector table are indeed on the expected 32-bit boundaries which matches and it has values for vectors 2 and 3, so INT0 and INT1. Vector 1 is RESET.
One oddity, each record is only 2 bytes long, so I guess it is assuming that the missing bytes will be zero? Really, the full records should perhaps be:
:04 0000 00 88C00000 B4 // Needed to recalc CRC
:04 0004 00 4AC00000 EE
:04 0008 00 56C00000 DEI don’t know the algorithm for calculating the CRC, but I don’t need to. When attempting to download via avrdude any CRC mismatch is reported, detailing what value was expected which allows me to set the correct value for each line at a time!
But as these are instructions rather than addresses themselves, maybe it would be fine. If the PC hits this address, then the instruction will be (presumably) a rjmp which is only two bytes and all would be fine.
But regardless, unfortunately this still isn’t working. I’m now at quite a loss as to what to try next.
I did go back and look at the original firmware I was using and it has the following:
:06 0000 00 70C039C045C0 CC
So I don’t see how this would work on an ATMega328P at all as each vector is only half the expected size.
The reset vector is probably ok, as the first two bytes will be read as a “rjmp” instruction and presumably the next two bytes ignored. But INT0 and INT1 will be jumping off into who-knows-where.
This would match the evidence that the board seems to get initialised but no register writes are processed.
So this at least does confirm that the original firmware was no good for me.
Conclusion
I’ve got to draw a line under this for now. I’m going to hit publish on this post as is, and put this aside for a bit.
Some possible future directions:
- Solder on the ICSP header to make uploading code a bit easier.
- Load up some simple Arduino firmware that will indicate if INT0/INT1 are actually getting through.
- Create a ATMega48 breakout to see if the board would work with the original MCU ok.
- I’m still not convinced with that clock – it seems a very low amplitude – I wonder if those capacitors are the right values.
- Port the whole lot to the Arduino environment. This is going to be a fair bit of work as you can’t use assembler directly, but only via asm(“nop”) style directives…
But for now, this one is on pause.
And I’m still annoyed about the error in the ICSP pinout…
Kevin
#arduino #atmega328 #ay38910 #ay38912 #emulation #MakerFail #pcbs -
And for a fun comparison here is the same with four AY-3-8910s...
-
@wyatt Put some AY-3-8910 music from X1, MSX, Mockingboard, or Atari ST on your website, and tell them you used a YM2149F SSG.
-
But I have now published my walk through of part of the AVR emulation of AY-3-8910/8912 devices.
Fascinating stuff.
https://diyelectromusic.com/2026/04/06/ay-3-8912-8910-hardware-emulation/
-
AY-3-8912/8910 Hardware Emulation
The 40-pin AY-3-8910 devices I’ve been playing with are no longer newly available, but they are pretty available if you are happy with a certain questionable quality (more on that here: Arduino and AY-3-8910).
But the slightly shorter version (with fewer general purpose I/O pins), the 28 pin AY-3-8912, seems a lot harder to find, despite being widely used at the time. At least, to find on its own – i.e. not already soldered onto a circuit board. There is apparently also an even smaller AY-3-8913 in 24-pin format and a few other lesser used options too. But the 8912 is the variant most often found in the ZX Spectrum 128, Amstrad CPC, and many home computers from the time.
A key modern option then is emulation and there is a very capable AVR emulation of the sound generator online including some on PCBs that directly fit within the 28-pin footprint of the original.
One is Yevgeniy-Olexandrenko’s avr-ay-board for the AY-3-8910, AY-3-8912 and YM2149F devices, with a 8912-compatible DIP-28 PCB design using an ATMega48.
Another is published on https://www.avray.ru/, but that appears to be the firmware only. There is a board that uses this firmware built for an ATMega8P here and another for a two-device (dual AY-3-8912 for 6 channel support is often called “Turbosound”) here.
I don’t believe either of these approaches emulate the general purpose IO pins of the 8912/8910, which might be an issue using them “as is” in a retro system. I know the ZX spectrum 128 uses the IO for example.
Some options for replacing original AY-3-8912 devices:
- vRetro’s AY-3-8912 replacement available from https://www.vretrodesign.com/. There is an option for the original plus a “Turbosound” (6 channel) version.
- Note this includes full IO support so really is a drop-in replacement for use in retro systems (my Harlequin 128K kit came with one for example). The chip I have uses the ATTiny1627 and ATTiny1614. I don’t believe the designs are open (that I’ve found). Curiously these two devices have a max clock of 20MHz (relevant later).
- Adaptor boards to allow use of an AY-3-8910 40-pin device in the AY-3-8912 28-pin footprint, e.g.: https://github.com/etomuc/CPC-AY-3-8910-to-8912-adapter
- RC2014 also provides a board for RC2014 systems using the emulator design from https://www.avray.ru/ in place of an original AY-3-8910: https://rc2014.co.uk/modules/why-em-ulator-sound-module/. This uses an ATMega48PA running at 27MHz (see schematic).
- At least one FPGA based replacement: https://retrolemon.co.uk/sinclair/256-ayfpga-dual-ay-turbosound.html
But perhaps my favourite so far is the slightly random, “building an AY-3-8910 out of discrete logic” that I must have a proper look at, at some point: https://github.com/mengstr/Discrete-AY-3-8910
This post looks at how AVR emulation of AY devices works in a little more detail and maybe take some starter steps to reproduce my own.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to microcontrollers, see the Getting Started pages.
AVR-AY-Board
The avr-ay-board is a fully open source design and can use an ATMega48, 88, 168 or 328. The ATMega328P is very commonly used on an Arduino Uno or Nano.
Full details, including a schematic, gerber files, BOM, and firmware are available here: https://github.com/Yevgeniy-Olexandrenko/avr-ay-board.
It shares a lot of firmware heritage with the source available from https://www.avray.ru/. Up until Feb this year there was a link back to the original source, but that has since been removed and it appears to have mostly diverged some time in 2022. There is a list of differences here, but much of the discussion that follows would probably apply to both versions of the code.
The only difference between the ATMega48, 88, 168 and 328 is the amount of memory. They are named for the amount of flash memory – 4K, 8K, 16K, 32K respectively. But otherwise they are functionally identical. Here is the key data from the datasheet:
Note that the ATMega328PB is an enhanced version of the ATMega328P which itself is a slightly lower power (as I understand things) version of the original ATMega328. Application note “AT15007: Differences between ATmega328/P and ATmega328PB” lists the full set of enhancements, but it includes additional UART, SPI, I2C, and timers, although it is essentially backwards compatible with the 328/328P.
All of this means that a standard Arduino Uno or Nano might be able to run the bespoke AY-3-8912 emulation firmware and with the appropriate pin connections might also be able to emulate an AY-3-8912 in another system. Naturally it will be physically larger than the original chip, but electrically it should all work fine.
The Circuit
The original ABR-AY-Board is relatively straight forward. The full schematic is available in the GitHub repository and shows the following:
- A minimal ATTMega48P support circuit (capacitors, oscillator, power, LED).
- Three low-pass filters to filter PWM to an audio output expecting a 1K load (apparently).
- A mapping onto the AY-3-8912 28-pin pinout.
There used to be a detailed “usage” section on the GitHub but that has since been removed, but from the published schematic (v1.5) the pins are accessed and mapped as follows.
AY-3-8912SchematicATMega48PArduino EquivalentD0-D5D0-D5PC0-PC5A0-A5D6-D7D6-D7PD6-PD7D6-D7A8MISO_A8PB4D12BC1BC1PD2D2BC2SCKPB5D13BDIRBDIRPD3D3CLOCKCLOCKPD4D4RESETRESETPC6RESETTESTMOSI_PWM_BPB3D11IOA7-IOA0N/CCOUT_C / PWM_CPB2D10BOUT_B / MOSI_PWM_BPB3D11AOUT_A / PWM_APB1D9CFG0PB0D8CFG1PD5D5RXPD0D0TXPD1D1VCCVCCVCCVCCGNDGNDGNDGNDThere is a UART header (TX, RX, VCC, GND), ICSP header (MISO, MOSI, SCK, RESET, VCC, GND), and two configuration jumpers (CFG0, CFG1).
One point that might cause issues mapping over to an Arduino Uno or Nano is that the avr-ay-board has a 27MHz oscillator, whereas the Arduino only runs at 16MHz. That will almost certainly need some looking at.
The Clocks
So about that 27MHz clock. The default Arduino has a 16MHz oscillator so could this run on an unmodified Arduino?
Looking through the code, there seems to be one specific mention of the CPU frequency:
; --------------------------------------------------------------------------
; Init Timer0
; --------------------------------------------------------------------------
; Fast PWM, TOP = OCR0A
ldi r16, (1 << WGM01) | (1 << WGM00)
OutReg TCCR0A, r16
ldi r16, (1 << WGM02) | (1 << CS00);
OutReg TCCR0B, r16
; 219512 Hz internal update clock
;ldi r16, (27000000 / (1750000 / 8) - 1)
;out OCR0A, r16
OutReg EEARL, YH ; set EEPROM address 2
sbi EECR, b0
InReg r18, EEDR ; load byte 2 from EEPROM to r18
OutReg OCR0A, r18 ; set PWM speed from byte 2 of EEPROM (affect AY chip frequency)In the commented out code, there is reference to 27000000. But then that appears to be replaced with code that is reading the PWM frequency from EEPROM.
At the start of the main file is the comment:
; ==============================================================================
; Configuration
; ==============================================================================
; EEPROM Config:
; byte 0 - Serial interface enable (1 - enabled)
; byte 1 - Parallel interface enable (1 - enabled)
; byte 2 - PWM speed depending on AY chip frequency and MCU clock frequency
; byte 3 - USART baud speed depending on MCU clock frequencyIn the build area there is the main hex firmware and then three configurations with the following contents:
; firmware/v1.0/compiled/config-1.75mhz.hex
:0500000001017A3AFF46
:00000001FF
; firmware/v1.0/compiled/config-1.78mhz.hex
:050000000101783AFF48
:00000001FF
; firmware/v1.0/compiled/config-2.00mhz.hex
:0500000001016B3AFF55
:00000001FFWe can see these differing in the values after 0500000. The next two bytes (0101) map to serial and parallel interface being enabled. These define how the AY-3-8912 registers can be accessed, either using the original device’s parallel data bus or via a newer serial link. The serial link can be used to send register-value pairs to the device rather than use a real AY compatible parallel bus interface.
Then there is a differing byte (7A, 78, or 6B respectively) which is pulled into the timer 0 frequency code and used to set OCR0A in the previous code.
Finally that last byte of the configuration (3A) relating to USART baud, which I infer from older comments in the README file will be 57600, but this is as yet unverified.
On studying the code, it quickly becomes apparent that the whole execution is optimised for specific MCU clock frequencies. This is particularly notable in the interrupt routines, for example, the following:
; ==============================================================================
; Parallel communication mode (BC1 on PD2/INT0, BDIR on PD3/INT1)
; ==============================================================================
ISR_INT0: ; [4] enter interrupt
sbic PinD, PD_BDIR ; [2/1] check BDIR bit, skip next if clear
rjmp LATCH_REG_ADDR ; [0/2]
; [ READ MODE ] (BC1=1, BDIR=0)
; --------------------------------------------------------------------------
; 350ns max to set data bus, 8 cycles to set
; 8 * 37ns = 296ns for 27MHz O.K.
; 8 * 40ns = 320ns for 25MHz O.K.
; 8 * 42ns = 336ns for 24MHz O.K.
; 8 * 50ns = 400ns for 20MHz !!!!
OutReg DDRC, BusOut1 ; [1] output -> low level on D0-D5
OutReg DDRD, BusOut2 ; [1] output -> low level on D6-D7Here we can see that running at 20MHz (for example) violates the timing constraint to respond with the data on the bus. Running at 24MHz, 25MHz and 27MHz appears to be fine. There are a number of other places in the code where similar comments have been made.
The conclusion seems pretty clear. A standard Arduino Uno or Nano running at 16MHz would not work. Something at 20MHz might do the job with some limits, but there is definitely a reason the board is using 27MHz.
There is a key issue however. AVR 8-bit microcontrollers are typically only specified for up to 20MHz operation. I’ve certainly not found any ATMega48 through to ATMega328 that has a higher frequency specification. There are some newer 8-bit devices that might stretch to 24MHz.
But I’m now wondering if the MCU is being overclocked on this board. It would appear, according to some superficial searching, that people have been overclocking AVRs for years…
Timer Configuration and PWM Output
The emulator is using PWM to produce audio from the AVR. The PWM channels/timers are allocated as follows:
AVR PinTimer OutputCompare RegisterTimerSystem ClockD4 / PD4N/AOCR0A0Channel AD9 / PB1OC1AOCR1AL1Channel BD11 / PB3OC2AOCR2A2Channel CD10 / PB2OC1BOCR1BL1The first timer, Timer 0, I’ve already mentioned, is used to set the basic internal system “clock” for the emulation. In the real AY PSG the internal clock for tones is set to the external clock / 16 and for envelopes is external clock / 256. In the emulation this is all set in code and the CLOCK input is ignored.
The 8-bit Timer 0 configuration is as follows:
- TCCR0A/TCCR0B = WGM00 | WGM01 | WGM02 | CS00
- OCR0A = value from EEPROM (as mentioned previously)
This uses timer 0 in Fast PWM mode (WGM = 7) with TOP = OCR0A and no prescaler. There is a check in the main loop for Timer 0 overflow which is then used to determine if the sound generation should be processed. This effectively sets the CLOCK for the emulation. The AY CLOCK input is meant to be between 1MHz and 2MHz and EEPROM configurations are provided to emulate 1.75MH, 1.78MHz and 2.0MHz external clocks.
In Fast PWM mode, from the ATMega328 datasheet, for Timer 0, the frequency is given by:
- FreqPWM = FreqCLOCK / (N * 256)
Where N is the prescaler factor, so in this case 1. But this appears to be stated for the case when TOP = 255. If the TOP is reduced, so when TOP = OC0A, then presumably that 256 should be (TOP + 1). Assuming this to be the case, then with a 27MHz clock and the previously mentioned values from the EEPROM, we have
- 0x7A = 122 -> timer freq = 27 MHz / 123 = 219.5 kHz
- 0x78 = 120 -> timer freq = 27 MHz / 121 = 223 kHz
- 0x6B = 107 -> timer freq = 27 MHz / 108 = 250 kHz
These give a basic operating frequency of between 3.5MHz and 4MHz which is twice as fast as the real PSG. I’m guessing that this is because the sound generation code (later) toggles the output value on each period, which therefore requires doubling the frequency to generate the high and low periods.
Note that although this timer is configured in Fast PWM mode, it isn’t actually running PWM, it is just used as a timer. The timer also never “triggers” as such – it is polled for overflow within the main code loop.
The 16-bit Timer 1 configuration:
- TCCR1A/TCCR1B = WGM10 | WGM12 | COM1A1 | COM1B1 | CS10
- PB1 and PB2 set to OUPUT for PWM out on OC1A and OC1B.
This uses timer 1 in 8-bit Fast PWM mode (WGM = 5) with TOP = 0xFF. There is no pre-scaling and OC1A/OC1B cleared on compare match with OC1A/OC1B set at BOTTOM (non-inverting mode).
The 8-bit Timer 2 configuration:
- TCCR2A/TCCR2B = WGM20 | WGM21 | COM2A1 | CS20
- PB3 set to OUTPUT for PWM out on OC2A.
This uses timer 2 in Fast PWM module with TOP = 0xFF (WGM = 3). OC2 is clear on match and set at BOTTOM (non-inverting again).
The PWM resolution for both Timer 1 and Timer 2 will be 8-bits (0 to 255) and the frequency for the output is given by the same formula used for Timer 0, giving a PWM frequency of:
- FreqPWM = 27 MHz / (255+1) = 105.5kHz
Bus Access
There is a comprehensive bus access protocol defined in the AY-3-8910/12 datasheet with several possible modes involving the control signals BC1, BC2, BDIR, and A8 plus /A9 (in the case of the 8910). In particular, there is some redundancy in how the “LATCH” is indicated (see section 2.3 in the “AY-3-8910/8912 Programmable Sound Generator Data Manual” – more here: AY-3-8910 Experimenter PCB Design).
For the emulator, BC1 is tied into the AVR INT0 (via PD2/D2) and BDIR is tied into AVR INT1 (via PD3/D3). If the serial interface is used then there is also an interrupt for the UART. BC2 is ignored so all responses are enacted upon as if BC2 is set to HIGH.:
The basic operation, as far as I can see, is as follows:
INT0 ISR - triggered on BC1 -> 1
IF BDIR == 0 // BC1=1; BDIR=0: Read data
Set data lines to OUTPUT
Write BusOut to the data bus
WAIT for BC1 -> 0
Set data lines to INPUT
ELSE BDIR == 1 // BC1; BDIR=1: Latch Address for read
Read ADDR from data lines
Grab value from pseudo register in RAM into BusOut
INT1 ISR - triggered on BDIR -> 1
IF BC1 == 0 // BC1=0; BDIR=1: Write data
Read DATA from data lines
Store value to pseudo register in RAM
ELSE BC1 == 1 // BC1=1; BDIR=1: Latch Address for write
Read ADDR from data linesThere is a pseudo image of all AY registers stored in RAM which is used by the main loop for the sound processing. This RAM image is updated when the AY is written to and can be accessed when the AY is read.
Note that there is no access control. If an interrupt comes in part way through an update to the sound generators they will stop process and then continue from that point unaware than any register updates have taken place. This does mean that if any registers are accessed twice, it is quite possible that they would have changed by the time of the second access.
Similar logic happens within the UART interrupt handler, but instead address and data values are obtained over the serial port and interactions with RAM updated according to the bytes recieved.
I’ve not looked further into the serial handling at this time other than to note that all updating is performed withing the UART ISR which is receive only.
Main Sound Processing Loop
The main logic free runs as follows:
MAIN Loop:
IF Timer 0 Overflow Flag is SET:
Process envelope generator
Process noise generator
Process tone generator for channel A
Process tone generator for channel B
Process tone generator for channel C
Process mixer control
Process amplitude control for channel A
Process amplitude control for channel B
Process amplitude control for channel C
Update PWM values in OCR1AL (ch A), OCR2A (ch B), OCR1BL (ch C)So the loop essentially pauses until timer 0 overflows at which point all sound generation activity undertakes a single scan and then the PWM sound generation registers are updated.
Before I dive in, I should note that the register definitions are of the form AY_REGnn where nn is a decimal from 00 to 15. The datasheet describes Ro where o is an octal value from R0 to R7, then R10 to R17. I will be using the decimal versions here to match with the code.
I’m not going to work through how the sound generation works in its entirety right now, but I will just include a note about the tone generation. Here is the code for channel A.
; Channel A
subi CntAL, 0x01 ; CntA - 1
sbci CntAH, 0x00
brpl CH_A_NO_CHANGE ; CntA >= 0
lds CntAL, AY_REG00 ; update channel A tone period counter
lds CntAH, AY_REG01
subi CntAL, 0x01 ; CntA - 1
sbci CntAH, 0x00
eor TNLevel, ZH ; TNLevel xor 1 (change logical level of channel A)
CH_A_NO_CHANGE:All counters (CntAL and CntAH for channel A) are 16-bit values. AY_REG00 and AY_REG01 are the RAM copies of the two tone generator registers for channel A.
We can see that the channel counter is decremented on each scan through the routine implementing the following pseudo code:
counter--
IF (counter == 0):
Reset counter from AY_REG00 and AY_REG01
Toggle logic level for channel AThis means that the output square wave value for channel A will toggle between HIGH and LOW every time the counter reaches zero and that the counter will have to count to zero twice to make a complete cycle of the square wave.
The datasheet states that the tone registers define a 12-bit tone generator period:
The resulting sound frequency is given by the equation:
As we have to count twice to get our square wave output, we can see why the timer 0 “clock” frequency has to be twice the desired running CLOCK of the AY-3-8912. An alternative implementation could have been to add an additional check as part of the countdown to change the waveform half-way through.
This does mean that all sound generator registers are processed twice as quickly as expected so that might have to be taken into account when calculating other parameters.
The main impact would be for the envelope generator, which according to the data sheet runs a frequency CLOCK / 256. There is a EG period counter defined by AY_REG11 and AY_REG12 (R13 and R14) for further subdivision, so a full sweep of the envelope will happen with a frequency of CLOCK / (256 * EGcounter).
The datasheet also notes that “the envelope generator further counts down the envelope frequency by 16 producing a 16-state per cycle envelope pattern”. This means that the frequency required to process each individual step of these 16 states is: 16 * CLOCK / (256 * EGCounter) or CLOCK / (16 * EGCounter).
The Envelope Code is essentially doing the following:
Every LOOP scan:
IF env reg updated:
Reinitialise EG
Reset EGCounter from AY_REG11, AY_REG12
EGPeriod = 31
Re-enable EG
ELSE
IF EG enabled:
EGCounter --
IF EGCounter == 0:
EGPeriod --
IF EGperiod == 0:
EGPeriod = 31
Disable EG
Reset EGCounter from AY_REG11, AY_REG12
Eval = Envelope valueThis implies that a full sweep of the envelope takes EGcounter * 32 scans of the main LOOP. As the LOOP is running at twice the frequency of the CLOCK. this gives an EG frequency of:
- LOOP / (32 * EGcounter) = CLOCK * 2 / (32 * EGCounter) = CLOCK / (16 * EGCounter)
Which matches the datasheet. So the doubling of the LOOP frequency is taken into account by having 32 steps for the EG base period rather than 16.
A few other observations from the code:
- The envelope period counters are stored in AY_REG11 and AY_REG12 (R13 and R14) and managed using CntEL and CntEH via r26 and r27.
- The line ‘sbiw CntEL, 0x01’ is a 16-bit instruction and so acts on both r26 (CntEL) and r27 (CntEH) at the same time as a HIGH/LOW 16-bit pair.
- The counter updating currently happens at the LOOP frequency which is 2 * CLOCK. Every time the counter hits 0 the step is advanced. It therefore takes 32 * counter passes through the LOOP to process the entire cycle of the envelope.
- The envelope period is managed using TabP which is initilised to 0x1F (31 decimal).
- Whenever the envelope counter reaches 0 the next envelope period is selected via TabP.
- The resultant envelope value is stored in Eval which is used as the maximum value for the amplitude calculations later in the loop.
Curiously there are two envelope volume tables provided as options. The first (“AY_TABLE”) distinctly shows the doubling of levels turning 16 values into 32 values. The second (“YM_TABLE”) appears to have some interpolation between values giving a higher resolution envelope.
From wikipedia: “The input clock is first divided by 16 (or by 8 in the YM2149, because the envelope generator has twice as many steps, and thus needs twice as many clocks to complete a full cycle), and then by the 16-bit value.”
So by starting with double the CLOCK frequency, we effectively get YM compatibility “for free”.
Only a single envelope table is required – it defines a linear incrementing pattern that is then used and reused in various ways according to the EG control bits in AY_REG13 (R15) as per figure 7 in the datasheet. I’m not going to dig into that further at this point. I might come back to it in the future.
I’m also not going to dig into the noise generation, mixer or amplitude control at this time.
Closing Thoughts
I really like I feel I know a lot more about how the AY-3-8910/8912 work now and certainly am, as usual, in awe of those who figured all this out and then how to emulate it on a modern (ish) microcontroller.
It is also interesting to note that the emulation hasn’t been updated, as far as I know, for anything more capable than an 8-bit AVR. I guessing it just isn’t necessary and avoiding the whole 3V3/5V logic thing has a certain appeal.
The two commercially available solutions I’ve seen from RC2014 for the WhyEm sound card and the vRetro 28-pin direct replacements, stick with AVR and overclock as far as I can tell. RC2014 using ATMega48AP at 27MHz and vRetro using two ATTiny MCUs and what I think is a 30MHz oscillator.
I would like to see if I can get a standard ATMega328P running the code and then I’d be really interested in seeing if it could be made to run on a Logic Green LGT8F328 “AVR clone” that apparently should be able to run at 32MHz.
I believe I’ve convinced myself not to attempt to build the AY-3-8910 out of discrete logic…
Kevin
#arduino #atmega328 #avr #ay38910 #ay38912 #vretro - vRetro’s AY-3-8912 replacement available from https://www.vretrodesign.com/. There is an option for the original plus a “Turbosound” (6 channel) version.
-
Note to self:
"I do not need to build an AY-3-8910 out of discrete logic chips."
"I do not need to build an AY-3-8910 out of discrete logic chips."
"I do not need to build..."https://github.com/mengstr/Discrete-AY-3-8910
But how cool would that be!
-
If you haven't seen it yet: https://ym2149-rs.org is a great new cycle-accurate YM-2149F web player with tons of music from the Atari ST, written in Rust by slippyex (see https://ym2149-rs.org for more details) #ym2149 #ay38910 #atarist #chiptune
-
A quick selection of web-based music trackers:
https://www.stef.be/bassoontracker/
https://bitphase.app/
https://keithclark.github.io/ZzFXM/tracker/
https://www.igorski.nl/application/efflux/#music #demoscene #chiptune #modules #tracker #sound #ay #ay38910 #ym2149
-
I've had another look at the Follin archive and spent a bit of time trying to understand the tester music player for the ZX Spectrum for one of Tim Follin's tracks, using Ste Ruddy's sound driver...
I think I'm getting a bit more of an idea of how neat this all is :)
https://diyelectromusic.com/2026/03/02/z80-and-ay-3-8910-part-2/
-
Z80 and AY-3-8910 – Part 2
I’ve spent a bit of time looking at the “Tester” part of the AY driver code for Tim Follin’s music archive that I talked about in Z80 and AY-3-8910.
This is documenting what I think I’ve worked out so far for the tester code.
The Sound Tester
As previously mentioned, there are essentially three parts to the code in Follin archive:
- The tune and effect data.
- Ste Ruddy’s Sound Driver.
- A tracker-style (ish) tester UI application.
The first part looked at the sound driver itself, and essentially skipped over the tester part of the code. This post picks up on that tester code.
Reminder, from part one, the main structure is as follows:
Code_Start: EQU 40000
Data_Start: EQU 50000
;-----------------------------
ORG Code_Start
; The UI/tester code
TESTER:
LOOP: Calls the following for each scan:
HALT - Suspends until an interrupt comes in?
CALL UPDATE
CALL REFRESH
CALL CLOCK
CALL KEYSCAN
Repeat as necessary
KEYSCAN: UI scanning
CLOCK: Possibly maintain a 50Hz refresh rate clock?
UPDATE: Loads the internal state of all sound variables from
the driver and displays them in real time via the UI.
; The sound driver
CODE_TOP:
TUNE: Select which tune to play.
TUNE_IN: Init all internal sound state variables for a new tune.
TUNEOFF: Stop a playing tune, eg to change tune or start an FX.
FX: Start playing an FX.
FLOOP: Keep processing FX instructions until complete.
REFRESH: "run" a scan of the sound driver updating and outputting the soundThe Tester Code
Initialisation information and main screen data:
;**************************************
; Z80 AY MUSIC DRIVER
;**************************************
; ORG 40000
; LOAD 0C000H
;======================================
;STACK DEPTHS
SD: EQU 3
;======================================
ASCII: EQU 23560 ; 23560 = $5C08 = System Variable "LAST K"
TESTER: PUSH AF
PUSH BC
PUSH DE
PUSH HL
XOR A ; ASCII = MINS = SECS = 0
LD (ASCII),A
LD (MINS),A
LD (SECS),A
CALL TUNEOFF ; TUNE initialisation
CALL STACKMESS ; Kick off the Tester code!
DB CLS ; The start of the main UI data
DB AT,0,0
DB INK,01010111B
DB "'AY' MUSIC DRIVE"
DB "R V2 BY S.RUDDY"
... Skip ...
DB INK,64+5
DB "VOLUME "
DB " "
DB 255
... Skip ...
AT: EQU 22
INK: EQU 16
CLS: EQU 15
STACKMESS: POP IX
CALL MESS
JP (IX)There is a whole lot of screen data in DB blocks which includes some “op codes” that are defined later: AT, INK, CLS. These are special codes that are used by the ROM-based print routines (more here), as used by Sinclair BASIC, but in this case they are spelt out directly, later in code. The final 255 signifies the end of the screen data.
So how are these definitions handled? That all comes up in the “MESS” routine I’ll get to in a moment, but first that “STACKMESS” routine needs a bit of explanation.
When a CALL instruction happens, such as the CALL STACKMESS at the start, the current program counter gets pushed onto the stack. In this case the current PC will point to the instruction after the CALL, which happens to be the start of the screen data. So the POP IX will grab the address of the screen data and drop it into IX and then call the “MESS” function to actually get on with it!
But before I get to that, there is some more code after the screen data:
LD HL,CALC1
PUSH HL
LD A,H
LD DE,4067H ; Output high byte
CALL HEX
POP HL
LD A,L
LD DE,4069H ; Output low byte
CALL HEX
LD HL,(CALC2)
PUSH HL
LD A,H
LD DE,4071H ; Output number of Tunes
CALL HEX
POP HL
LD A,L
LD DE,4073H ; Output number of effects
CALL HEX
LD HL,CALC1
LD DE,(CALC2)
ADD HL,DE
PUSH HL
LD A,H
LD DE,407CH ; Not entirely sure what this is outputting...
CALL HEX
POP HL
LD A,L
LD DE,407EH
CALL HEXThis is writing some basic data out to the display. CALC1 seems to relate to code section size. I believe CALC2 is the start address of the tune data, which is the following:
ORG Data_Start
TUNES: EQU 5
EFFECTS: EQU 21All three of these sections are outputting a 16-bit value in two single-byte chunks using the “HEX” routine, which takes a screen address (in the range $4000-$57FF) and outputs a hex number at that screen location.
So while I’m at it then, how is that HEX function working?
;--------------------------------------
HEX: INC DE ; DE contains the screen address to use
PUSH AF ; Start with DE+1
CALL ONEnib ; Write out the LOW 4-bits
POP AF
RRA ; A = A>>4
RRA ; to write out HIGH 4-bits
RRA
RRA
DEC DE ; Back to original DE screen address
ONEnib: AND 15 ; A = A & 0xF
ADD A ; BC = A * 2
LD C,A
LD B,0
LD HL,ROM_TAB ; Read from ROM_TAB[BC]
ADD HL,BC
LD A,(HL)
INC HL
LD H,(HL)
LD L,A ; HL = (uint16_t)ROM_TAB[A]
MIKESbug: LD C,D ; So HL now points to character bitmap in ROM
LD B,8 ; Write out 8 bytes to display memory directly
PRloop: LD A,(HL) ; (DE) = (HL)
LD (DE),A
INC HL ; HL++
INC D ; NB: Layout of display mem means D++ is next line of char
; for same value of E.
DJNZ PRloop ; WHILE (B-- > 0)
LD D,C ; (Restore D before returning, so DE still = screen addr)
RET
ROM_TAB: DW 3D80H ; ROM character set: 3D80 = "0"
DW 3D88H ; Each char = 8 x 8 bits
DW 3D90H
DW 3D98H
DW 3DA0H
DW 3DA8H
DW 3DB0H
DW 3DB8H
DW 3DC0H
DW 3DC8H ; = "9"
DW 3E08H ; = "A"
DW 3E10H
DW 3E18H
DW 3E20H
DW 3E28H
DW 3E30H ; = "F"This is making use of the character set stored in the Spectrum ROM (more here) which is indexed via a 16-word jump table mapping the characters onto each of the 16 hex characters: 0..9, A..F.
Then each byte, 8 in total, of the character is written directly out to the Spectrum screen memory taking advantage of the odd formatting of the screen memory to easily skip to the next line of the display for each line of the character (more here).
So before I get into the main update loop, how the screen initialised and set up? That happens in the “MESS” and some ancillary functions.
MESS: LD A,(IX+0) ; At this point, McursorX, McursorY = (0,0)
INC IX ; So read a byte of screen data
OR A
RET M ; Stop IF A=255 (i.e. negative)
CP 32
JR C,Mcontrol ; IF A<32 process control character then RET back to "MESS"
CALL Mgetchar ; ELSE Process character
CALL Mgetaddr ; Get screen address for next output in DE
CALL MIKESbug ; Output the character
CALL PRattr ; Set the colour attributes
CALL INCcursor ; Update the screen position for the next byte of screen data
JR MESS
Mcontrol: LD HL,MESS ; Stick the address of "MESS" on the stack for the RET
PUSH HL
CP 15 ; IF A == CLS
JR Z,Mcls
CP 22 ; IF A == AT
JP Z,Mat
CP 16 ; IF A == INK
JR Z,Mink
RET ; RETurn to "MESS"
Mcolour: DB 0 ; Working variables for cursor position and colour
McursorX: DB 0
McursorY: DB 0 ; Has to be directly after McursorX (see later)
Mink: LD A,(IX+0) ; Process INK to set colour
INC IX
LD (Mcolour),A
RET
Mcls: LD HL,4000H ; Process CLS to clear screen
LD (HL),L
LD DE,4001H
LD BC,1AFFH
LDIR
LD (McursorX),BC
RET
INCcursor: LD HL,McursorX ; Moves the cursor on one position
LD A,(HL)
INC A
AND 31
LD (HL),A ; X++; X = X % 32
RET NZ ; IF X==0; Y++
INC HL ; Assumes McursorY is McursorX++
INC (HL)
RET
Mgetchar: LD L,A ; HL = A*8 + 3C00
LD H,0 ; Note: A > 32; where 32="Space"
ADD HL,HL ; In ROM, space is address 3D00
ADD HL,HL ; 32 * 8 = 0x100
ADD HL,HL
LD BC,3C00H
ADD HL,BC ; HL = Start address of character map for char in A in ROM
RET
.... skip ....
Mgetaddr: LD A,(McursorY) ; Calculate the screen address for (McursorX, McursorY)
AND 18H
OR 40H
LD D,A
LD A,(McursorY)
RRCA
RRCA
RRCA
AND 0E0H
LD E,A
LD A,(McursorX)
ADD E
LD E,A
RET ; DE = required screen address
Mat: LD A,(IX+0) ; Set cursor to provided X, Y in screen data
LD (McursorX),A
INC IX
LD A,(IX+0)
LD (McursorY),A
INC IX
RET
PRattr: LD A,D ; Get address of ATTRibute memory
RRA
RRA
RRA
AND 3
OR 58H
LD D,A
LD A,(Mcolour)
LD (DE),A ; And set the colour
RETBasically this loop keeps working on the provided screen data until the value 255 is found, at which point it returns. There are two paths for handling the data:
- IF the value is < 32 then it is a control value. Only CLS, AT and INK are recognised.
- ELSE the value is assumed to be an ASCII character and is displayed.
Whatever is happening, happens at the coordinates given by (McursorX, McursorY) which start out as (0,0) and get updated automatically when a character is output, or in response to an AT command. INK will set the required colour in Mcolour, which again starts out as 0. This is applied after the character is written to the screen, using the PRattr function.
There is a fun bit of optimisation going on in Mcontrol. At the start it pushes the address of the MESS function on the stack, which means that the RET will jump back to the start of MESS rather than where the jump happened to Mcontrol itself.
There is another shortcut in the Mcls function: LDIR. From http://z80-heaven.wikidot.com/instructions-set:ldir: “Repeats LDI (LD (DE),(HL), then increments DE, HL, and decrements BC) until BC=0.” By setting the contents of HL (the first byte of the display) to zero, this will tile that same value across the display memory until BC, which starts at $1AFF, is zero. This will zero the whole display – both pixels and attributes – from 0x4000 through to 0x5AFF.
Now finally, we get to the main update loop.
LOOP:
HALT
CALL UPDATE ; Update the display from the current Sound parameters
LD A,2
OUT (254),A ; Set border to 2
CALL REFRESH ; Update the sound driver parameters
XOR A
OUT (254),A ; Set border to 0
CALL CLOCK ; Run 50Hz clock
CALL KEYSCAN ; Guess what - scans the keyboard 🙂
LD A,07FH
IN A,(254) ; Reads 0x7FFE which is the bottom row of the keyboard
AND 1
JP NZ,LOOP ; Checks bit 0, which is the SPACE key
LD BC,65533 ; AY OUTPUT PORTS (FFFD, BFFD)
LD A,7
OUT (C),A
LD BC,49149
LD A,63 ; Set AY register 7 to 63 - i.e. all channels OFF
OUT (C),A
POP HL
POP DE
POP BC
POP AF
RETI’m not going through the sub routines of the loop, other than to note the following:
- UPDATE is a whole series of instructions that basically do the following to output the HEX value of a sound parameter:
LD A, (contents of one of the sound variables)
LD DE, (corresponding screen address for the variable to be displayed)
CALL HEX- REFRESH runs the sound driver itself, as described in Z80 and AY-3-8910.
- CLOCK decrements the FIFTY variable and every time it gets to zero updates SECS and MINS and writes them out to the display. As it also uses the HEX routine, I guess it is storing the time using binary-coded decimal (BCD).
- KEYSCAN reads the last key pressed from the system variable location stored in ASCII (23560 / 0x5C08).
At some point I might come back and work out what keys do what…
Closing Thoughts
I’d really like to get some of this code running on some of the alternate Z80 platforms I have. Getting the sound output shouldn’t be too much of an issue, but I’d really like to have some kind of display too.
But as can be seen above, the tester UI is pretty well tied into the oddities of the ZX Spectrum display, so porting it won’t be trivial.
I suspect there are already some existing AY/chiptune players that perhaps would be a better starting point, but from what I’ve seen they tend to stream the register data after having sampled it at regular intervals, which isn’t quite what I was after… there would be something really quite interesting about actually running Ste Ruddy’s Sound Driver with a Tim Follin soundtrack programmed in.
Kevin
#ay38910 #TimFollin #zxSpectrum -
YM2149-rs - Chiptune Sound Synthesis in Pure Rust
https://ym2149-rs.org/
https://github.com/slippyex/ym2149-rs#sound #audio #chiptune #rust #synthesis #ay38910 #ym2149 #opensource #foss #yamaha #demoscene #gamedev
-
RC2014
As I mentioned in RC2040 earlier last year I finally got myself a RC2014 – something I had been planning on doing for ages. From the website:
“RC2014 is a simple 8 bit Z80 based modular computer originally built to run Microsoft BASIC. It is inspired by the home built computers of the late 70s and computer revolution of the early 80s. It is not a clone of anything specific, but there are suggestions of the ZX81, UK101, S100, Superboard II and Apple I in here. It nominally has 8K ROM, 32K RAM, runs at 7.3728MHz and communicates over serial at 115,200 baud.”
I’ve had a lot of fun with it over the last 9 months or so, but only a small amount of that has made it to my blog. Mostly because I’ve been catching up with stuff the community has already been doing for some time, so didn’t really feel like I had much that was unique to say.
I’m still not sure I’m at the point where I’m adding to the global knowledge pool of RC2014, even by my “reinventing wheels” standards, but I am at the point where I need to start making some “notes to self” about it, so I thought it was about time to start a proper post on the topic.
RC2014 Classic II
I got myself a RC2014 Classic II figuring that would be a suitable outlay to get started, and it was a good choice for me. Enough going on to get interesting, but not too expensive to start with.
This is made up from the following:
- Backplane 8
- Z80 CPU (a variant of this one) fitted with a Zilog Z84C0010PEG, which can go up to 10MHz.
- Banked ROM – a 64K 27C512 ROM providing 8 ROM banks selectable via jumpers. It comes pre-programmed with ROM R0000009 (details here). The R indicates Microsoft BASIC in bank b000 and the 9 indicates the SC Monitor in bank b111.
- 32K RAM – a single 62256 SRAM chip mapped onto address 0x8000.
- Serial I/O (a variant of this one) – a single serial port, running at 115200 (assuming the standard 7.3728 MHz clock for the system), based on the MC68B50.
- Clock and Reset – the aforementioned 7.3728MHz clock plus a reset switch.
- 5V, USB-C, FTDI module from 8086 (optional). More details here.
The modules provided in the Classic II are not available on their own, as far as I can see, only as part of the Classic II kit.
I’ve made the following small customisations:
- As can perhaps be seen in the photo, I have a simple 3D-printed “buffer” for the PCB to sit in.
- I’ve added the 7805 regulator so it can be powered by 7-12V via the barrel jack instead of the 5V via the FTDI link.
- I went and connected up the additional slots on the backplane, as described in an earlier version of the page for the kit on rc2014.co.uk. But then realised that later versions of the PCB has all slots connected by default. Spencer has since updated the page to say that this is no longer necessary 🙂
- I’ve put in a higher value resistor as I found the LED very bright! The original specifies 330R. I ended up with 5K.
VGA Terminal
A mentioned in a previous blog post, I spent a bit of time trying out the Raspberry Pi as a serial terminal and managed to get something working quite nicely. But eventually I caved in and picked up the RP2040 VGA terminal. This works really well, but of course the issue is that is requires a VGA monitor.
I found a pretty cheap, small, and neat display that in addition to USB-C power and HDMI input also incorporates a VGA input. Interestingly it also has a composite video input. Mine had a typical key-word heavy title of “7 Inch Portable Display IPS 1024×600 LCD HDMI-compatible VGA AV Input DC Type C Power In for PC Laptop Camera TV Box DVD Screen” on an overseas marketplace and can typically be found for around £30-£35. I found that 7″ is quite a nice size for the RC2014 text.
As I’m still using the serial link to my laptop for a keyboard, the UART jumpers on the RP2040 VGA must be set as follows:
- TX: UART 1
- RX: Not connected
Without changing the RX jumper the keypresses over the serial link are not registered. I’m guessing this is because two things are trying to drive the serial I/O bus. I’m not sure if this is an officially supported configuration, I expect it is assumed that a USB keyboard would be used, but it seems to work for me. Shown below.
Audio Boards
I have a few different audio related boards:
- Ed Brindley’s RC2014 YM/AY Sound Card. This allows the addition of an AY-3-8910 or the Yamaha equivalent, programmable sound generator chip to your RC2014. Assuming you can get a chip of course. There are some great notes on this board here and some additional notes on using them with an Arduino here. Note: I have revision 5, not the latest as shown in the repository.
- Why Em-Ulator Sound Module. This is basically the same as the above card, but uses an AVR chip to emulate the AY-3-8910 which completely side-steps the issue of how to get hold of a device. One really neat feature is the addition of a 40-pin DIP socket for purely cosmetic reasons. If you have an old 40-pin DIP chip you can stick it over the emulator circuit and it will look like the real thing!
- SID-Ulator Sound Module. This is an equivalent card for the (in)famous SID chip (Sound Interface Device) as used on the Commodore 64. I never had a C64, but have watched some of the really cool stuff that Shiela Dixon does with the SID, so am looking forward to having a play at some point.
- MG005 Speech Synthesiser. This is a RC2014 board from Mr Gelee’s Tech, for the SP0256A-AL2 speech synthesizer chip (there are some notes here). It is actually quite a lot of fun to play around with.
- MIDI Module, designed for RC2014. This is a MIDI module by Shiela Dixon based on the same 68B50 device used on the standard serial I/O module.
Miscellaneous Other Boards
The RC2014 is definitely one of those systems, for me at least, where it is very tempting to try to get “one of everything”. And following the RC2014 Assembly last year, I’ve a few additional boards stacked up that I’ve been playing around with a bit.
- Digital I/O – This provides 8 LEDs as OUTPUTs and 8 switches as INPUTs, which can be accessed via the IN/OUT instructions to Port 0.
- MG017 I2C Interface – This is another module from Mr Gelee’s Tech that provides a link between the wide range of I2C devices that exist for an Arduino environment and the RC2014. It does that by basically including an Arduino on the board.
- RC2014 Assembly Badge v1.0 – Having visited the RC2014 Assembly last year, I came away with the event badge, which itself is pretty neat too. There is a Hackaday write-up here.
- MG008 ZX Spectrum Peripheral Adaptor – This allows some ZX Spectrum hardware peripherals, that would usually plug into the edge connector of a Spectrum, to possibly be used with an RC2014 system. I would love to get some of my old Spectrum devices recognised, but this will be quite a lot of work to get going. This is on the “bigger” things to-do list.
I also have a 5-way backplane and additional power/reset module from SCC.
Other RC2014 Systems
Not content with the basic Classic II, I also have the following which I keep tinkering with in various combinations.
- RC2014 Micro – This is the main Z80 CPU, ROM, RAM, clock and reset on a single card. But otherwise is essentially the same as the Classic II in functionality. It is particularly pleasing how all the chips are packed onto this single RC2014 module.
- RC2014 Mini II Picasso – I kept looking at these ever since they first came out and resisted. But then one day caved in, and I’m really glad I did. I love this little board. It is a reworking of the RC2014 Mini II, which itself is another version of the Classic II or Micro functionality but in a single module.
Small Computer Central
As well as the original RC2014 there is a whole range of compatible, extended and expanded devices out there that started with the Z80 and RC2014 bus standard. One particular set of extensions is based around standardising extensions to the bus in a way that allows for up to 40 additional signals.
Small Computer Central is the home of the SC Monitor programme that comes with the RC2014 as well as a wide range of computers and modules supporting the various RCxx bus standards: RC2014, 40-pin RCBus, and 80-pin RCBus. The standards are defined here.
After meeting Stephen at the RC Assembly, and coming away with some of the SCC boards, I’ve been experimenting with some alternatives to my initial Classic II setup. Here is my SCC-board RC2014 equivalent.
On the left, my original RC2014 Classic II modules. On the right, my SCC replacement modules:
- SC149 Z80 Module (RC2014) – has a built-in 7.3728MHz clock source onboard and a Zilog Z84C0008PEG CPU.
- SC150 Paged RAM Module (RC2014) – this is a 128K paged RAM module meant to be paired with the equivalent paged ROM module. The expanded memory compared to the original Classic II should allow it to run CPM.
- SC151 Paged ROM Module (RC2014) – the 128K paged ROM module supporting the paired RAM module. Based on a SST39SF010A 128K Flash ROM.
- SC139 Serial 68B50 Module (RC2014) – a replacement serial I/O module that has the option of its own clock and more address options.
Together, the expanded ROM and RAM should allow the Classic II to run CPM once some additional storage is provided. For me that will be in the shape of:
- SC604 Compact Flash Module – This should allow me to be able to use compact flash as the “disk” for a CPM installation. strictly speaking this is a RCBus module, but it states RC2014 compatibility too. But I’ve not built it yet. Watch this space…
These are all part of the SCC RC2014 compatible range. There are other ranges for RCBus based on the Z80 and Z180 in a range of form factors.
RC2014 Emulation
As well as a range of actual Z80 based computers, as code exists to emulate the Z80 on more modern microcontrollers (usually) there are a number of projects that have popped up with kits that can emulate the RC2014.
I have the following that I’ve been playing with:
- RC2040 – This is an emulation of a CPM compatible RC2014 system, running on a Raspberry Pi Pico. My notes about getting this up and running can be found here: RC2040.
- Pico RomWBW – A version of the RC2040 geared up to run RomWBW. This is particularly nicely packages, especially if you go for the wooden box.
Software
The basic system comes with Microsoft BASIC and the SC Monitor. Two common aims for these systems are to run RomWBW or CPM (although RomWBW is another monitor that also allows running CPM – so is sort of a superset of the others as I understand things).
Options for running CPM from here:
- Get a Classic II CPM Upgrade kit – this expands the memory and adds CF storage, but reuses many of the parts from the original Classic II.
- Get a Mini II CPM Upgrade kit (and use it with my Picasso) – this is an second board to add to the Mini II with everything that is required.
- Use the SCC RAM/ROM replacements for my Classic II with the CF Storage.
- Use RC2040 or Pico RomWBW.
Emulation would get me going, but I want to get a non-emulated system up and running too. For now that means working on the SCC modules, which to be honest, was essentially why I got them in the first place.
Conclusion
Getting your first RC2014 style kit starts a journey down a bit of a rabbit hole, but it has been a lot of fun so far. The peak was the RC2014 Assembly last year and seeing what so many others are getting up to.
But if you’ve read this far, you’re probably thinking something along the lines of “wait, this is all just building modules – has he actually done anything with them?” And you’d essentially be right. In one way the writing of this blog post is partly to avoid actually getting on with something with the things I’ve now built.
But I do have a few aims of what to explore next, so assuming the agony of choice isn’t too much, leading to another blog post in support of continued procrastination, here are some of the ideas I’ve had kicking around for the past 9 months or so:
- Play with the Pico RomWBW that I got from the Assembly. I’ve built it but not really used it.
- Get the SCC alternative modules up and running and CPM installed on some “real” hardware.
- Have a proper look at the SID-Ulator. I’ve already had a bit of a play with the Why Em-Ulator, but only as a starting point.
- Get MIDI up and running. I’ve had the MIDI board for a while now, but haven’t really done anything with it yet.
- Do some proper music related stuff with the AY-3-18910/Why Em-Ulator.
- And at some point I’d like to build my own module to get a feel for how things like address decoding all work.
So, watch this space. But don’t wait 🙂
Kevin
-
Кросс-трекеры: ретро-музыка на современном ПК
Я не раз обращался к теме музыкальных редакторов системы «трекер». Казалось бы, сколько можно, горшочек, не вари. Но этих программ насчитывается сотни, и несмотря на сходство до степени смешения, созданы они с разными намерениями, посвящены решению различных задач, а к их появлению привели исторические причины разной степени занимательности. В то же время, эта нишевая тема, развивавшаяся десятилетиями, почти не имела выхода за пределы специализированных сообществ в формате обзорных публикаций для массового читателя. А значит, можно и нужно продолжать её раскрывать. Сегодня уделю пристальное внимание явлению «кросс-трекеров» — программ для современных ПК и операционных систем типа Windows и Linux, позволяющих создавать музыку для различных старых компьютеров, игровых приставок и прочих подобных устройств, а точнее, для их музыкальных синтезаторов. Зачем, почему, что происходит, кто здесь — как обычно, сейчас разберёмся во всех этих животрепещущих вопросах.
https://habr.com/ru/companies/ruvds/articles/976554/
#трекер #tracker #звуковой_чип #soundchip #chiptune #sid #ay38910 #2a03 #pokey #ruvds_статьи
-
I've now published the design and build guide for my dual AY-3-8910 Arduino Uno Shield.
https://diyelectromusic.com/2025/09/25/arduino-ay-3-8910-shield-design/
-
Arduino AY-3-8910 Shield Build Guide
Here are the build notes for my Arduino AY-3-8910 Shield Design.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to electronics and microcontrollers, see the Getting Started pages.
Bill of Materials
- Arduino AY-3-8910 Shield PCB (GitHub link below)
- 2x AY-3-8910 or YM2419 devices (see notes on sourcing here: Arduino and AY-3-8910)
- 8x 1KΩ resistors
- 2x 100nF ceramic capacitors
- 1x or 2x 1uF electrolytic capacitors
- 1x 3.5mm stereo TRS socket (see photos and PCB for footprint)
- Arduino headers: 1x 6-way; 2x 8-way; 1x 10-way pin or extended headers as required
- Optional: 2x 40 pin wide DIP sockets (highly recommended)
If both chips audio outputs are to be combined, using the solder bridges, then only one 1uF electrolytic capacitor should be used.
Build Steps
Taking a typical “low to high” soldering approach, this is the suggested order of assembly:
- Resistors.
- Disc capacitors (see notes below).
- DIP sockets (if used) and TRS socket.
- Electrolytic capacitors.
- Arduino headers.
There are two solder bridge jumpers which can be used for the following:
- To mix all channels from both chips onto the same output.
- To combine left and right channels for the TRS socket.
By default, one chip goes to the left audio output and one goes to the right, but it is possible to combine them into a single mono output. But then there is another choice: combine the left and right audio channels (tip and ring) for the TRS socket; or leave all outputs just to the tip of the socket.
If these options are being considered, then one of the output electrolytic capacitors should be omitted too. More details below.
Here are some build photos.
The ceramic capacitors are actually shown as being installed on the underside of the board, but depending on the 40 pin DIP socket used (or not) it may be possible to install them on the top side of the board as I’ve done below.
I’ve used “extended headers” which give me a breakout for the Arduino GPIO on the top of the board. If simple pin headers are used, then care should be taken about the height of the board and avoiding the possibility of the resistors shorting out on the USB socket of the Arduino.
Solder jumper options
For mono operation:
- Only install electrolytic capacitor highlighted in PURPLE. Do not install the capacitor with the YELLOW cross.
- Bridge the solder jumper highlighted in RED.
For mono socket operation, i.e. TIP and GROUND only, leave the solder bridge highlighted in ORANGE unbridged. This allows a mono jack lead to be used as RING is unconnected in the socket and can be ignored.
To take the mono signal into a stereo socket, i.e. TIP, RING and GROUND but with TIP and RING having the same mono output signal, solder the bridge highlighted in ORANGE. This allows a stereo jack lead to be used and both channels will received the same output signal.
Testing
I recommend performing the general tests described here: PCBs.
Once everything appears electrically good, a variation of the test application from my AY-3-8910 Experimenter PCB Build Guide can be used that will play a chord on both of the devices at a different octave.
Note: the GPIO usage of the Arduino is printed on the back of the PCB and listed in the Arduino AY-3-8910 Shield Design.
PCB Errata
There are no known issues with the PCB at present.
Enhancements:
- None
Closing Thoughts
This seems to work fine and is a lot simpler than my quad board if some simple experimentation is required.
I still haven’t gotten around to building some real applications for any of these boards yet though, so ought to get on to that.
Kevin
-
Arduino AY-3-8910 Shield Design
Having build my AY-3-8910 Experimenter PCB I thought a slightly simpler format board would be useful, so I’ve put together an Uno shield-format PCB that can support one or two AY-3-8910 chips.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to electronics and microcontrollers, see the Getting Started pages.
The Circuit
This is simply the two-chip version of my AY-3-8910 Experimenter PCB Design. I fixed the “reset on D13” thing though, so the connections are now as follows:
ArduinoAY-3-8910D2-D9D0-D7D10CLOCKD11/RESETA0/A1BC1/BDIR for device AA2/A3BC1/BDIR for device BThis leaves A4/A5 free for analog IO or I2C, D0/D1 free for the UART, and D12/D13 free for other uses including the on-board LED on D13.
I trimmed down the output audio stage, but arranged one chip on the L channel and one chip on the R channel, but left the solder jumpers in to allow the mixing of both devices onto single or dual channels too.
I’ve also added the two pull-downs, opting for the same arrangement as the patch on the previous board, and I’ve correctly the silkscreen capacitor values.
PCB Design
I’ve just managed to squeeze everything into the Uno shield format. I put the chips smoothing capacitors on the underside of the board to allow them to sit close to the chips’ power pins.
I’ve broken out the spare GPIO pins to additional headers, partly to make it clear which pins are spare.
I’ve also listed the GPIO usage for each chip on the underside of the board.
I was tempted to remove the mounting holes at the shaped end of the board, but left them in the end. One is a bit close to the additional breakout headers for A4/A5, but I have the option not to add those if I want to.
I have extended the board slightly though compared to the traditional Arduino shield shape just to accommodate the length of the 40-pin devices a bit more easily.
Closing Thoughts
I must admit I wasn’t sure if I could get two 40-pin wide DIP devices onto a shield, but it just about fits.
Fingers crossed, having a four-device version already, this will be a little easier to get going than the last one!
Kevin
-
Звуковые эффекты на ZX Spectrum: AYFXEdit и BeepFX
Расскажу, как устроены и реализуются звуковые эффекты в рамках примитивных возможностей 8-битных машин на примере нашего народно любимого ZX Spectrum, для которого я сделал несколько соответствующих редакторов и наборов процедур, для бипера и для звукового чипа AY-3-8910.
https://habr.com/ru/companies/ruvds/articles/943960/
#beeper #бипер #звуковые_эффекты #zx_spectrum #1bit #игры #ay38910 #спектрум #sfx #ruvds_статьи
-
And my "go to" demo for Arduino tones - an arrangement of the throne room and end titles for Star Wars Episode IV for my #LoFiOrchestra tones.
Now running on #AY38910 chips :)
-
Adding MIDI to my Quad AY-3-8910 Arduino board.
https://diyelectromusic.com/2025/08/29/arduino-and-ay-3-8910-part-5/
-
AY-3-8910 Tones - Another New Hope
-
Arduino and AY-3-8910 – Part 5
My next bit of messing around with Arduno and AY-3-8910 takes my AY-3-8910 Experimenter PCB Design and adds some simple MIDI reception to create a 12-channel AY-3-8910 tone module.
- Part 1 – Getting started and looking at playing YM files.
- Part 2 – Adding basic MIDI control.
- Part 3 – Basic experiments with direct digital synthesis.
- Part 4 – Using the AY-3-8910 as a 4-bit DAC for Mozzi.
- Part 5 – Driving four AY-3-8910s using my AY-3-8910 Experimenter PCB.
https://makertube.net/w/hLo4HLYcQkcGvf8N9XzgCS
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key tutorials for the main concepts used in this project:
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Nano.
- 4x AY-3-8910 chips.
- Built AY-3-8910 Experimenter PCB
The Code
This is taking a combination of the following previous projects:
- AY-3-8910 Experimenter PCB Build Guide – the sample code to drive the Quad AY-3-8910 board.
- Arduino Tones – A New, New Hope – driving 12 channels of Arduino tones.
I had the option of assigning unique MIDI channels to each of the 12 channels of the quad AY-3-8910s, but instead opted for a system that listens on all MIDI channels but assigns incoming notes to the next free channel.
If there are no spare channels, the notes are ignored.
I’ve included an option to respond to velocity, by translating a MIDI velocity value (0 to 127) into a AY-3-8910 amplitude level (0 to 15). But for now, I’m using it with a fixed velocity.
In order to map a polyphonic note index onto a chip and channel, I use the following:
void ayNoteOn (int chan, int pitch, int vel) {
int ay = chan / 3;
int ch = chan % 3;
aySetFreq (ay, ch, pitch, vel);
}The aySetFreq() function takes a MIDI nonte number and turns it into a course an fine frequency value for programming into the AY-3-8910.
void aySetFreq (int ay, int ch, int note, int vel) {
int vol = vel >> 3;
uint16_t freq = 0;
if (note != 0) {
freq = pgm_read_word(&Notes[note-NOTE_START]);
}
switch (ch) {
case 0:
ayFastWrite (ay, AY38910Regs::A_TONE_C, freq >> 8);
ayFastWrite (ay, AY38910Regs::A_TONE_F, freq & 0x0FF);
ayFastWrite (ay, AY38910Regs::A_AMP, vol);
break;
}
}Additional case statements are provided for channels 1 (B) and 2 (C). The Notes array is the list of frequencies calculated for a 1MHz clock using the equation provided in the data sheet:
- Freq (tone) = Freq (clock) / (16 TP)
Where TP is the 12-bit value placed in the course and fine frequency registers. So turning this around and plugging in the frequencies for MIDI notes, we can figure out the 12-bit values required to be programmed into the registers.
In the end, I cheated and used the table already provided here: https://github.com/Andy4495/AY3891x/blob/main/src/AY3891x_sounds.h
This covers all notes from C0 (MIDI 12) to B8 (MIDI 119).
I should also note that I’ve now removed all of the original AY3891x library and am using my own fast-access routines now tailored for supporting four devices.
As I’m using port IO though, this does mean there is a fair bit of hardcoded assumptions about Arduino PORT usage and GPIO pins.
Closing Thoughts
The video shows my, now, go-to test of anything linked to Arduinos and tones – a 12-channel arrangement of the end titles of Star Wars Episode IV – A New Hope.
As the code will select the next free channel for incoming notes, sometimes consecutive notes sound slightly different due, presumably, to differences in the output channels of the devices. Something to look at, at some point.
It would also be useful to have a “multi-track” version where each channel is an independent MIDI channel in its own right, but for now, using OMNI and “next free channel” is fine.
I have to say, when the theme really gets going with those vintage 8-bit tone sounds, I could be sitting back in that 80s Star Wars vector graphics video arcade machine… (although apparently that used several Atari POKEY chips, not AY-3-891x- shame. I wonder if you can get hold of those too…)
“The force will be with you. Always.”
Kevin
-
Driving up to four vintage AY-3-8910 sound chips from an Arduino Nano.
Design and build guide now up for the PCB.
https://diyelectromusic.com/2025/08/28/ay-3-8910-experimenter-pcb-design/
-
AY-3-8910 Experimenter PCB Build Guide
Here are the build notes for my AY-3-8910 Experimenter PCB Design.
https://makertube.net/w/fULfpG9LNwpb3iCfavVkAp
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to electronics and microcontrollers, see the Getting Started pages.
Bill of Materials
- AY-3-8910 Experimenter PCB (GitHub link below)
- Arduino Nano
- Up to 4x AY-3-8910 40-pin DIP devices (see notes here on obtaining devices: Arduino and AY-3-8910)
- 1x 6N138 optoisolator
- 1x 1N4148 or 1N914 signal diode
- Resistors: 4x 220Ω, 1x 4K7, 14x 1K
- 6x 100nF ceramic capacitors
- 2x 1uF electrolytic capacitors (the PCB has 220uF on the sinkscreen)
- 1x 100uF electrolytic capacitor
- Either: 3x 3.5mm TRS PCB mount sockets
- Or: 1x 3.5mm TRS PCB mount sockets and 2x 180 DIN PCB mount sockets
- 1x 2.1mm barrel jack socket
- 2x 15-way pin header sockets
- 4x 40-way wide DIP sockets
- Pin headers
- Optional: 1x SPDT, 1x DPDT both with 2.54mm pitch connectors
- Optional: 1x 8-way DIP socket
Build Steps
Taking a typical “low to high” soldering approach, this is the suggested order of assembly:
- All resistors and diode.
- DIP and TRS socket(s).
- Disc capacitors.
- Switches (if used).
- Electrolytic capacitors.
- 15-way pin header sockets.
- Barrel jack socket.
- DIN sockets (if used).
It is necessary to add two additional 1K resistors as patch-links on the underside of the board. Details below.
Here are some build photos.
The DIP sockets should go on next before the TRS sockets.
Pin headers and jumpers could be used for the MIDI on/off switch. The power switch could be bypassed with a wire link if not required.
There are a number of optional pin header breakouts: power, UART, additional IO and all the IO for the four AY-3-8910 chips. For this build I’m not populating those.
Errata Fixes
As mentioned in the design notes, two additional resistors must be added to pull the audio outputs to GND as part of the output/mixer circuit. I used two additional 1K resistors.
These can be added to the underside of the board as shown below.
Testing
I recommend performing the general tests described here: PCBs.
Once everything appears electrically good, here is a test application that will play a chord on each of the devices at a different octave. If this works it should be possible to hear all 12 notes in the four chords across four octaves sounding.
Find the code here: https://github.com/diyelectromusic/sdemp/tree/main/src/SDEMP/ArduinoAY38910QuadTest
PCB Errata
As already mentioned there are the following issues with this PCB:
- The two 220uF capacitors should be replaced with 1uF capacitors.
- Two additional resistors need to be patched into the audio output circuit.
Enhancements:
- None
Sample Applications
Here are some applications to get started with:
- (on their way)
Closing Thoughts
It took quite a long time to realise the issue with the output channels. For ages, it appeared that the interface to the chip just wasn’t functioning correctly. With hindsight, some kind of register read/write test would have confirmed that a lot earlier.
It was only when going back to the schematics of other designs and recognising that the output was always HIGH did the penny drop that the additional resistor was required. Then there was some experimentation to find something that would work with my board and not cause issues in use.
But it seems like I got there in the end. Now I can get on with doing something a little more interesting MIDI and music wise.
Kevin
-
Arduino Quad AY-3-8910 First Demonstration
-
@tattlemuss In fact, I thought I may as well post up what I have so far, and I'll just update that as I go!
Your descriptions of the OpCodes will be really useful, especially for the effects ones, which I was still puzzling over, although I think I'd got the general gist :)
-
Z80 and AY-3-8910
Finally starting to look at the Arduino and AY-3-8910 was triggered by a couple of things recently. First getting an RC2014 and playing with AY-3-8910 based sound on that.
But also, having visiting RetroFest in Swindon this year, talking to Dean Belfield about the methods he used to develop for the ZX Spectrum and how he was donated a number of archive disks from the Follin brothers related to their producing music for 8-bit games.
The archive can be found here: https://github.com/breakintoprogram/archive-follin and it is a real treasure trove of information.
I wanted to try to understand some of it myself, so this is me poking about in the archives to learn a little about how some of this music was produced at the time.
After having a bit of an initial look and after posting a “this is interesting” post on Mastodon, Steven Tattersall told me about a similar thing they’d done with Follin’s ST driver too. This is for the YM2149 which is the Yamaha equivalent of the AY-3-8910. There is a great description of this one here: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html
Once I’m a bit more through my own discussion it will be really interesting to compare the two.
For now, I’m just posting this based on my initial thoughts, and will update it as I go.
To be continued…
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to microcontrollers, see the Getting Started pages.
The Archives
From Dean’s introduction:
“The music and sound effects were hand-coded by Tim and Geoff Follin in assembler as a series of DEFB’s representing note pitch and duration for each channel. This data can also contains commands, for example to loop a sequence, call a subroutine or switch on an effect.”
“Once completed, their music source was included in the intended game along with a small music driver, also written in assembler.”
“This could then be called from within the game for both in-game music and sound effects”
The sound driver effectively implemented a series of instructions and turned these into commands to write out to the chip as part of its main “scan”.
There is an example of the sound driver here: https://github.com/breakintoprogram/archive-follin/blob/main/Examples/AY/Ghouls%20n%20Ghosts/aydrive.z80
And there is a set of instructions for the music here: https://github.com/breakintoprogram/archive-follin/blob/main/Examples/AY/Ghouls%20n%20Ghosts/g%2Bgmusic.z80
Combining these gives a block of assembly that can be built and run directly to play the sound.
Ste Ruddy’s Sound Driver
Ste Ruddy was interviewed about his work with Tim Follin and the sound driver he produced here: https://www.c64.com/interviews/ruddy.html.
“Working with Tim was essential when working on the music driver as basically he told me what he wanted the driver to do and I made it do it.”
As far as I can make out, the aydrive.z80 sound driver in the archive is in two main parts:
- A manager interface to load the track, run some kind of user-interface to show what is going on, and then call the sound routines.
- The sound driver itself.
In the assembly, the sound driver starts with the following:
;======================================
; AY MUSIC DRIVER V1.00 BY S.RUDDY
CODE_TOP:There then follows a series of initialisation statements. These hold the internal state of the various aspects of the sound driver – things like current playing frequency, loop counts, volume levels, ADSR settings and so on. These are the items that implement the “language” that provides the instructions for what to play.
So what is that language? There is a list of commands (“OpCodes”) in the music file:
FOR: EQU 080H
NEXT: EQU 081H
LENGTH: EQU 082H
STOP: EQU 083H
GOSUB: EQU 084H
RETURN: EQU 085H
TRANS: EQU 086H
DISTORT: EQU 087H
SEND: EQU 088H
ADSR: EQU 089H
ENVON: EQU 08AH
WOBBLE: EQU 08BH
PORT: EQU 08CH
VIB: EQU 08DH
IGNORE: EQU 08EH
EFFECT: EQU 08FH
GOTO: EQU 090H
GATE_CON: EQU 091H
ENDIT: EQU 092HThe tune file contains sets of sound FX and tunes that consist of series of the above instructions to play notes and otherwise manipulate the AY-3-8910.
The basic code structure of the sound driver is as follows (using the assembler labels from aydrive.z80):
- Working values, variables, and parameters – “CODE_TOP”.
- Main tune driver setup – “TUNE”.
- Sound FX driver setup – “FX”.
- Main driver scanning routine – “REFRESH”.
- This takes the current state of all the variables, parameters, and so on, performs any updates as per the provided commands, and outputs the latest status to the AY-3-8910.
- This runs once “per frame”.
- There are three copies of this routine, processing in turn each of channel A, B and C.
- Main driver command routine.
- A “jump off” table for each OpCode command in the sound driver.
- Assembly routines for each command to update the variables, parameters, and process state of the sound driver.
- Again there are three copies of the jump table and routines, one for each of channel A, B and C.
- AY-3-8910 driver routine.
- Architecture specific code to access the chip.
- Note frequency table for 102 notes.
- Close-out code.
The entry points, as far as I can see, are as follows:
- Main tune driver setup “TUNE”.
- Sound FX setup “FX”.
- Scanning “REFRESH” routine.
The combined driver, tunes, and tester UI, when all combined, is structured as follows:
- UI / Tester.
- Sound Driver.
- Tune and FX instructions.
This is expanded upon in pseudo-code below using the labels from the original assembly:
Code_Start: EQU 40000
Data_Start: EQU 50000
;-----------------------------
ORG Code_Start
; The UI/tester code
TESTER:
LOOP: Calls the following for each scan:
HALT - Suspends until an interrupt comes in?
CALL UPDATE
CALL REFRESH
CALL CLOCK
CALL KEYSCAN
Repeat as necessary
KEYSCAN: UI scanning
CLOCK: Possibly maintain a 50Hz refresh rate clock?
UPDATE: Loads the internal state of all sound variables from
the driver and displays them in real time via the UI.
; The sound driver as described above
CODE_TOP:
TUNE: Select which tune to play.
TUNE_IN: Init all internal sound state variables for a new tune.
TUNEOFF: Stop a playing tune, eg to change tune or start an FX.
FX: Start playing an FX.
FLOOP: Keep processing FX instructions until complete.
REFRESH:
CHANNEL_A: IF STOP_A jump to CHANNEL_B
START_A: Process channel A state with CALLS to OUT_CA_AY or
OUT_FLESH as reqd and then RET back into this code.
Processing includes: ADSR, vibrato, distortion,
wobble, portamento, note frequency, etc.
DO_A: Entry point for getting next instruction.
COMMAND_A: Process a command for CH A using JUMP_A table.
GOTNOTE_A: Process a note for CH A with CALLS to OUT_CA_AY or
OUT_FLESH as reqd and RET back into this code.
Carry on to CH B
CHANNEL_B: IF STOP_B jump to CHANNEL_C
START_B: As above for Channel B
DO_B:
COMMAND_B:
GOTNOTE_B:
Carry on to CH C
CHANNEL_C: IF STOP_C RET to caller.
START_C: As above for Channel C
DO_C:
COMMAND_C:
GOTNOTE_C:
Finally JUMP to (not CALL) OUT_CA_AY so that the RET
is a RET to caller of the REFRESH code.
OUT_FLESH: Sets CH A,B,C and noise levels then jump to
OUT_CA_AY which includes the RET to caller.
JUMP_A: Jump table and associated instructions for CH A.
All instructions JUMP back to DO_A when complete.
JUMP_B: Jump table and associated instructions for CH B.
All instructions JUMP back to DO_B when complete.
JUMP_C: Jump table and associated instructions for CH C.
All instructions JUMP back to DO_c when complete.
OUT_CA_AY: Send contents of Z80 register C (0 to 255) to AY
register defined by Z80 register A (0 to 15).
RET to caller
NOTES: Frequency definitions for 102 notes.
CODE_BOT: End of Sound Driver
;-----------------------------
ORG Data_Start
; Tune and FX instructions
; 5 tunes and 21 effects
TUNES: EQU 5
EFFECTS: EQU 21
DATA_TOP:
TUNES_A: Jump table for 5 tunes info for CH A.
TUNES_B: Jump table for 5 tunes info for CH B.
TUNES_C: Jump table for 5 tunes info for CH C.
FX_TAB: Jump table for 21 effects info.
Instructions for 21 effects.
Instructions for 5 tunes.So there is a main logic block – the “REFRESH” code, which defines the current frequency and volume level according to the state of the playing note, ADSR and any added effects. Then there are logic blocks for each of the recognised sound commands to adjust that state as appropriate.
The Sound OpCodes
I was going to document each in turn, but actually Steven Tattersall has already done a really good job of that here: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html
I’ll expand on that as I go in terms of what it seems to mean for the Z80 driver, but for starting point that is a pretty good summary!
State Variables
Before we get into the main block of code, there are a number of state variables within the driver, which are essentially locations reserved in the assembly at the start, with a specific label. In most cases there are three of each, one for each of channel A, B, C.
These are a mixture of internal state variables and stored parameters from the various OpCodes.
There are four DW (word) declarations, all other are DB (byte) locations. I’ve re-ordered them here to put what I believe are related ones together.
- PC (WORD) – the “program counter” for recording the next instruction to process
- LOOP (WORD) – for loop “address”
- REPEAT – the loop count condition
- COUNT – the loop counter
- FREQ (WORD) –
- STOP – Stop paying this channel if > 0
- TRANS – Transpose in semitones
- LENGTH – Default note length
- IGNORE – ignore transpose for following note if > 0
- PORT – Portamento
- TARGET – for portamento
- DISTORT – raw “detune” number to add to playing frequency
- OLDFREQ
- VOLUME – channel volume
- FLIP1
- OLDNOTE – for portamento
- W_WAIT
- W_DEL1
- W_DEL2
- W_OFF
- A_INIT – ADSR parameters (4-bit)
- A_ATT – ADSR attack(4-bit)
- A_DEC – ADSR decay (4-bit)
- A_SUS – ADSR sustain level(4-bit)
- A_CYC – ADSR parameters (8-bit)
- A_STAGE – ADSR internal state
- A_TIME – ADSR internal state
- A_CONT – ADSR reset action
- V_DEL
- V_DEL1
- V_RATE
- V_LIM1
- V_LIM2
- V_DIR
- E_FREQ (WORD) – fixed frequency effect
- E_TIME – Fixed frequency effect
- E_WAIT – internal variable for effect
- E_BITS – mixer settings for effect
- FLESH
- ENDIT – turn mute off
- GATE
- GATERES – mute time?
- MEMGATE
I’ll walk through the main logic shortly, but before that, it is worth looking at the part of the code that processes the OpCodes:
LD BC,(PC_A) ; On entry, BC = PC_A
DO_A: LD A,(BC) ; Grab the next instruction
INC BC ; Inc the Program Counter
OR A ; If < 0x80 then it's a note
JP P,GOTNOTE_A
COMMAND_A: AND 127 ; Instruction &= 0x7F
ADD A ; x2
LD E,A
LD D,0 ; Now DE = Index into JUMP table
LD HL,JUMP_A ; HL = &JUMP_A
ADD HL,DE ; HL = &JUMP_A[Idx]
LD E,(HL) ; Load first byte into E
INC HL ; Move to 2nd byte to load into D
LD D,(HL) ; DE = OpCode address
EX DE,HL ; Swap DE/HL
JP (HL) ; Jump to OpCode code at addr in HL
GOTNOTE_A: ...The instruction JUMP table comes later. Each entry is a WORD and is the address of the appropriate code to implement the OpCode, as follows:
JUMP_A: DW A_FOR
DW A_NEXT
DW A_LENGTH
DW A_STOP
DW A_GOSUB
DW A_RETURN
DW A_TRANS
DW A_DISTORT
DW A_SEND
DW A_ADSR
DW A_ENVON
DW A_WOBBLE
DW A_PORT
DW A_VIBRATO
DW A_IGNORE
DW A_EFFECT
DW A_GOTO
DW A_GATECON
DW A_ENDITOn entry to each chunk of OpCode assembly, the registers appear to be:
- HL: Start address of the OpCode now running
- BC: Program/Stack Counter – now pointing to first location after OpCode
- DE: OpCode entry in the JUMP table (probably)
The descriptions in the following come from: http://clarets.org/steve/projects/reverse_engineering_tim_follin.html
FOR (repeats)
“0 – start_loop: Start a loop from this point. Loops are not stacked.”
A_FOR: LD A,(BC)
LD (REPEAT_A),A ; Store repeat counter in REPEAT_A
INC BC ; Loop repeats from next OpCode
LD (LOOP_A),BC ; Store loop location in LOOP_A
JP DO_A ; Next instructionNEXT ()
“1 – end_loop: Decrement counter and if not zero, go back to loop point.”
A_NEXT: LD HL,REPEAT_A ; REPEAT_A--
DEC (HL)
JP Z,DO_A ; IF == 0 THEN next instruction
LD BC,(LOOP_A) ; ELSE return to LOOP_A
JP DO_ALENGTH (value)
“2 – set_default_note_time: If not zero, all following notes take this value as the duration. If zero, all following notes have an extra byte with the note’s duration.”
A_LENGTH: LD A,(BC) ; Store LENGTH_A
LD (LENGTH_A),A
INC BC
JP DO_A ; Next instructionSTOP ()
“3 – stop: Stop playback of the channel.”
A_STOP: LD HL,STOP_A ; STOP_A++ INC (HL) JP CHANNEL_B ; Move onto channel B
GOSUB (addr(WORD)) / RETURN ()
“4 – gosub: Push the current command address on a stack. Start processing from a new address. Next 2 bytes: offset of the subroutine from the start of the tune data (little-endian).”
“5 – return: Pop the return address off the stack and continue processing from the popped address.”
A_GOSUB: LD A,(BC) ; Set HL to required address
LD L,A
INC BC
LD A,(BC)
LD H,A
INC BC
PUSH HL ; Store address on real stack
LD HL,(SP_A) ; Grab OpCode SP (SP_A)
LD (HL),C
INC HL
LD (HL),B
INC HL ; Store OpCode PC in OpCode stack
LD (SP_A),HL ; Store updated OpCode SP
POP BC ; Grab address off real stack and...
JP DO_A ; ... run from there
A_RETURN: LD HL,(SP_A) ; Grab OpCode SP from SP_A
DEC HL ; Grab OpCode PC from OpCode stack
LD B,(HL) ; Stick it back into BC
DEC HL
LD C,(HL)
LD (SP_A),HL ; Update SP_A again
JP DO_A ; then continue processing again
SP_A: DW 0 ; OpCode stack pointer
STACK_A: DS 2*SD,0 ; OpCode stack (SD = 3)TRANS (value)
“6 – set_transpose: Next byte: number of semitones to transpose all following notes. Signed 8-bit value.”
A_TRANS: LD A,(BC) ; Store value in TRANS_A
LD (TRANS_A),A
INC BC
JP DO_A ; Next instructionDISTORT (value)
“7 – set_raw_detune: Next byte: raw value to add to the final note period in YM register space. Unsigned 8-bit value.”
A_DISTORT: LD A,(BC) ; Store value in DISTORT_A
LD (DISTORT_A),A
INC BC
JP DO_A ; Next instructionSEND (reg, value)
“8 – direct_write: Write a value directly to the YM registers. This is often used to write noise pitch.
The “mixer” register, register 7, is treated differently. This register combines settings for all 3 channels A,B,C to determine whether they use the square or noise channel, so the driver ensures that only the bits relevant to the active channel are set and cleared.”A_SEND: LD A,(BC)
LD L,A ; L = register to write
INC BC
LD A,(BC)
INC BC
PUSH BC ; Store OpCode PC for later
LD C,A ; C = value to write
LD A,L
CP 7 ; IF reg != 7 JP A_NOT_IO
JP NZ,A_NOT_IO
LD A,C
LD (FLESH_A),A
LD (MEMGATE_A),A
CALL OUT_FLESH ; OUT_FLESH handles R7 (mixer)
POP BC ; Grab OpCode PC back
JP DO_A ; Next instruction
A_NOT_IO: CALL OUT_CA_AY ; A=reg, C=value
POP BC ; Grab OpCode PC back
JP DO_A ; Next instructionADSR (INIT|SUS, ATT|DEC, CYC)
“9 – set_adsr: Sets the note envelope. This takes 3 bytes and contains the attack and decay speeds, the minimum and maximum volume levels after attack or decay, and which stage to start in (attack, decay, or hold).”
A_ADSR: LD A,(BC)
RRA
RRA
RRA
RRA
AND 15
LD (A_INIT_A),A ; A_INIT_A = param[0] >> 4
LD A,(BC)
AND 15
LD (A_SUS_A),A ; A_SUS_A = param[0] & 0x0F
INC BC
LD A,(BC)
RRA
RRA
RRA
RRA
AND 15
LD (A_ATT_A),A ; A_ATT_A = param[1] >> 4
LD A,(BC)
AND 15
LD (A_DEC_A),A ; A_DEC_A = param[1] & 0x0F
INC BC
LD A,(BC)
LD (A_CYC_A),A ; A_CYC_A = param[2]
INC BC
JP DO_A ; Next instructionENVON (value)
“10 – set_adsr_reset: Next byte: if zero, moving to a new note does not reset ADSR, otherwise ADSR is reset. (A zero value is usually used to define complex arpeggio sequences.)”
A_ENVON: LD A,(BC) ; Store value in A_CONT_A
LD (A_CONT_A),A
INC BC
JP DO_A ; Next instructionWOBBLE (offset, del1, del2)
“11 – set_arpeggio: Sets the semitone note offset of the arpeggio and the times they are held for.”
A_WOBBLE: LD A,(BC)
LD (W_OFF_A),A ; W_OFF_A = param[0]
INC BC
LD A,(BC)
LD (W_DEL1_A),A ; W_DEL1_A = param[1]
INC BC
LD A,(BC)
LD (W_DEL2_A),A ; W_DEL2_A = param[2]
INC BC
JP DO_A ; Next instructionPORT (value)
“12 – set_slide: Set the number of semitones to jump per update when applying glissando between notes. Usually set to 1.”
A_PORT: LD A,(BC)
LD (PORT_A),A ; PORT_A = value
INC BC
JP DO_A ; Next instructionVIB (del, rate, lim, dir)
“13 – set_vibrato: Set the delay, size, speed and starting direction of the vibrato effect.”
A_VIBRATO: LD A,(BC)
LD (V_DEL_A),A ; V_DEL_A = param[0]
INC BC
LD A,(BC)
LD (V_RATE_A),A ; V_RATE_A = param[1]
INC BC
LD A,(BC)
LD (V_LIM2_A),A ; V_LIM2_A = param[2]
INC BC
LD A,(BC)
LD (V_DIR_A),A ; V_DIR_A = param[3]
INC BC
JP DO_A ; Next instructionIGNORE()
“14 – skip_transpose: For the next note only, don’t apply transpose. Usually used for drums mixed in with bassline notes.”
A_IGNORE: LD A,255
LD (IGNORE_A),A ; IGNORE_A = 255 (on)
JP DO_A ; Next instructionEFFECT (time, bits, freq)
“15 – set_fixfreq: Force using a fixed frequency, defined in the next 3 bytes (mixer and period low/high), or turn it off by using a single zero-byte.”
A_EFFECT: LD A,(BC)
LD (E_TIME_A),A ; E_TIME_A = param[0]
INC BC
; (skipping some commented out code)
A_SETEFF: LD A,(BC)
LD (E_BITS_A),A ; E_BITS_A = param[1]
INC BC
LD A,(BC)
LD (E_FREQ_A),A ; E_FREQ_A = param[2]
INC BC
LD A,(BC)
LD (E_FREQ_A+1),A ; E_FREQ_A = param[3]
INC BC
JP DO_AGOTO (addr)
“16 – jump: Jump to a new offset. Used for the infinite looping of tunes.”
A_GOTO: LD A,(BC) ; L = addr[0]
LD L,A
INC BC
LD A,(BC) ; H = addr[1]
LD H,A
PUSH HL
POP BC ; Set BC = addr then
JP DO_A ; run the next instructionGATECON (value)
“17 – set_mute_time: Turn the channel off after N more updates.”
A_GATECON: LD A,(BC)
LD (GATERES_A),A ; GATERES_A = value
INC BC
JP DO_A ; Next instructionENDIF (value)
“18 – set_nomute: If set to non-zero suppress the automatic muting. The starting commands of a channel usually set this to 0xff.”
A_ENDIT: LD A,(BC)
LD (ENDIT_A),A ; ENDIT_A = value
INC BC
JP DO_A ; Next instructionThe Main Logic Function
I’ll work through the assembly for the main REFRESH function in time. Watch this space!
Closing Thoughts
I’ll keep coming back to this and posting an update as I get into more of it. When I think I’m done, I’ll post a proper conclusion.
For now, its just interesting to note that even though I’m sure many people have been through this before me already, it is a really interesting activity to try to figure it out myself.
As I say – to be continued…
Kevin
-
Ok, starting to get a little silly now, but I thought I'd try to use the AY-3-8910 as a 4-bit DAC for Mozzi synthesis...
https://diyelectromusic.com/2025/07/14/arduino-and-ay-3-8910-part-4/