SNES hacking setup

John Tsiombikas (Nuclear / Mindlapse)
7 July 2020

test1 red screen

Source archive

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

References


Back to SNES hacking notes index