Sunday, August 8, 2021

Megadrive / Genesis assembly - Z80 sound driver example

[ Buildable example written in C: 
https://github.com/bferguson3/m68k-gcc-pi/tree/main/projects/z80test ]

 If you've poked around in the Megadrive/Genesis development community for any period of time, you've probably found the official documentation from SEGA, such as it is. It's very roughly translated, and there are a few errors (if you don't have the errata, uh-oh!).

Well, if you are like me and like doing everything the hard way, this is rough. There aren't any great resources out there for doing low level Megadrive coding, excepting the occasional raw disassembly listings (like Sonic 1 and 2). Driving the sound is especially hard because you need to be roughly familiar with not just 68000, but Z80 as well. 

I scoured around for what I could, but besides some very old code examples (some 'current' links are over 10 years at this point) I remained somewhat baffled. The Genesis Sound Software Manual, in particular, includes a very cryptic "example program" - essentially a listing of the FM register values. 

It goes like this (not quite verbatim):




This is all well and good, but there aren't any code examples given in the manual in _either_ 68000 or Z80 (since you can use either to drive the sound chip - Sonic is a famous case where the sequel changed from a 68k music driver to a z80 music driver). 

The advantage of launching the music player on the Z80 is that the 68000 can be free to do pure graphics processing and won't be caught up on certain hardware hangups, like waiting for the Z80 I/O bus to be free. 

The trick of course, is actually doing it on the hardware.
This stumped me for like two months! Ouch!

So let's dig in. First of all, the manual gives this short description of activating the Z80:

68K CONTROL OF Z-80
Start-Up Operation Sequence:

1. BUS REQ ON
2. BUS RESET OFF
3. 68k copies program into Z-80 S-RAM
4. BUS RESET ON 
5. BUS REQ OFF 
6. BUS RESET OFF 

BUS REQUEST ON
DATA 100H (WORD) -> $A11100

BUS REQ OFF 
DATA 0H (WORD) -> $A11100

RESET Z80 ON
DATA 0H (WORD) -> $A11200

RESET Z80 OFF
DATA 100H (WORD) -> $A11200
 * Requires 26ms

CONFIRM BUS STATUS
bit 0 of $a11100
1 - 68K can access, 0 - z80 is using

From this, we can attempt to write a 68000 program, following the only steps we are given. 

PRGSIZE equ _endprg-Z80PRG
Z80_BUS equ $a11100
Z80_RESET equ $a11200

    org *

move.w $100,(Z80_BUS)
move.w $100,(Z80_RESET)
movea.l Z80PRG,a0
movea.l $a00000,a1
move.l PRGSIZE,d1
.z80copyloop:
move.b (a0)+,d0
move.b d0, (a1)+
subq #1,d1
bne .z80copyloop
move.w $0,(Z80_RESET)
nop
nop
nop
nop
move.w $0,(Z80_BUS)
move.w $100,(Z80_RESET)

Z80PRG: defb etc etc
_endprg:

If you noticed the 26ms wait (this equals out to four NOPs) is actually after the RESET ON and not the RESET OFF call, you win the prize! This was taken from ROM disassemblies. It is unclear to me if RESET ON and RESET OFF are confused in the text (since the values BUS ON/OFF uses are opposite) or if the 26ms wait footnote is just in the wrong place, but either way, think about it logically for a moment.

After you reset a system, it needs a second to catch up. Writing $100 to $a11200 pauses the z80 in a sense - I think of this as entering it into "reset mode". When you write 0 to the reset bus, you are in another sense allowing it to exit "reset mode" and continue normal operation with the new program in memory - but only once it's cleaned up and ready to reset!

Anyway, the above code will certainly get SOME bytes into the Z80's memory, but what do we write?

First, and MOST IMPORTANTLY, the biggest rule of embedded development is DO NOT TRUST RANDOM MEMORY. Why, you may ask? Because of cases like this. The stack pointer on the Z80 could be anywhere. The memory could be clean, or it could be full of random digits that will immediately cause a stack corruption. 

