@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.

#AY38910 #YM2149 #YM2149F

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/

#AY38910 #RetroComputing

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:

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_APB1D9CFG0PB0D8CFG1PD5D5RXPD0D0TXPD1D1VCCVCCVCCVCCGNDGNDGNDGND

There 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 frequency

In 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
:00000001FF

We 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-D7

Here 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 / PB2OC1BOCR1BL1

The 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 lines

There 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 A

This 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 value

This 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

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!

#AY38910 #RetroComputing

GitHub - mengstr/Discrete-AY-3-8910: AY-3-8910 made out of discrete 74-series logic ICs

AY-3-8910 made out of discrete 74-series logic ICs - mengstr/Discrete-AY-3-8910

GitHub
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
YM2149-rs | Chiptune Sound Synthesis in Rust

A comprehensive Rust ecosystem for YM2149/AY-3-8910 sound chip emulation, chiptune playback, and retro audio visualization.

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/

#AY38910 #TimFollin #ZXSpectrum

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 sound

The 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 HEX

This 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 21

All 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
RET

Basically 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
RET

I’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
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…

Simple DIY Electronic Music Projects

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:

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

#ay38910 #cpm #midi #rc2014 #rc2040 #romwbw #sid #z80