Article History
 
 
 

207 views
100% ViLXDRYAD

Program for the SNES APU
 

::|CONTENTS

  1. Overview
  2. Resources
  3. Example code

Overview



A peculiar startup procedure ought to be performed to operate the SNES APU, in overview, consisting of:
Read $aa from memory port $2140
Read $bb from memory port $2141
Write $cc to memory port $2140
Send BRR sample to SNES APU memory
Set DSP registers to play audio on a channel

Resources



FullSNES
documentation regarding the SNES APU
ASAR
SNES assembler; AddMusicK uses it too
65c816 opcodes
documentation in 6502.org
MAME
for its neat debugger! One may open with, using an archive or folder named as snes, containing a respective file named as spc700.rom, using a command like this: mame snes -cart output.sfc -debug
no$sns
albeit features a disassembler with different mnemonics than seen on some other assemblers and disassemblers, it has a nice sound I/O map viewer one may use to debug with

Example code



This example code uses ASAR v1.81
to assemble; using the next command
asar --fix-checksum=on example.s output.sfc

Delete output.sfc before assembling to ensure the output is as it assembles itself rather than patched to

It works in real hardware


; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; This program plays a square wave BRR sample on the SNES APU channel 0
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
org $8000

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Memory address map
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
!AUDIO_MASTER_TICK_CLOCK = $00 ; Zero page address 0x00

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; The BRR sample used in this example
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
brr_sample: ; to be sent at address 0x200 of SNES APU
dw $0204 ; start point of this sample
dw $0204 ; loop start point of this sample
db $c3 ; start of 9 byte block, shifts left 12 bits and sets loop point and sample end flags
db $77,$77,$77,$77,$99,$99,$99,$99
brr_sample_end:
!brr_sample_len = brr_sample_end-brr_sample ;Used to calculate its byte length

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; The code begins executing from here
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
reset_6502_mode_vector:
sei ; Turns off IRQs
cld ; Turns off decimal mode
clc
xce ; set processor 65c816 mode
sep #$30 ; set accumulator and index registers to be 8-bit wide each instead of 16-bit

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Upon turning on the SNES, waiting for $2140 to be $aa and $2141 to read $bb indicateS the SNES APU to be ready
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
.wait_for_spu_startup_signal:
lda $2140
cmp #$aa ; The first signal that the SNES APU sends
bne .wait_for_spu_startup_signal
lda $2141
cmp #$bb ; The second signal that the SNES APU sends
bne .wait_for_spu_startup_signal

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Send the $cc to 0x2140 is part of the SNES APU initialization procedure
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
lda #$cc ; It will be sent to, in the next send_snes_apu_command_and_wait subroutine
sta !AUDIO_MASTER_TICK_CLOCK

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Set the address to send the BRR sample to SNES APU memory at
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
stz $2142 ; low byte of address to send data at
lda #$02 ; high byte of address to send sample data: SNES APU address 0x0200 is above its i/o ports and stack
sta $2143 ; high byte of address to send data at

jsr send_snes_apu_command_and_wait ; send start command: Register a is non-zero
stz !AUDIO_MASTER_TICK_CLOCK ; set master tick clock to zero

ldy #$00 ; byte index of the sample
.send_next_byte_of_brr_sample
lda brr_sample, y ; send a byte of the BRR sample to the SNES APU memory
jsr send_snes_apu_command_and_wait
iny ; increase the byte index of the sample
cpy.b #!brr_sample_len
bne .send_next_byte_of_brr_sample

inc !AUDIO_MASTER_TICK_CLOCK
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Set SNES APU registers to produce a sound
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ldx #$6c ; FLG DSP register
ldy #$80 ; All envelopes are set to zero, all eight channels are set key off
jsr write_to_snes_apu_register

ldy #$30 ; echo buffer writes are enabled, mute amplifier is set to mute, and noise frequency is set to 0
jsr write_to_snes_apu_register

ldx #$5d ; address of the sample table DSP register
ldy #$02 ; high byte of address of the sample table, low byte is 0x00
jsr write_to_snes_apu_register

ldy #$20 ; master volume value
ldx #$0c ; channel 0 master volume left master
jsr write_to_snes_apu_register
ldx #$1c ; channel 0 master volume right master
jsr write_to_snes_apu_register