The emulator dgen, one of my favorites for Linux dev and disassembly, does not initialize the Z80's memory the same as actual hardware. This was a pain point for me, because I was getting sound in dgen, but not on my actual Genesis 2. After a while of poking at this and that, I came across these two lines of code in the Z80 init portion of the Sonic 2 disassembly:

    di 

    ld sp, $1b80



Suddenly, everything was clear. I wasn't disabling interrupts (not that I thought that was the issue) but I wasn't setting the stack - I had no idea where it even started, and that's when I realized I wasn't zeroing the Z80's SRAM, either. Doing those two things got me sound on my hardware and inspired this blog post.

SO - The very first thing we want to do, before even thinking about writing to the FM registers, is make sure our Z80 is "sane".

FMREG EQU $4000
FMDAT EQU $4001
DATSIZE EQU ENDFMDATA-FMDATA

org $0

; disable interrupts
di

; clear the stack
ld a, 0
ld de, $1b00
ld b, 0
CLRSTACK:
LD (DE),A
inc de
djnz CLRSTACK

; set the stack pointer
ld sp,$1b80

This does the trick. 1b00-1bff will be set to 0, and the stack will be set to 1b80. The choice of 1b80 was mostly arbitrary - this is what it is set to for Sonic, so why not?

Now we can finally start writing the registers. For the large batch of initialization data (which is listed in the full listing below), its easier to wrap it all in a loop. I chose to store the register byte in B, the data byte in C, and write a loop around calling a separate function to write the register:


LD HL,FMDATA ; LENGTH OF DATA
LD BC,DATSIZE
srl b
rr c ; divided by two!
FMINITLOOP:
PUSH BC ; ++
; Store REG# and DATA in B and C
LD B,(HL)
INC HL
LD C,(HL)
INC HL
; Write FM1, preserving HL
PUSH HL
CALL WRITEFMR
POP HL
POP BC ; --
DEC BC
LD A,C
OR B ; quick check for 16bit 0
JR NZ,FMINITLOOP

General Z80 bits:
- SRL B ; RR C is a quick way of dividing 16bit BC by two. It is a logic shift pair. We divide by two because we are writing in two-byte pairs, so we want half the size of the total data block.
- LD A, C ; OR B is a fast 16bit zero-check. The flag is set from register A, which we compare quickly with both registers.

Here is the fairly easy WRITEFMR function:

;;;;;;;;;;
WRITEFMR:
;;;;;;;;;;;;;;;;;;;;;;;;;;;
; WRITE FM REGISTER
; * A, B, C
; INPUT:
; B = REG TO WRITE
; C = VALUE TO WRITE
call ZWAIT
; REG select
ld a,b
ld ($4000),a
call ZWAIT
; Write DATA
ld a,c
ld ($4001),a
RET

Before writing to the FM in any fashion, whether to the register select port or the data port, we have to wait until the bus is ready. This particular method (add $4000 to itself until it is <255) was taken from disassembly, but presumably BIT 7,a works just as well. 


;;;;;;;;;;
ZWAIT:
;;;;;;;;;;;;;;;;;;;;;;;;;;
; Waits until fm bus is ready.
; * A
LD A,($4000)
add a, a
JR c,ZWAIT
ret


Regardless of the method, I advise writing to $4000/$4001 directly, and not via indirect addressing JUST IN CASE - for no other reason than that's what SEGA does.

Great - now Channel 1 is set up and we can actually play a sound now!


START:
LD BC,$B032 ; feedback/alg
CALL WRITEFMR
LD BC,$B4C0 ; speakers on
CALL WRITEFMR
LD BC,$2800 ; KEY OFF
CALL WRITEFMR
LD BC,$A422
CALL WRITEFMR ; SET FREQ
LD BC,$A069
CALL WRITEFMR

LD BC,$28F0
CALL WRITEFMR ; KEY ON

