John Tsiombikas (Nuclear / Mindlapse)
7 July 2020
The Super Nintendo has the 65816 processor, a sad marginal improvement over the 8-bit 6502. Since nobody in their right mind would choose this processor for their 16-bit design over a 68000, there isn't much of a community interest, and development tools are scarce. No C compilers seem to support this CPU, and the only assembler I found is the one that comes with cc65, so that's what I'll be using for the SNES: https://cc65.github.io On the plus side, the ld65 linker seems nice and flexible enough, with a capable configuration language.
Here's the Makefile
for the first test program:
test.sfc: test.o
ld65 -o $@ -m link.map -C snes.ld $<
%.o: %.asm
ca65 -o $@ --cpu 65816 $<
.PHONY: clean
clean:
rm -f test.sfc test.o
And here's the snes.ld
config to go with it:
MEMORY {
WRAM: start = 0, size = $2000;
ROM: start = $8000, size = $8000;
}
SEGMENTS {
code: load = ROM, type = ro;
rodata: load = ROM, type = ro;
data: load = ROM, run = WRAM, type = rw, define = yes;
bss: load = WRAM, type = bss, define = yes;
carthdr: load = ROM, start = $ffc0, type = ro;
}
The SNES memory map is not straightforward, and varies from cartridge to cartridge. There are 256 banks (see DBR and PBR registers) and everything is mirrored all over the address space.
The first 8k of RAM is mapped at the first 8k of all banks, and that's all I have declared in this script, since I'm not going to need any more than 8k for the first tests.
I/O space is at 21xxh and 42xxh, with some DMA registers at 43xxh.
ROM decoding is handled by the cartridge willy-nilly. Simplest convention seems to be the "LoROM mapping", which puts 32k of ROM at the top half (8000h) of all the banks, which is what I defined in this link script.
The linker configuration language is flexible enough to define different load/run addresses and have the linker define symbols for all of them to be used by startup code if needed.
I've included a section for the cartridge header + interrupt vectors, mapped to the top of the ROM at ffc0h.
The cartridge header is defined like so in test.asm
:
; cartridge header
.segment "carthdr"
.byte "TEST " ; name needs to be 21 chars
.byte $20 ; fast ROM, LoROM mapping
.byte 0 ; ROM only
.byte 1 ; ROM banks 1 = 32k
.byte 0 ; RAM size
.byte 2 ; country europe/oceania/asia
.byte 0 ; developer id: none
.byte 0 ; ROM version
.word $ffff ; checksum complement
.word 0 ; checksum
; 65816 vectors
.word 0, 0
.word 0 ; cop
.word 0 ; brk
.word 0 ; abort
.word 0 ; NMI (vblank)
.word 0
.word 0 ; IRQ
; 6502 vectors
.word 0, 0
.word 0 ; cop
.word 0
.word 0 ; abort
.word 0 ; NMI (vblank)
.word $8000 ; reset
.word 0 ; IRQ/BRK
For the initialization, the SNES developer manual specifies a long list of
hardware registers which need to be initialized, most of them to 0. At some
point I might take a closer look at how many of those are in contigious ranges
and initialize them all in a loop, but for now I just stz
-ed them one by one.
See snes_init
in test.asm
. Then it's a good idea to zero-out all the video
ram:
; clear vmem
rep #$10 ; 16-bit index registers
.i16
setreg REG_VMAINC, $80
ldx $4000
@clear: stz REG_VMDATAL
stz REG_VMDATAH
dex
bne @clear
sep #$10 ; back to 8-bit index registers
.i8
rts
Video RAM is not mapped in the 65816 address space, and all the writes need to
go through the VMDATAL
/VMDATAH
registers. The first vmem address is set by
writing to the VMADDL
/VMADDH
registers (implicit here, since they were
zeroed first), then bit 7 of VMAINC
can be set to auto-increment the address
with every write. Video RAM is word-addressed, so 4000h writes equates to
writing 32k (8000h) worth of video RAM.
Register addresses and the setreg
macro are defined in hwregs.inc
.
For the first test I'm just clearing the screen to red, by changing the first entry of the palette, and then looping for ever:
jsr snes_init
setreg REG_BGMODE, $02 ; mode 2, 8x8 tiles
setpal 0, 31, 0, 0 ; set palette index 0 to red
fblank 0
halt: jmp halt
We can't write to Video RAM and the color palette outside of blanking periods.
snes_init
turns "forced blanking" on with the fblank 1
macro, and after
setting the palette, before going into the infinite loop I'm turning it off
again to enable the display.
Here are the relevant macros from hwregs.inc
:
.macro setreg reg, val
lda #val
sta reg
.endmacro
.macro fblank onoff
lda #($0f | (onoff << 7))
sta REG_INIDISP
.endmacro
.macro setpal idx, r, g, b
setreg REG_CGADD, idx
setreg REG_CGDATA, (r | g << 5)
setreg REG_CGDATA, (g >> 3 | b << 2)
.endmacro