ldx #$04 ; instrument number for channel 0 DSP register
ldy #$00 ; first instrument
jsr write_to_snes_apu_register

ldx #$05 ; ADSR setting DSP register for channel 0: $00 sets it to use GAIN instead of ADSR
jsr write_to_snes_apu_register

ldx #$07 ; GAIN value DSP register for channel 0
ldy #$1f ; Up to $7f
jsr write_to_snes_apu_register

ldy #$7f ; maximum volume value
ldx #$00 ; channel 0 left volume DSP register
jsr write_to_snes_apu_register
ldx #$01 ; channel 0 right volume DSP register
jsr write_to_snes_apu_register

ldx #$4c ; Key on DSP register
ldy #$01 ; Set channel 0 key on
jsr write_to_snes_apu_register

ldx #$02 ; low byte of channel 0 pitch DSP register
ldy #$00
jsr write_to_snes_apu_register

ldx.b #$03 ; high byte of channel 0 pitch DSP register
ldy.b #$04
jsr write_to_snes_apu_register

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; End of code flow
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
main_loop:
bra main_loop


; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; A subroutine to send a command to the SNES APU and wait for it to be ready
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
send_snes_apu_command_and_wait:
sta $2141 ; command or data
lda !AUDIO_MASTER_TICK_CLOCK ; set the clock: The first time, sending 0xcc is part of the initialization procedure
sta $2140 ; set the master clock
inc !AUDIO_MASTER_TICK_CLOCK ; increase the master clock
.wait_for_master_clock:
cmp $2140 ; wait for it to be the same as master clock
bne .wait_for_master_clock
rts

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; A subroutine for the SNES APU to write at its DSP registers
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
write_to_snes_apu_register:
lda #$f2 ;\
sta $2142 ; Low byte of destination to SNES APU, $f2 is DSP index register
stz $2143 ; High byte of destination to SNES APU
jsr send_snes_apu_command_and_wait ; begin command, $f2 being non-zero
stz !AUDIO_MASTER_TICK_CLOCK ; Initialize clock

; send a byte to DSP index register, $f2
txa
jsr send_snes_apu_command_and_wait
; send a byte to DSP data register, $f3
tya
jsr send_snes_apu_command_and_wait
inc !AUDIO_MASTER_TICK_CLOCK ; this clock must be bigger than before, and higher than $00
rts

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Interrupt vectors
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cop_65c816_mode_vector:
brk_65c816_mode_vector:
nmi_65c816_mode_vector:
irq_65c816_mode_vector:
cop_6502_mode_vector:
nmi_6502_mode_vector:
irq_and_brk_6502_mode_vector:
rti

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; Start SNES "header" data
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
org $ffc0
snes_header:
db "SOUND ENGINE TEST " ; 21 bytes upper case title padded with spaces
db $20 ; rom makeup or rom speed, and map mode
db $00 ; chipset rom and ram information
db $05 ; denotes ROM size
db $00 ; denotes RAM size
db $00 ; country: Information one may associate with NTSC or PAL paradigms
db $00 ; informs developer ID code
db $00 ; informs software version
dw $ffff ; checksum complement: the checksum field XORd with $ffff
dw $0000 ; checksum of all bytes in ROM, with checksum complement field as $ffff, and checksum field as $0000

; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
; SNES vector addresses
; xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
dd $00000000 ; may denote wram-bootable file
dw cop_65c816_mode_vector ; COP opcode vector
dw brk_65c816_mode_vector ; BRK opcode vector
fill 2 ; abort vector, unused in SNES
dw nmi_65c816_mode_vector ; v-blank interrupt
fill 2 ; unused
dw irq_65c816_mode_vector ; h-vtimer, or external interrupt
; 6502 mode vectors
fill 4 ; unused
dw cop_6502_mode_vector ; COP opcode vector
fill 2 ; unused
fill 2 ; abort vector, unused in SNES
dw nmi_6502_mode_vector ; v-blank interrupt
dw reset_6502_mode_vector ; entry point
dw irq_and_brk_6502_mode_vector ; external interrupt or BRK opcode