Using the WRITEFMR method the way I implemented it, the rest of the program is easy to write. The exception comes when we need to wait before turning the key off again. I wrote a basic cycle wait, but you can do whatever you want. 

And that's it!

The main things to watch out for are:

- Wait 4 NOP (26ms) in between writing 0 to z80_reset and $100 to z80_reset

- Clear memory! Watch for interrupts! Set your stack pointer!!

- Wait before _every_single_write to FM and address them directly


Full Z80 source:
FMREG EQU $4000
FMDAT EQU $4001
DATSIZE EQU ENDFMDATA-FMDATA

org $0

; disable interrupts
di

; clear the stack
ld a, 0
ld de, $1b00
ld b, 0
CLRSTACK:
LD (DE),A
inc de
djnz CLRSTACK

; set the stack pointer
ld sp,$1b80

LD HL,FMDATA ; LENGTH OF DATA
LD BC,DATSIZE
srl b
rr c ; divided by two!
FMINITLOOP:
PUSH BC ; ++
; Store REG# and DATA in B and C
LD B,(HL)
INC HL
LD C,(HL)
INC HL
; Write FM1, preserving HL
PUSH HL
CALL WRITEFMR
POP HL
POP BC ; --
DEC BC
LD A,C
OR B ; quick check for 16bit 0
JR NZ,FMINITLOOP

START:
LD BC,$B032 ; feedback/alg
CALL WRITEFMR
LD BC,$B4C0 ; speakers on
CALL WRITEFMR
LD BC,$2800 ; KEY OFF
CALL WRITEFMR
LD BC,$A422
CALL WRITEFMR ; SET FREQ
LD BC,$A069
CALL WRITEFMR

LD BC,$28F0
CALL WRITEFMR ; KEY ON

LD C, 5
CALL WAIT ; SIMPLE WAIT

LD BC,$2800
CALL WRITEFMR ; KEY OFF

LOOP:
JP LOOP ; Done!

;;;;;;;;;;
WRITEFMR:
;;;;;;;;;;;;;;;;;;;;;;;;;;;
; WRITE FM REGISTER
; * A, B, C
; INPUT:
; B = REG TO WRITE
; C = VALUE TO WRITE
call ZWAIT
; REG select
ld a,b
ld ($4000),a
call ZWAIT
; Write DAT
ld a,c
ld ($4001),a
RET

;;;;;;;;;;
ZWAIT:
;;;;;;;;;;;;;;;;;;;;;;;;;;
; Waits until fm bus is ready.
; * A
LD A,($4000)
add a, a
JR c,ZWAIT
ret

;;;;;;;;;
WAIT:
;;;;;;;;;;;;;;;;;;;;;;;;;;;
; WAIT FOR FFFF * C CYCLES
; * A, C, H, L
; INPUT:
; C = NUM LOOPS

BIGLOOP:
LD HL,$ffff ; 64K loops
WAITLOOP:
DEC HL
LD A,L
OR H
JR NZ, WAITLOOP

DEC C
XOR A
OR C
JR NZ,BIGLOOP

RET
;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
FMDATA:
defb $22,0 ; lfo and dac
defb $27,0
defb $28,0
defb $2B,0
; 3x channel 0
defb $30,$71 ; dt1/mul
defb $34,$0D
defb $38,$33
defb $3c,$01
; 4x channel 0
defb $40,$23 ; total level
defb $44,$2d
defb $48,$26
defb $4c,$00
; 5x channel 0
defb $50,$5f ; rs/ar
defb $54,$99
defb $58,$5f
defb $5c,$94
; 6x channel 0
defb $60,5 ; am/d1r
defb $64,5
defb $68,5
defb $6c,7
; 7x channel 0
defb $70,2 ; d2r
defb $74,2
defb $78,2
defb $7c,2
; 8x channel 0
defb $80,$11 ; d1l/rr
defb $84,$11
defb $88,$11
defb $8c,$a6
; ??
defb $90,0
defb $94,0
defb $98,0
defb $9c,0
ENDFMDATA: