#ay38910 — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #ay38910, aggregated by home.social.
-
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/
-
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
-
@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/
-
Arduino and AY-3-8910 – Part 4
After Part 3 I started to go back and add MIDI, and changed the waveform on the touch of a button, and then started to wonder if I could add envelopes and so on.
And then it occurred to me, I didn’t really need to re-implement my own synthesis library, I could probably write a custom audio output function for Mozzi and get it to use the AY-3-8910 as a 4-bit DAC…
- 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/ast3HQ2a3fCanKy9Pr6qUc
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:
- Arduino AY3891x Library: https://github.com/Andy4495/AY3891x
- Arduino Nano AY-3-8910 PCB: https://github.com/GadgetReboot/AY-3-8910
- AY-3-8910 on synth DIY wiki: https://sdiy.info/wiki/General_Instrument_AY-3-8910
- Mozzi: https://sensorium.github.io/Mozzi/learn/
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno.
- AY-3-8910 chip.
- Either GadgetReboot’s PCB or patch using solderless breadboard or prototyping boards.
- 5V compatible MIDI interface.
- Jumper wires.
Mozzi Custom Audio Output
Mozzi supports a wide range of microcontrollers with a range of different output methods from PWM, built-in DACs, I2S, through to custom output options with DMA or something else.
I’m not going to go over how Mozzi works here, but here are details of how to run with the different audio output modes here: https://sensorium.github.io/Mozzi/learn/output/
The key option for me is MOZZI_OUTPUT_EXTERNAL_CUSTOM. There are a number of configuration options that must be set prior to include the main Mozzi file as follows:
#include "MozziConfigValues.h"
#define MOZZI_AUDIO_MODE MOZZI_OUTPUT_EXTERNAL_CUSTOM
#define MOZZI_AUDIO_BITS 8
#define MOZZI_CONTROL_RATE 64
#define MOZZI_AUDIO_RATE 16384
#define MOZZI_ANALOG_READ MOZZI_ANALOG_READ_NONE
#include <Mozzi.h>
#include <Oscil.h>
#include <tables/cos2048_int8.h>
#include <mozzi_midi.h>
#include <mozzi_fixmath.h>This sets up the audio synthesis parameters to 8 bit audio with a sample rate of 16384Hz.
Implementing a custom audio output this way requires two functions. One for the audio output and one to tell Mozzi when it is time to call the audio output function.
I would rather have used MOZZI_OUTPUT_EXTERNAL_TIMED which handles the calling at the correct AUDIO_RATE for me, but that relies on the use of the ATMega328’s Timer 1, but in this case Timer 1 is providing the 1MHz clock for the AY-3-3810.
But rather than implementing yet another timing routine, I just used the micros() counter to decide if it was time to generate audio or not.
void audioOutput(const AudioOutput f)
{
int out = MOZZI_AUDIO_BIAS + f.l();
ayOutput(0,out);
}
unsigned long lastmicros;
bool canBufferAudioOutput() {
unsigned long nowmicros = micros();
if (nowmicros > lastmicros+58) {
lastmicros=nowmicros;
return true;
}
return false;
}To get samples produced at the required 16384Hz sample rate means there needs to be one sample produced 16384 times a second. There thus needs to be a sample every 60uS. If I implement the above function checking for nowmicros > lastmicros + 60 then the resulting sound is slightly flat (in tuning). I’m guessing this is related to the overheads of the function call and logic, so I’ve gone with lastmicros+58 and that sounds pretty good to me.
My ayOutput() routine takes an 8-bit sample and cuts it down to the 4-bits required for a level on the AY-3-8910.
FM Synthesis on the AY-3-8910 (sort of)
I wanted to try the FM synth mode just to see what would happen and thought it would be interesting to switch between the carrier sine wave signal and the modulated signal by pressing the button.
Unfortunately, I just could not get the button logic to work, even though I could see the state of the pin (A5) changing.
Finally after an hour or so of puzzling why such an apparently simple test of logic wasn’t working, I realised what the issue must be. Mozzi, for the AVR microcontrollers, has its own fast ADC routines. It turns out that these were interferrng with using A5 as a digital input pin.
It is fairly easy to override the Mozzi fast ADC though by setting MOZZI_ANALOG_READ to NONE.
The Mozzi code has a carrier and modulator waveform running at audio rate and an index running at the control rate to bring the modulator in and out.
It is just about possible to see the FM modulation on the oscilloscope as shown below.
Of course, the AY-3-8910 isn’t actually doing FM synthesis itself. It is just acting as a 4-bit DAC, but it is still quite fun to see.
Closing Thoughts
This is all getting a little pointless really, as there is nothing being done that the Arduino Nano couldn’t do better on its own, but it is a bit of fun to see where this thread ends up.
There are a number of interesting angles now. One of which would be to utilise all three channels. This could provide a form of additive synthesis, it could perform some fixed interval additional oscillators, or it could be used for 3-note polyphony.
Now that Mozzi is running it is also possible to do anything Mozzi can do, and that includes implementing envelope generation.
Kevin
-
"Simple" direct digital synthesis using the 4-bit level control of one channel of the AY-3-8910.
Spoilers: it wasn't as simple as I'd thought it would be!
https://diyelectromusic.com/2025/07/13/arduino-and-ay-3-8910-part-3/
-
Arduino and AY-3-8910 – Part 3
I suggested in Part 2 that it might be possible to do some simple modulation of the amplitude of the AY-3-8910 channels rather than drive frequencies directly. This is taking a look at the possibilities of some kind of lo-fi direct digital synthesis using that as a basis.
- 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/uCSiBG5RBufGqspoHMYFPt
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:
- Arduino AY3891x Library: https://github.com/Andy4495/AY3891x
- Arduino Nano AY-3-8910 PCB: https://github.com/GadgetReboot/AY-3-8910
- AY-3-8910 on synth DIY wiki: https://sdiy.info/wiki/General_Instrument_AY-3-8910
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno.
- AY-3-8910 chip.
- Either GadgetReboot’s PCB or patch using solderless breadboard or prototyping boards.
- 5V compatible MIDI interface.
- Jumper wires.
Direct Digital Synthesis on the AY-3-8910
I’ve talked about direct digital synthesis before, so won’t go into full detail again. For more, see Arduino R2R Digital Audio – Part 3 and Arduino PWM Sound Output.
But the top-level idea is to set the level of the signal according to a value in a wavetable. If this value is updated at a useful audio rate then it will be interpreted as sound.
There are some pretty major limitations with attempting to do this on the AY-3-8910 however. The biggest one being that there are only 15 levels for the output on each channel.
So I’ll be working to the following properties:
- 4-bit resolution for the output.
- 8-bit wavetable.
- 8.8 fixed point accumulator to index into the wavetable.
- 8096 Hz sample rate.
YouTuber https://www.youtube.com/@inazumadenki5588 had a look at this and showed that the AY-3-8910 needs to be set up as follows:
- Frequency value for the channel should be set to the highest frequency possible.
- All channels should be disabled.
This is due to comments in the datasheet stating that the only way to fully disable a channel is to have 0 in the amplitude field.
Note: for a 8192 sample rate, that means writing out a sample to the AY-3-8910 registers approximately once every 124uS. With a 256 value wavetable, it takes almost 32 mS to write a complete cycle at the native sample rate, which would be around a 30 Hz output.
I’m not sure what the largest increment that would still give a useful signal might be, but say it was 8 values from the wavetable, then that would make the highest frequency supported around 1kHz. Not great, but certainly audible, so worth a try.
Setting up for DDS
I want a regular, reliable, periodic routine to output the levels from the wavetable, and the usual way to achieve this is using a timer and interrupt. As Timer 1 is already in use to generate the 1MHz clock for the AY-3-8910, I’m going to be configuring Timer 2 as follows:
- Timer 2 is an 8-bit timer.
- Use prescalar of 32 which gives a 500kHz clock source (16MHz/32).
- Use CTC (clear timer on compare) mode.
- Generate a compare match interrupt.
- Do not enable any output pins.
The appropriate ATMega328 registers to enable this are:
// COM2A[1:0] = 00 No output
// WGM2[2:0] = 010 CTC mode
// CS2[2:0] = 011 Prescalar=32
ASSR = 0;
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS21) | _BV(CS20);
TCNT2 = 0;
OCR2A = 60;
TIMSK2 = _BV(OCIE2A);Although it is worth noting that enabling OC1A can be quite useful for debugging. The following toggles the OC2A output (on D11) every time there is a compare match. The frequency seen on D11 will thus be half the anticipated sample frequency.
pinMode(11, OUTPUT);
TCCR2A |= _BV(COM2A0); // COM2A[1:0] = 01 for OC2A toggleAnd this does indeed generate a signal. Here is a trace showing a timing GPIO pin and the AY-3-8910 output.
The problem is that this is meant to be a 440Hz sine wave, and whilst the shape isn’t too bad (it is a little distorted as the amplitude isn’t a true linear shape), the frequency is much nearer 100Hz than 440.
Analysis of Performance
The clue is the other trace, which is a timing pin being toggled every time the Interrupt routine is called. This is showing a 1kHz frequency, which means the IRS is being called with a 2kHz frequency rather than the anticipated 8192Hz. Curiously though I am getting an accurate 4kHz toggle on the timer output pin OC1A indicating the timer is correctly counting with a 8kHz frequency.
No matter how I configured things, the interrupt routine just would not do anything at a faster rate. I had to drop the frequency right down to 2kHz to get the output pin and interrupt routing running together. This means that something in the interrupt routine seems to be taking ~ 450uS to run.
After a fair bit of prodding and probing and checking the ATMega328 datasheet and double checking the register values, I have to conclude that the AY3891x library is just too slow at updating the registers for it to be able to run from the interrupt routine at this speed.
Taking a look at the register write() function in the library, which I need to use to update the channel level, I can see the following is happening:
void AY3891x::write(byte regAddr, byte data) {
latchAddressMode(regAddr);
daPinsOutput(data);
noInterrupts();
mode010to110();
mode110to010();
interrupts();
daPinsInput();
}
void AY3891x::latchAddressMode(byte regAddr) {
mode010to000();
daPinsOutput(_chipAddress | regAddr); // Register address is 4 lsb
mode000to001();
mode001to000();
mode000to010();
}
void AY3891x::daPinsOutput(byte data) {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], OUTPUT);
}
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) {
digitalWrite(_DA_pin[i], data & 0x01);
data = data >> 1;
}
}
}
void AY3891x::daPinsInput() {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], INPUT);
}
}And every one of those modeXXXtoYYY() functions is a call to digitalWrite(), so I make that 22 calls to ditigalWrite() in order to write a single register value, plus around 16 calls to pinMode(). There are also 5 loops each looping over 8 values.
One person measured the Arduino Uno digitalWrite() function and concluded that it takes 3.4uS to run, so that is a minimum of 75uS of processing in every run through the interrupt routine just for those calls alone. That doesn’t include the calls and other logic going on. It could easily be more than twice that when everything is taken into account.
Dropping in some temporary pin IO either side of the call to the AY write function itself, and I’m measuring just over 250uS for the register update to happen, and that is just for one channel. This means that anything with a period of that or faster is starving the processor from running at all.
Measuring the Basic Performance
At this point I took a step back and created a free-running test sketch to really see what is going on.
#include "AY3891x.h"
AY3891x psg( 17, 8, 7, 6, 5, 4, 3, 2, 16, 15, 14);
#define AY_CLOCK 9 // D9
void aySetup () {
pinMode(AY_CLOCK, OUTPUT);
digitalWrite(AY_CLOCK, LOW);
TCCR1A = (1 << COM1A0);
TCCR1B = (1 << WGM12) | (1 << CS10);
TCCR1C = 0;
TIMSK1 = 0;
OCR1AH = 0;
OCR1AL = 7; // 16MHz / 8 = 2MHz Counter
psg.begin();
// Output highest frequency on each channel, but set level to 0
// Highest freq = 1000000 / (16 * 1) = 62500
psg.write(AY3891x::ChA_Amplitude, 0);
psg.write(AY3891x::ChA_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChA_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChB_Amplitude, 0);
psg.write(AY3891x::ChB_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChB_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChC_Amplitude, 0);
psg.write(AY3891x::ChC_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChC_Tone_Period_Fine_Reg, 0);
// LOW = channel is in the mix.
// Turn everything off..
psg.write(AY3891x::Enable_Reg, 0xFF);
}
int toggle;
void setup() {
pinMode(11, OUTPUT);
toggle = LOW;
digitalWrite(11, toggle);
aySetup();
}
void loop() {
toggle = !toggle;
digitalWrite(11, toggle);
for (int i=0; i<16; i++) {
psg.write(AY3891x::ChA_Amplitude, i);
}
}All this is doing is continually writing 0 to 15 to the channel A level register whilst toggling a GPIO pin. Putting an oscilloscope trace on the IO pin and the AY-3-8910 channel A output gives me the following:
This is running with a period of 6.96mS, meaning each cycle of 16 writes takes 3.5mS, giving me almost 220uS per call to the AY write function which seems to align pretty well with what I was seeing before.
And this is generating an audible tone at around 280Hz, so regardless of any timer settings or waveform processing, this is going to be the baseline frequency on which everything else would have to rest, which isn’t great.
Optimising Register Writes
So at this point I have the choice of attempting to write to the AY-3-8910 myself using PORT IO to eliminate the time it takes for all those loops and digitalWrite() calls. Or I could try some alternative libraries.
The library I’m using aims for the most portable compatibility: “This library uses the generic
digitalWrite()function instead of direct port manipulation, and should therefore work across most, if not all, processors supported by Arduino, so long as enough I/O pins are available for the interface to the PSG.”It is a deliberate design choice, but does require all three bus control signals to be used: BDIR, BC1, BC2.
Alternatives are possible with less pin state changes, but much stricter timing requirements. Some options include:
- https://github.com/53175ddd/AY-3-8910_Arduino – uses a mixture of PORT IO and digitalWrite(). Assumes use of D0-D7 for data channel.
The following are projects that have not used a library, but just done their own thing:
- https://github.com/internalregister/AY-3-8910 – uses a mixture of digitalWrite and PORT IO. Assumes use of D0-D7 for the data channel.
- https://github.com/GaryA/TB-AY-3_MIDI – uses direct PORT IO for D2-D9
Unfortunately none of these really solves the problem as the PCB I’m using does not neatly map onto IO ports to allow the use of direct PORT IO for the data.
So to improve things whilst using this same PCB will require me to re-write the library myself.
As a test however, it is possible to take the IO pin definitions used with the PCB and write a bespoke, optimised register write routine as follows:
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
digitalWrite(BC1, HIGH);
digitalWrite(BDIR, HIGH);
// Latch address
// NB: Addresses are all in range 0..15 so don't need to
// worry about writing out bits 6,7 - just ensure set to zero
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
delayMicroseconds(10);
// Mode = Write
digitalWrite(BC1, LOW);
digitalWrite(BDIR, HIGH);
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
}I’m using the following mapping of data pins to Arduino digital IO pins to PORTS:
DA0-DA5D2-D7PORTD Bits 0-5DA6D8PORT B Bit 0DA7A3/D17PORT C Bit 3To make this happen I have to ensure that the right bits are set to OUTPUTs and that BC2 is held HIGH prior to using the fastWrite function.
digitalWrite(BC2, HIGH);
DDRD |= 0xFC;
DDRC |= 0x04;
DDRB |= 0x01;This now improves on that previous 280Hz and gives me 1600Hz performance.
So can I do any better? Well there are still between 6 and 8 calls to digitalWrite going on to handle the control signals…
#define BC1LOW {PORTC &= 0xFE;} // A0 LOW
#define BC1HIGH {PORTC |= 0x01;} // A0 HIGH
#define BC2LOW {PORTC &= 0xFD;} // A1 LOW
#define BC2HIGH {PORTC |= 0x02;} // A1 HIGH
#define BDIRLOW {PORTC &= 0xF7;} // A2 LOW
#define BDIRHIGH {PORTC |= 0x04;} // A2 HIGH
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
BC1HIGH;
BDIRHIGH;
// Latch address
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Need 400nS Min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS settle then 50nS preamble
delayMicroseconds(1);
// Mode = Write
BC1LOW;
BDIRHIGH;
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Need 500nS min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS min
}The timings come from the AY-3-8910 datasheet:
The actual minimum and maximum timings for the various “t” values are given in the preceeding table. Most have a minimum value, but tBD has to be noted: the “associative delay time” is 50nS. This means that any changing of BC1, BC2 and BDIR has to occur within 50nS to be considered part of the same action.
There is no means of having a nano-second delay (well, other than just spinning code), so I’ve just used a delayMicroseconds(1) here and there. This isn’t reliably accurate on an Arduino, but as I’m have delays of around half of that as a maximum it seems to be fine.
This now gives me the following:
This is now supporting a natural “as fast as possible” frequency of around 24kHz, meaning each call to the write function is now around 3uS. That is almost a 100x improvement over using all those pinMode and digitalWrite calls.
The downside of this method:
- It is ATMega328 specific.
- It is specific to the pin mappings and PORT usage of this PCB.
- It does not support reading or other chip operations between the writes.
It is also interesting to see that the traces also show the high frequency oscillation (62.5kHz) that is being modulated regardless of the channel frequency and enable settings.
DDS Part 2
Success! At least with a single channel. This is now playing a pretty well in tune 440Hz A.
Notice how the frequency of the timing pin is now ~4.2kHz meaning that the ISR is now indeed firing at the required 8192 Hz.
Here is a close-up of the output signal. The oscilloscope was struggling to get a clean frequency reading, but this is one time I caught it reading something close! I checked the sound itself with a tuning fork (see video). It is indeed 440Hz.
Closing Thoughts
I wanted to get something put together to allow me to drive a DSS wavetable over MIDI, with different waveforms, and so on, but it turned out to be a little more involved getting this far than I anticipated, so I’ll leave it here for now.
But hopefully filling in the gaps won’t take too long and will be the subject of a further post.
Now that I have something that works, I’m actually quite surprised by how well it is working.
Kevin
#arduinoNano #ay38910 #dds #define #directDigitalSynthesis #include #midi
-
Arduino and AY-3-8910 – Part 3
I suggested in Part 2 that it might be possible to do some simple modulation of the amplitude of the AY-3-8910 channels rather than drive frequencies directly. This is taking a look at the possibilities of some kind of lo-fi direct digital synthesis using that as a basis.
- 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/uCSiBG5RBufGqspoHMYFPt
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:
- Arduino AY3891x Library: https://github.com/Andy4495/AY3891x
- Arduino Nano AY-3-8910 PCB: https://github.com/GadgetReboot/AY-3-8910
- AY-3-8910 on synth DIY wiki: https://sdiy.info/wiki/General_Instrument_AY-3-8910
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno.
- AY-3-8910 chip.
- Either GadgetReboot’s PCB or patch using solderless breadboard or prototyping boards.
- 5V compatible MIDI interface.
- Jumper wires.
Direct Digital Synthesis on the AY-3-8910
I’ve talked about direct digital synthesis before, so won’t go into full detail again. For more, see Arduino R2R Digital Audio – Part 3 and Arduino PWM Sound Output.
But the top-level idea is to set the level of the signal according to a value in a wavetable. If this value is updated at a useful audio rate then it will be interpreted as sound.
There are some pretty major limitations with attempting to do this on the AY-3-8910 however. The biggest one being that there are only 15 levels for the output on each channel.
So I’ll be working to the following properties:
- 4-bit resolution for the output.
- 8-bit wavetable.
- 8.8 fixed point accumulator to index into the wavetable.
- 8096 Hz sample rate.
YouTuber https://www.youtube.com/@inazumadenki5588 had a look at this and showed that the AY-3-8910 needs to be set up as follows:
- Frequency value for the channel should be set to the highest frequency possible.
- All channels should be disabled.
This is due to comments in the datasheet stating that the only way to fully disable a channel is to have 0 in the amplitude field.
Note: for a 8192 sample rate, that means writing out a sample to the AY-3-8910 registers approximately once every 124uS. With a 256 value wavetable, it takes almost 32 mS to write a complete cycle at the native sample rate, which would be around a 30 Hz output.
I’m not sure what the largest increment that would still give a useful signal might be, but say it was 8 values from the wavetable, then that would make the highest frequency supported around 1kHz. Not great, but certainly audible, so worth a try.
Setting up for DDS
I want a regular, reliable, periodic routine to output the levels from the wavetable, and the usual way to achieve this is using a timer and interrupt. As Timer 1 is already in use to generate the 1MHz clock for the AY-3-8910, I’m going to be configuring Timer 2 as follows:
- Timer 2 is an 8-bit timer.
- Use prescalar of 32 which gives a 500kHz clock source (16MHz/32).
- Use CTC (clear timer on compare) mode.
- Generate a compare match interrupt.
- Do not enable any output pins.
The appropriate ATMega328 registers to enable this are:
// COM2A[1:0] = 00 No output
// WGM2[2:0] = 010 CTC mode
// CS2[2:0] = 011 Prescalar=32
ASSR = 0;
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS21) | _BV(CS20);
TCNT2 = 0;
OCR2A = 60;
TIMSK2 = _BV(OCIE2A);Although it is worth noting that enabling OC1A can be quite useful for debugging. The following toggles the OC2A output (on D11) every time there is a compare match. The frequency seen on D11 will thus be half the anticipated sample frequency.
pinMode(11, OUTPUT);
TCCR2A |= _BV(COM2A0); // COM2A[1:0] = 01 for OC2A toggleAnd this does indeed generate a signal. Here is a trace showing a timing GPIO pin and the AY-3-8910 output.
The problem is that this is meant to be a 440Hz sine wave, and whilst the shape isn’t too bad (it is a little distorted as the amplitude isn’t a true linear shape), the frequency is much nearer 100Hz than 440.
Analysis of Performance
The clue is the other trace, which is a timing pin being toggled every time the Interrupt routine is called. This is showing a 1kHz frequency, which means the IRS is being called with a 2kHz frequency rather than the anticipated 8192Hz. Curiously though I am getting an accurate 4kHz toggle on the timer output pin OC1A indicating the timer is correctly counting with a 8kHz frequency.
No matter how I configured things, the interrupt routine just would not do anything at a faster rate. I had to drop the frequency right down to 2kHz to get the output pin and interrupt routing running together. This means that something in the interrupt routine seems to be taking ~ 450uS to run.
After a fair bit of prodding and probing and checking the ATMega328 datasheet and double checking the register values, I have to conclude that the AY3891x library is just too slow at updating the registers for it to be able to run from the interrupt routine at this speed.
Taking a look at the register write() function in the library, which I need to use to update the channel level, I can see the following is happening:
void AY3891x::write(byte regAddr, byte data) {
latchAddressMode(regAddr);
daPinsOutput(data);
noInterrupts();
mode010to110();
mode110to010();
interrupts();
daPinsInput();
}
void AY3891x::latchAddressMode(byte regAddr) {
mode010to000();
daPinsOutput(_chipAddress | regAddr); // Register address is 4 lsb
mode000to001();
mode001to000();
mode000to010();
}
void AY3891x::daPinsOutput(byte data) {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], OUTPUT);
}
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) {
digitalWrite(_DA_pin[i], data & 0x01);
data = data >> 1;
}
}
}
void AY3891x::daPinsInput() {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], INPUT);
}
}And every one of those modeXXXtoYYY() functions is a call to digitalWrite(), so I make that 22 calls to ditigalWrite() in order to write a single register value, plus around 16 calls to pinMode(). There are also 5 loops each looping over 8 values.
One person measured the Arduino Uno digitalWrite() function and concluded that it takes 3.4uS to run, so that is a minimum of 75uS of processing in every run through the interrupt routine just for those calls alone. That doesn’t include the calls and other logic going on. It could easily be more than twice that when everything is taken into account.
Dropping in some temporary pin IO either side of the call to the AY write function itself, and I’m measuring just over 250uS for the register update to happen, and that is just for one channel. This means that anything with a period of that or faster is starving the processor from running at all.
Measuring the Basic Performance
At this point I took a step back and created a free-running test sketch to really see what is going on.
#include "AY3891x.h"
AY3891x psg( 17, 8, 7, 6, 5, 4, 3, 2, 16, 15, 14);
#define AY_CLOCK 9 // D9
void aySetup () {
pinMode(AY_CLOCK, OUTPUT);
digitalWrite(AY_CLOCK, LOW);
TCCR1A = (1 << COM1A0);
TCCR1B = (1 << WGM12) | (1 << CS10);
TCCR1C = 0;
TIMSK1 = 0;
OCR1AH = 0;
OCR1AL = 7; // 16MHz / 8 = 2MHz Counter
psg.begin();
// Output highest frequency on each channel, but set level to 0
// Highest freq = 1000000 / (16 * 1) = 62500
psg.write(AY3891x::ChA_Amplitude, 0);
psg.write(AY3891x::ChA_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChA_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChB_Amplitude, 0);
psg.write(AY3891x::ChB_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChB_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChC_Amplitude, 0);
psg.write(AY3891x::ChC_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChC_Tone_Period_Fine_Reg, 0);
// LOW = channel is in the mix.
// Turn everything off..
psg.write(AY3891x::Enable_Reg, 0xFF);
}
int toggle;
void setup() {
pinMode(11, OUTPUT);
toggle = LOW;
digitalWrite(11, toggle);
aySetup();
}
void loop() {
toggle = !toggle;
digitalWrite(11, toggle);
for (int i=0; i<16; i++) {
psg.write(AY3891x::ChA_Amplitude, i);
}
}All this is doing is continually writing 0 to 15 to the channel A level register whilst toggling a GPIO pin. Putting an oscilloscope trace on the IO pin and the AY-3-8910 channel A output gives me the following:
This is running with a period of 6.96mS, meaning each cycle of 16 writes takes 3.5mS, giving me almost 220uS per call to the AY write function which seems to align pretty well with what I was seeing before.
And this is generating an audible tone at around 280Hz, so regardless of any timer settings or waveform processing, this is going to be the baseline frequency on which everything else would have to rest, which isn’t great.
Optimising Register Writes
So at this point I have the choice of attempting to write to the AY-3-8910 myself using PORT IO to eliminate the time it takes for all those loops and digitalWrite() calls. Or I could try some alternative libraries.
The library I’m using aims for the most portable compatibility: “This library uses the generic
digitalWrite()function instead of direct port manipulation, and should therefore work across most, if not all, processors supported by Arduino, so long as enough I/O pins are available for the interface to the PSG.”It is a deliberate design choice, but does require all three bus control signals to be used: BDIR, BC1, BC2.
Alternatives are possible with less pin state changes, but much stricter timing requirements. Some options include:
- https://github.com/53175ddd/AY-3-8910_Arduino – uses a mixture of PORT IO and digitalWrite(). Assumes use of D0-D7 for data channel.
The following are projects that have not used a library, but just done their own thing:
- https://github.com/internalregister/AY-3-8910 – uses a mixture of digitalWrite and PORT IO. Assumes use of D0-D7 for the data channel.
- https://github.com/GaryA/TB-AY-3_MIDI – uses direct PORT IO for D2-D9
Unfortunately none of these really solves the problem as the PCB I’m using does not neatly map onto IO ports to allow the use of direct PORT IO for the data.
So to improve things whilst using this same PCB will require me to re-write the library myself.
As a test however, it is possible to take the IO pin definitions used with the PCB and write a bespoke, optimised register write routine as follows:
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
digitalWrite(BC1, HIGH);
digitalWrite(BDIR, HIGH);
// Latch address
// NB: Addresses are all in range 0..15 so don't need to
// worry about writing out bits 6,7 - just ensure set to zero
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
delayMicroseconds(10);
// Mode = Write
digitalWrite(BC1, LOW);
digitalWrite(BDIR, HIGH);
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
}I’m using the following mapping of data pins to Arduino digital IO pins to PORTS:
DA0-DA5D2-D7PORTD Bits 0-5DA6D8PORT B Bit 0DA7A3/D17PORT C Bit 3To make this happen I have to ensure that the right bits are set to OUTPUTs and that BC2 is held HIGH prior to using the fastWrite function.
digitalWrite(BC2, HIGH);
DDRD |= 0xFC;
DDRC |= 0x04;
DDRB |= 0x01;This now improves on that previous 280Hz and gives me 1600Hz performance.
So can I do any better? Well there are still between 6 and 8 calls to digitalWrite going on to handle the control signals…
#define BC1LOW {PORTC &= 0xFE;} // A0 LOW
#define BC1HIGH {PORTC |= 0x01;} // A0 HIGH
#define BC2LOW {PORTC &= 0xFD;} // A1 LOW
#define BC2HIGH {PORTC |= 0x02;} // A1 HIGH
#define BDIRLOW {PORTC &= 0xF7;} // A2 LOW
#define BDIRHIGH {PORTC |= 0x04;} // A2 HIGH
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
BC1HIGH;
BDIRHIGH;
// Latch address
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Need 400nS Min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS settle then 50nS preamble
delayMicroseconds(1);
// Mode = Write
BC1LOW;
BDIRHIGH;
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Need 500nS min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS min
}The timings come from the AY-3-8910 datasheet:
The actual minimum and maximum timings for the various “t” values are given in the preceeding table. Most have a minimum value, but tBD has to be noted: the “associative delay time” is 50nS. This means that any changing of BC1, BC2 and BDIR has to occur within 50nS to be considered part of the same action.
There is no means of having a nano-second delay (well, other than just spinning code), so I’ve just used a delayMicroseconds(1) here and there. This isn’t reliably accurate on an Arduino, but as I’m have delays of around half of that as a maximum it seems to be fine.
This now gives me the following:
This is now supporting a natural “as fast as possible” frequency of around 24kHz, meaning each call to the write function is now around 3uS. That is almost a 100x improvement over using all those pinMode and digitalWrite calls.
The downside of this method:
- It is ATMega328 specific.
- It is specific to the pin mappings and PORT usage of this PCB.
- It does not support reading or other chip operations between the writes.
It is also interesting to see that the traces also show the high frequency oscillation (62.5kHz) that is being modulated regardless of the channel frequency and enable settings.
DDS Part 2
Success! At least with a single channel. This is now playing a pretty well in tune 440Hz A.
Notice how the frequency of the timing pin is now ~4.2kHz meaning that the ISR is now indeed firing at the required 8192 Hz.
Here is a close-up of the output signal. The oscilloscope was struggling to get a clean frequency reading, but this is one time I caught it reading something close! I checked the sound itself with a tuning fork (see video). It is indeed 440Hz.
Closing Thoughts
I wanted to get something put together to allow me to drive a DSS wavetable over MIDI, with different waveforms, and so on, but it turned out to be a little more involved getting this far than I anticipated, so I’ll leave it here for now.
But hopefully filling in the gaps won’t take too long and will be the subject of a further post.
Now that I have something that works, I’m actually quite surprised by how well it is working.
Kevin
#arduinoNano #ay38910 #dds #define #directDigitalSynthesis #include #midi
-
Arduino and AY-3-8910 – Part 3
I suggested in Part 2 that it might be possible to do some simple modulation of the amplitude of the AY-3-8910 channels rather than drive frequencies directly. This is taking a look at the possibilities of some kind of lo-fi direct digital synthesis using that as a basis.
- 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/uCSiBG5RBufGqspoHMYFPt
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:
- Arduino AY3891x Library: https://github.com/Andy4495/AY3891x
- Arduino Nano AY-3-8910 PCB: https://github.com/GadgetReboot/AY-3-8910
- AY-3-8910 on synth DIY wiki: https://sdiy.info/wiki/General_Instrument_AY-3-8910
If you are new to Arduino, see the Getting Started pages.
Parts list
- Arduino Uno.
- AY-3-8910 chip.
- Either GadgetReboot’s PCB or patch using solderless breadboard or prototyping boards.
- 5V compatible MIDI interface.
- Jumper wires.
Direct Digital Synthesis on the AY-3-8910
I’ve talked about direct digital synthesis before, so won’t go into full detail again. For more, see Arduino R2R Digital Audio – Part 3 and Arduino PWM Sound Output.
But the top-level idea is to set the level of the signal according to a value in a wavetable. If this value is updated at a useful audio rate then it will be interpreted as sound.
There are some pretty major limitations with attempting to do this on the AY-3-8910 however. The biggest one being that there are only 15 levels for the output on each channel.
So I’ll be working to the following properties:
- 4-bit resolution for the output.
- 8-bit wavetable.
- 8.8 fixed point accumulator to index into the wavetable.
- 8096 Hz sample rate.
YouTuber https://www.youtube.com/@inazumadenki5588 had a look at this and showed that the AY-3-8910 needs to be set up as follows:
- Frequency value for the channel should be set to the highest frequency possible.
- All channels should be disabled.
This is due to comments in the datasheet stating that the only way to fully disable a channel is to have 0 in the amplitude field.
Note: for a 8192 sample rate, that means writing out a sample to the AY-3-8910 registers approximately once every 124uS. With a 256 value wavetable, it takes almost 32 mS to write a complete cycle at the native sample rate, which would be around a 30 Hz output.
I’m not sure what the largest increment that would still give a useful signal might be, but say it was 8 values from the wavetable, then that would make the highest frequency supported around 1kHz. Not great, but certainly audible, so worth a try.
Setting up for DDS
I want a regular, reliable, periodic routine to output the levels from the wavetable, and the usual way to achieve this is using a timer and interrupt. As Timer 1 is already in use to generate the 1MHz clock for the AY-3-8910, I’m going to be configuring Timer 2 as follows:
- Timer 2 is an 8-bit timer.
- Use prescalar of 32 which gives a 500kHz clock source (16MHz/32).
- Use CTC (clear timer on compare) mode.
- Generate a compare match interrupt.
- Do not enable any output pins.
The appropriate ATMega328 registers to enable this are:
// COM2A[1:0] = 00 No output
// WGM2[2:0] = 010 CTC mode
// CS2[2:0] = 011 Prescalar=32
ASSR = 0;
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS21) | _BV(CS20);
TCNT2 = 0;
OCR2A = 60;
TIMSK2 = _BV(OCIE2A);Although it is worth noting that enabling OC1A can be quite useful for debugging. The following toggles the OC2A output (on D11) every time there is a compare match. The frequency seen on D11 will thus be half the anticipated sample frequency.
pinMode(11, OUTPUT);
TCCR2A |= _BV(COM2A0); // COM2A[1:0] = 01 for OC2A toggleAnd this does indeed generate a signal. Here is a trace showing a timing GPIO pin and the AY-3-8910 output.
The problem is that this is meant to be a 440Hz sine wave, and whilst the shape isn’t too bad (it is a little distorted as the amplitude isn’t a true linear shape), the frequency is much nearer 100Hz than 440.
Analysis of Performance
The clue is the other trace, which is a timing pin being toggled every time the Interrupt routine is called. This is showing a 1kHz frequency, which means the IRS is being called with a 2kHz frequency rather than the anticipated 8192Hz. Curiously though I am getting an accurate 4kHz toggle on the timer output pin OC1A indicating the timer is correctly counting with a 8kHz frequency.
No matter how I configured things, the interrupt routine just would not do anything at a faster rate. I had to drop the frequency right down to 2kHz to get the output pin and interrupt routing running together. This means that something in the interrupt routine seems to be taking ~ 450uS to run.
After a fair bit of prodding and probing and checking the ATMega328 datasheet and double checking the register values, I have to conclude that the AY3891x library is just too slow at updating the registers for it to be able to run from the interrupt routine at this speed.
Taking a look at the register write() function in the library, which I need to use to update the channel level, I can see the following is happening:
void AY3891x::write(byte regAddr, byte data) {
latchAddressMode(regAddr);
daPinsOutput(data);
noInterrupts();
mode010to110();
mode110to010();
interrupts();
daPinsInput();
}
void AY3891x::latchAddressMode(byte regAddr) {
mode010to000();
daPinsOutput(_chipAddress | regAddr); // Register address is 4 lsb
mode000to001();
mode001to000();
mode000to010();
}
void AY3891x::daPinsOutput(byte data) {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], OUTPUT);
}
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) {
digitalWrite(_DA_pin[i], data & 0x01);
data = data >> 1;
}
}
}
void AY3891x::daPinsInput() {
byte i;
for (i = 0; i < NUM_DA_LINES; i++) {
if (_DA_pin[i] != NO_PIN) pinMode(_DA_pin[i], INPUT);
}
}And every one of those modeXXXtoYYY() functions is a call to digitalWrite(), so I make that 22 calls to ditigalWrite() in order to write a single register value, plus around 16 calls to pinMode(). There are also 5 loops each looping over 8 values.
One person measured the Arduino Uno digitalWrite() function and concluded that it takes 3.4uS to run, so that is a minimum of 75uS of processing in every run through the interrupt routine just for those calls alone. That doesn’t include the calls and other logic going on. It could easily be more than twice that when everything is taken into account.
Dropping in some temporary pin IO either side of the call to the AY write function itself, and I’m measuring just over 250uS for the register update to happen, and that is just for one channel. This means that anything with a period of that or faster is starving the processor from running at all.
Measuring the Basic Performance
At this point I took a step back and created a free-running test sketch to really see what is going on.
#include "AY3891x.h"
AY3891x psg( 17, 8, 7, 6, 5, 4, 3, 2, 16, 15, 14);
#define AY_CLOCK 9 // D9
void aySetup () {
pinMode(AY_CLOCK, OUTPUT);
digitalWrite(AY_CLOCK, LOW);
TCCR1A = (1 << COM1A0);
TCCR1B = (1 << WGM12) | (1 << CS10);
TCCR1C = 0;
TIMSK1 = 0;
OCR1AH = 0;
OCR1AL = 7; // 16MHz / 8 = 2MHz Counter
psg.begin();
// Output highest frequency on each channel, but set level to 0
// Highest freq = 1000000 / (16 * 1) = 62500
psg.write(AY3891x::ChA_Amplitude, 0);
psg.write(AY3891x::ChA_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChA_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChB_Amplitude, 0);
psg.write(AY3891x::ChB_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChB_Tone_Period_Fine_Reg, 0);
psg.write(AY3891x::ChC_Amplitude, 0);
psg.write(AY3891x::ChC_Tone_Period_Coarse_Reg, 0);
psg.write(AY3891x::ChC_Tone_Period_Fine_Reg, 0);
// LOW = channel is in the mix.
// Turn everything off..
psg.write(AY3891x::Enable_Reg, 0xFF);
}
int toggle;
void setup() {
pinMode(11, OUTPUT);
toggle = LOW;
digitalWrite(11, toggle);
aySetup();
}
void loop() {
toggle = !toggle;
digitalWrite(11, toggle);
for (int i=0; i<16; i++) {
psg.write(AY3891x::ChA_Amplitude, i);
}
}All this is doing is continually writing 0 to 15 to the channel A level register whilst toggling a GPIO pin. Putting an oscilloscope trace on the IO pin and the AY-3-8910 channel A output gives me the following:
This is running with a period of 6.96mS, meaning each cycle of 16 writes takes 3.5mS, giving me almost 220uS per call to the AY write function which seems to align pretty well with what I was seeing before.
And this is generating an audible tone at around 280Hz, so regardless of any timer settings or waveform processing, this is going to be the baseline frequency on which everything else would have to rest, which isn’t great.
Optimising Register Writes
So at this point I have the choice of attempting to write to the AY-3-8910 myself using PORT IO to eliminate the time it takes for all those loops and digitalWrite() calls. Or I could try some alternative libraries.
The library I’m using aims for the most portable compatibility: “This library uses the generic
digitalWrite()function instead of direct port manipulation, and should therefore work across most, if not all, processors supported by Arduino, so long as enough I/O pins are available for the interface to the PSG.”It is a deliberate design choice, but does require all three bus control signals to be used: BDIR, BC1, BC2.
Alternatives are possible with less pin state changes, but much stricter timing requirements. Some options include:
- https://github.com/53175ddd/AY-3-8910_Arduino – uses a mixture of PORT IO and digitalWrite(). Assumes use of D0-D7 for data channel.
The following are projects that have not used a library, but just done their own thing:
- https://github.com/internalregister/AY-3-8910 – uses a mixture of digitalWrite and PORT IO. Assumes use of D0-D7 for the data channel.
- https://github.com/GaryA/TB-AY-3_MIDI – uses direct PORT IO for D2-D9
Unfortunately none of these really solves the problem as the PCB I’m using does not neatly map onto IO ports to allow the use of direct PORT IO for the data.
So to improve things whilst using this same PCB will require me to re-write the library myself.
As a test however, it is possible to take the IO pin definitions used with the PCB and write a bespoke, optimised register write routine as follows:
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
digitalWrite(BC1, HIGH);
digitalWrite(BDIR, HIGH);
// Latch address
// NB: Addresses are all in range 0..15 so don't need to
// worry about writing out bits 6,7 - just ensure set to zero
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
delayMicroseconds(10);
// Mode = Write
digitalWrite(BC1, LOW);
digitalWrite(BDIR, HIGH);
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Mode = Inactive
digitalWrite(BC1, LOW);
digitalWrite(BDIR, LOW);
}I’m using the following mapping of data pins to Arduino digital IO pins to PORTS:
DA0-DA5D2-D7PORTD Bits 0-5DA6D8PORT B Bit 0DA7A3/D17PORT C Bit 3To make this happen I have to ensure that the right bits are set to OUTPUTs and that BC2 is held HIGH prior to using the fastWrite function.
digitalWrite(BC2, HIGH);
DDRD |= 0xFC;
DDRC |= 0x04;
DDRB |= 0x01;This now improves on that previous 280Hz and gives me 1600Hz performance.
So can I do any better? Well there are still between 6 and 8 calls to digitalWrite going on to handle the control signals…
#define BC1LOW {PORTC &= 0xFE;} // A0 LOW
#define BC1HIGH {PORTC |= 0x01;} // A0 HIGH
#define BC2LOW {PORTC &= 0xFD;} // A1 LOW
#define BC2HIGH {PORTC |= 0x02;} // A1 HIGH
#define BDIRLOW {PORTC &= 0xF7;} // A2 LOW
#define BDIRHIGH {PORTC |= 0x04;} // A2 HIGH
void ayFastWrite (byte reg, byte val) {
// Mode=Addr Latch
BC1HIGH;
BDIRHIGH;
// Latch address
PORTD = (PORTD & 0x03) | ((reg & 0xCF)<<2);
PORTB = (PORTB & 0xFE);
PORTC = (PORTC & 0xF7);
// Need 400nS Min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS settle then 50nS preamble
delayMicroseconds(1);
// Mode = Write
BC1LOW;
BDIRHIGH;
// Write data
PORTD = (PORTD & 0x03) | ((val & 0xCF)<<2); // Shift bits 0:5 to 2:7
PORTB = (PORTB & 0xFE) | ((val & 0x40)>>6); // Shift bit 6 to 0
PORTC = (PORTC & 0xF7) | ((val & 0x80)>>4); // Shift bit 7 to 3
// Need 500nS min
delayMicroseconds(1);
// Mode = Inactive
BC1LOW;
BDIRLOW;
// Need 100nS min
}The timings come from the AY-3-8910 datasheet:
The actual minimum and maximum timings for the various “t” values are given in the preceeding table. Most have a minimum value, but tBD has to be noted: the “associative delay time” is 50nS. This means that any changing of BC1, BC2 and BDIR has to occur within 50nS to be considered part of the same action.
There is no means of having a nano-second delay (well, other than just spinning code), so I’ve just used a delayMicroseconds(1) here and there. This isn’t reliably accurate on an Arduino, but as I’m have delays of around half of that as a maximum it seems to be fine.
This now gives me the following:
This is now supporting a natural “as fast as possible” frequency of around 24kHz, meaning each call to the write function is now around 3uS. That is almost a 100x improvement over using all those pinMode and digitalWrite calls.
The downside of this method:
- It is ATMega328 specific.
- It is specific to the pin mappings and PORT usage of this PCB.
- It does not support reading or other chip operations between the writes.
It is also interesting to see that the traces also show the high frequency oscillation (62.5kHz) that is being modulated regardless of the channel frequency and enable settings.
DDS Part 2
Success! At least with a single channel. This is now playing a pretty well in tune 440Hz A.
Notice how the frequency of the timing pin is now ~4.2kHz meaning that the ISR is now indeed firing at the required 8192 Hz.
Here is a close-up of the output signal. The oscilloscope was struggling to get a clean frequency reading, but this is one time I caught it reading something close! I checked the sound itself with a tuning fork (see video). It is indeed 440Hz.
Closing Thoughts
I wanted to get something put together to allow me to drive a DSS wavetable over MIDI, with different waveforms, and so on, but it turned out to be a little more involved getting this far than I anticipated, so I’ll leave it here for now.
But hopefully filling in the gaps won’t take too long and will be the subject of a further post.
Now that I have something that works, I’m actually quite surprised by how well it is working.
Kevin
#arduinoNano #ay38910 #dds #define #directDigitalSynthesis #include #midi
-
Русская «Ардуина»: первый взгляд любителя
Я — самодельщик-ардуинщик со стажем. Люблю пихать ардуины во всякие подходящие и не очень места. Как-то раз я уже показывал свою коллекцию Arduino-совместимых плат, и с тех пор она только росла и ширилась. Теперь в ней случилось особенное пополнение: русская (пока не) народная «Ардуина» ELBEAR от сибирской компании «Элрон» на базе отечественного микроконтроллера MIK32 «Амур», о существовании которой я узнал несколько дней назад из статьи на Хабре . В статье я изложу частный опыт искушённого любителя, который пытается импортозаместить зарубежную Arduino и приспособить данную плату для своих любительских нужд, не залезая в дебри. Конечно, это далеко не первая подобная публикация, с поездкой на поезде хайпа я припозднился примерно на годик. Зато она отражает актуальное положение дел и демонстрирует, чем чреват смелый прыжок веры прямо в неизвестность без предварительного изучения вопроса. К тому же, я не самый обычный ардуинщик. Вкусы мои специфичны: я не сделал ни одной метеостанции, мой дом глуп как пробка, и даже мои часы на Arduino — стрелочные. Вместо этого я делаю вещи, так или иначе связанные с электронными и видеоиграми, демосценой, звуком и музыкой с уклоном в ретро. И разнообразные ардуины мне нужны и интересны именно в этом контексте. А значит, есть шанс, что будет интересно.
https://habr.com/ru/companies/ruvds/articles/919202/
#ruvds_статьи #arduino #arduino_nano #arduino_uno #arduino_ide #ардуина #микроконтроллеры #микроэлектроника #ws2812 #adafruit #амур #мик32_амур #mik32_amur #amur #К1948ВК018 #микрон #элрон #ELBEAR #ELBEAR_ACEUNO #ELBEAR_ACENANO #AY38910 #ST7789 #SH1106 #ILI9488 #ili9341 #импортозамещение
-
Русская «Ардуина»: первый взгляд любителя
Я — самодельщик-ардуинщик со стажем. Люблю пихать ардуины во всякие подходящие и не очень места. Как-то раз я уже показывал свою коллекцию Arduino-совместимых плат, и с тех пор она только росла и ширилась. Теперь в ней случилось особенное пополнение: русская (пока не) народная «Ардуина» ELBEAR от сибирской компании «Элрон» на базе отечественного микроконтроллера MIK32 «Амур», о существовании которой я узнал несколько дней назад из статьи на Хабре . В статье я изложу частный опыт искушённого любителя, который пытается импортозаместить зарубежную Arduino и приспособить данную плату для своих любительских нужд, не залезая в дебри. Конечно, это далеко не первая подобная публикация, с поездкой на поезде хайпа я припозднился примерно на годик. Зато она отражает актуальное положение дел и демонстрирует, чем чреват смелый прыжок веры прямо в неизвестность без предварительного изучения вопроса. К тому же, я не самый обычный ардуинщик. Вкусы мои специфичны: я не сделал ни одной метеостанции, мой дом глуп как пробка, и даже мои часы на Arduino — стрелочные. Вместо этого я делаю вещи, так или иначе связанные с электронными и видеоиграми, демосценой, звуком и музыкой с уклоном в ретро. И разнообразные ардуины мне нужны и интересны именно в этом контексте. А значит, есть шанс, что будет интересно.
https://habr.com/ru/companies/ruvds/articles/919202/
#ruvds_статьи #arduino #arduino_nano #arduino_uno #arduino_ide #ардуина #микроконтроллеры #микроэлектроника #ws2812 #adafruit #амур #мик32_амур #mik32_amur #amur #К1948ВК018 #микрон #элрон #ELBEAR #ELBEAR_ACEUNO #ELBEAR_ACENANO #AY38910 #ST7789 #SH1106 #ILI9488 #ili9341 #импортозамещение
-
Русская «Ардуина»: первый взгляд любителя
Я — самодельщик-ардуинщик со стажем. Люблю пихать ардуины во всякие подходящие и не очень места. Как-то раз я уже показывал свою коллекцию Arduino-совместимых плат, и с тех пор она только росла и ширилась. Теперь в ней случилось особенное пополнение: русская (пока не) народная «Ардуина» ELBEAR от сибирской компании «Элрон» на базе отечественного микроконтроллера MIK32 «Амур», о существовании которой я узнал несколько дней назад из статьи на Хабре . В статье я изложу частный опыт искушённого любителя, который пытается импортозаместить зарубежную Arduino и приспособить данную плату для своих любительских нужд, не залезая в дебри. Конечно, это далеко не первая подобная публикация, с поездкой на поезде хайпа я припозднился примерно на годик. Зато она отражает актуальное положение дел и демонстрирует, чем чреват смелый прыжок веры прямо в неизвестность без предварительного изучения вопроса. К тому же, я не самый обычный ардуинщик. Вкусы мои специфичны: я не сделал ни одной метеостанции, мой дом глуп как пробка, и даже мои часы на Arduino — стрелочные. Вместо этого я делаю вещи, так или иначе связанные с электронными и видеоиграми, демосценой, звуком и музыкой с уклоном в ретро. И разнообразные ардуины мне нужны и интересны именно в этом контексте. А значит, есть шанс, что будет интересно.
https://habr.com/ru/companies/ruvds/articles/919202/
#ruvds_статьи #arduino #arduino_nano #arduino_uno #arduino_ide #ардуина #микроконтроллеры #микроэлектроника #ws2812 #adafruit #амур #мик32_амур #mik32_amur #amur #К1948ВК018 #микрон #элрон #ELBEAR #ELBEAR_ACEUNO #ELBEAR_ACENANO #AY38910 #ST7789 #SH1106 #ILI9488 #ili9341 #импортозамещение
-
Русская «Ардуина»: первый взгляд любителя
Я — самодельщик-ардуинщик со стажем. Люблю пихать ардуины во всякие подходящие и не очень места. Как-то раз я уже показывал свою коллекцию Arduino-совместимых плат, и с тех пор она только росла и ширилась. Теперь в ней случилось особенное пополнение: русская (пока не) народная «Ардуина» ELBEAR от сибирской компании «Элрон» на базе отечественного микроконтроллера MIK32 «Амур», о существовании которой я узнал несколько дней назад из статьи на Хабре . В статье я изложу частный опыт искушённого любителя, который пытается импортозаместить зарубежную Arduino и приспособить данную плату для своих любительских нужд, не залезая в дебри. Конечно, это далеко не первая подобная публикация, с поездкой на поезде хайпа я припозднился примерно на годик. Зато она отражает актуальное положение дел и демонстрирует, чем чреват смелый прыжок веры прямо в неизвестность без предварительного изучения вопроса. К тому же, я не самый обычный ардуинщик. Вкусы мои специфичны: я не сделал ни одной метеостанции, мой дом глуп как пробка, и даже мои часы на Arduino — стрелочные. Вместо этого я делаю вещи, так или иначе связанные с электронными и видеоиграми, демосценой, звуком и музыкой с уклоном в ретро. И разнообразные ардуины мне нужны и интересны именно в этом контексте. А значит, есть шанс, что будет интересно.
https://habr.com/ru/companies/ruvds/articles/919202/
#ruvds_статьи #arduino #arduino_nano #arduino_uno #arduino_ide #ардуина #микроконтроллеры #микроэлектроника #ws2812 #adafruit #амур #мик32_амур #mik32_amur #amur #К1948ВК018 #микрон #элрон #ELBEAR #ELBEAR_ACEUNO #ELBEAR_ACENANO #AY38910 #ST7789 #SH1106 #ILI9488 #ili9341 #импортозамещение
-
A fascinating new look at what is basically an Apple II Mockingboard soundcard...for the PC...sold by Mindscape.
"Do you own the rarest PC sound card in the world?" -- https://www.youtube.com/watch?v=Eeo4INoGyRY
#AppleII #IBMPC #DOSPC #DOS #soundcard #soundboard #AY38910 #SweetMicroSystems #Mockingboard #Mindscape #chiptune #audio #PCaudio #soundblaster #adlib #vintagecomputing #retrocomputing #retrogaming #computinghistory #vintageapple #deepdive #video #gamers #games #interview #ADSR #nostalgia #tech
-
Устройство «музыкалки» AY-3-8910 и эмулятор на Arduino
Астрологи объявили месяц статей про ZX Spectrum, звук и Arduino. Количество публикаций увеличилось вдвое! В прошлый раз я показывал, как можно перенести однобитные, они же «биперные», музыкальные «движки» с ZX Spectrum на Arduino. Но всё-таки это была максимально нишевая тема внутри нишевой темы, весьма узкоспециализированный проект. Сегодня сделаю шаг ближе к народу. Расскажу, как работает легендарный звуковой чип AY-3-8910, как написать его простой эмулятор полностью с нуля, и как проиграть им музыкальный файл формата PT3 на обычной Arduino Nano.
https://habr.com/ru/companies/ruvds/articles/884436/
#arduino #arduino_uno #arduino_nano #ардуино #ay38910 #ay38912 #ym2149f #chiptune #vgm #music #эмуляция #эмулятор #синтез_звука #синтезатор #psg #pwm #pwm_dac #ruvds_статьи
-
ZX Spectrum vs. Commodore 64 – ATF Face-Off with an Audio Upgrade!
#Commodore64 #C64 #ZXSpectrum #RetroGaming #ATFGame #8BitComputers #AY38910 #SID6581 #GamingHistory #TripleFreedom
https://theoasisbbs.com/zx-spectrum-vs-commodore-64-atf-face-off-with-an-audio-upgrade/?feed_id=1667&_unique_id=67af4e4b9859a -
AY3-8910 tries to do SID voice. Syncing the internal modulator to the bass note periods for pretend pwm. Not bad at all for the humble AY without resorting to the intense cpu control of the volume registers, as might be found in much demoscene stuff.
#ay38910 #soundchip #chiptune #zxspectrum #amstradcpc #vectrex #sidchip #c64 #eurorack #eurorackmodule #thegoat
-
More AY3 / YM2149 sound chip action. AY300 and YM300, each as a three voice stack, with some SID drums as the driver. Lots of lovely trills when it gets going and some buff octaving C64 style bass line action. The kind of tune that'd definitely have had the telly cranked in my school daze. It's awesome👾🕹📺🙂
#AY38910 #ym2149 #psg #programmablesoundgenerator #soundchip #chiptune #eurorack #thegoat #zxspectrum #atarist #sidchip #eurorackmodule
-
Okay, so I tired this new program for #rc2014 which is called #msx8 and will allow you to run a selection of #MSX #Game #ROMs on your system if you have a #TMS9918A video module and a #AY38910 sound module.
So I fetched the latest sources and built a set of fresh binaries for my system, including the dummy MSX-US.ROM file you need where I adjusted the IO port address of my AY38910 module ($D8).
Now, the initial screen loads but then the system freezes. No sound, not reacting to keypresses.
Anyone got an idea?!
EDIT: I may have found a possible answer here (https://hackaday.io/project/190408/instructions) which states that a Rev. 6 #AY38910 #module is required, which allows for MSX #IO #Ports - Rev. 5 boards (like the one I have) do not support these.
p.s.: I'm running a #RomWBW system here.
p.p.s.: project repo is here -> https://github.com/lesbird/MSX8/tree/main?mc_cid=e127d99778
-
SN76489 conversion of a new h0ffman track from the Oric demo "His Masters Rasters" released at the Nova 2024 demo party last weekend: https://pouet.net/prod.php?which=97165
I can't reproduce all effects used on the AY chip via the SN, but still sounds great.
-
It's Revision 2024 weekend, and I'm hearing some great tunes to get playing on the BBC Micro's SN76489 through my converters. Here's the 1st: Oldskool Demo "Ghost NOP" by Futurs' & Pulpo Corrosivo.
-
I'm still on track for a new module every month in 2024, I am pleased to announce that the RC2014 Why Em-Ulator Sound Module is now available at z80kits!
This does away with the reliance on old YM2149 / AY-3-8910 or the gamble of buying pulled chips from eBay with AVR-AY firmware running on an ATMega.
If you don't like the idea of a small surface mount micro spoiling your vintage looking RC2014, don't worry, it can be hidden under any DIP40 chip!
-
Anyone familiar with #RP2040 and #PiPico stuff? I'm pretty much a beginner with microcontrollers/hardware and I'm trying to port over some #Arduino test code to run on it, however there are some details I can't quite figure out (mostly in regards to clock signal generation and writing 8-bit values over GPIO).
I'm trying to power a #YM2149 (or #AY38910) sound chip using it, unfortunately however I can't seem to get any sound out of the thing. Here's the original code I'm trying to port: https://github.com/internalregister/AY-3-8910/blob/master/AY-3-8910_test.ino
And here's my current attempt: https://gist.github.com/TheEssem/c5d8c9802d70eb9e02843036ff3d69e1
If anyone could help figure out what's wrong with this code, that would be greatly appreciated.