65816 assembly and SNES PPU reference for the sd2snes menu ROM. Auto-loads when editing .a65 assembly files, working with SNES graphics, palettes, HDMA, or the menu ROM codebase.
| Syntax | Meaning |
|---|---|
@label or @$addr | Long (24-bit) address |
!label or !$addr | Absolute (16-bit) address |
#^label | Bank byte of label address |
#!label | 16-bit address of label |
.as / .al | 8-bit / 16-bit accumulator mode hint |
.xs / .xl | 8-bit / 16-bit index register mode hint |
sep #$20 : .as | Set 8-bit A (instruction + mode hint, : chains) |
.byt | Define byte(s) |
.word | Define 16-bit word(s) |
#define NAME value | Preprocessor constant (cpp) |
.link page $C0 | Set output address/bank |
- bra - | Branch to previous anonymous label |
- / + | Anonymous labels (backward / forward) |
| Syntax | Meaning |
|---|---|
sta $7E0027 | Long addressing auto-selected (addr > $FFFF) |
sta $2100 | Absolute addressing auto-selected (addr < $10000) |
label >> 16 | Bank byte of label (replaces #^label) |
label & $ffff | 16-bit address of label (replaces #!label) |
.as / .al / .xs / .xl | Same as snescom |
sep #$20 then .as on next line | No : chaining — separate lines |
.byte | Define byte(s) (NOT .byt) |
.word | Define 16-bit word(s) (same) |
.text "string" | Define ASCII text (NOT .byt "text") |
NAME = $value | Constant assignment (replaces #define) |
* = $C00000 | Set program counter (replaces .link page) |
.include "file" | Include source file (no separate linker) |
.fill count, value | Fill bytes |
.cpu "65816" | Set CPU type (required at top) |
64tass enforces that absolute addresses match the current data bank register. This catches real hardware bugs (e.g., accessing PPU registers with wrong DBR).
| Directive | When to use |
|---|---|
.databank 0 | After RESET, or when DBR=$00 (PPU/CPU register access) |
.databank $7e | After lda #$7e / pha / plb (WRAM variable access) |
.databank ? | NMI/IRQ handlers where DBR is unknown |
Key rules:
sta $2100 writes to WRAM $7E2100, NOT the PPU register!stz has no long addressing mode — must set DBR correctly or use direct pagesta $7E0027 (opcode $8F).word LABEL & $ffff (labels are 24-bit in 64tass)64tass tracks .as/.al/.xs/.xl to determine immediate operand sizes. It does NOT propagate this state across jsr/rts or jml boundaries.
Every function must declare its register sizes at entry:
my_function
sep #$20 ; also sets CPU state (belt + suspenders)
.as
rep #$10
.xl
Why this matters: Without .xl, ldy #0 assembles as 2 bytes ($A0 $00) instead of 3 ($A0 $00 $00). If the CPU is actually in 16-bit index mode, it reads an extra byte, misaligning ALL subsequent instructions. This causes silent garbage execution — extremely hard to debug.
Also required: .dpage 0 — Declare once at top of main.a65 for correct indirect long [dp],y operand generation. Without it, the assembler generates wrong DP offsets.
Verification: hexdump the binary at the function's address (from --labels output). Check that LDY #imm ($A0) has the right operand width.
; snescom: DMA7 #$01, src, $2118, #size
; 64tass:
.macro DMA7 mode, src, dest, size
lda #\mode
sta $4370
lda #\dest
sta $4371
ldx #\src & $ffff
stx $4372
lda #\src >> 16
sta $4374
ldx #\size
stx $4375
lda #$80
sta $420b
.endm
sep #$20 = set 8-bit accumulator (M flag)sep #$10 = set 8-bit index (X flag)rep #$20 = set 16-bit accumulatorrep #$10 = set 16-bit indexrep #$30 = set 16-bit A and X/Ysep #$30 = set 8-bit A and X/Yphp / plp = push/pull processor status (CRITICAL for mode preservation)phb / plb = push/pull data bank registerphk / plb = set data bank to program bankmvn src,dst = block move next (decrementing)xba = exchange B and A (swap high/low bytes of 16-bit accumulator)tax / tay = transfer accumulator to index — DANGER: with M=1/X=0, transfers full 16-bit C including hidden B byte. See Mixed-Mode Transfers.txa / tya = transfer index to accumulator — with M=1/X=0, sets hidden B to index high bytejsl / rtl = long subroutine call/return (24-bit)jsr / rts = short subroutine call/return (16-bit, same bank)Each color is 2 bytes, little-endian: low_byte, high_byte
Bit layout: 0bbbbbgg_gggrrrrr (15-bit, bit 15 unused)
| Example | Bytes | Color |
|---|---|---|
| Black | $00, $00 | R=0, G=0, B=0 |
| White | $ff, $7f | R=31, G=31, B=31 |
| Bright red | $1f, $00 | R=31, G=0, B=0 |
| Bright green | $e0, $03 | R=0, G=31, B=0 |
| Bright blue | $00, $7c | R=0, G=0, B=31 |
| Dark blue | $00, $40 | R=0, G=0, B=16 |
Conversion: low = (green_low3 << 5) | red5, high = (blue5 << 2) | green_high2
[bit7=B][bit6=G][bit5=R][bits4-0=intensity]
$3f = R=31, $5f = G=31, $9f = B=31$e0 = all planes intensity 0, $ff = all planes intensity 31; snescom syntax:
lda #<row> ; tile row (9+ = below logo)
sta print_y
lda #<col> ; tile column (0 = left edge)
sta print_x
lda #^string_label ; bank byte of string
sta print_bank
ldx #!string_label ; 16-bit address of string
stx print_src
stz print_pal ; palette 0=normal, 1=bold
lda #<max_chars>
sta print_count
jsr hiprint
Strings: null-terminated ASCII. Chars 0 and 1 = end of string.
After hiprint, wait for NMI DMA: - lda isr_done / lsr / bcc - / stz isr_done
Fires every VBlank (~60Hz). DMAs BG1/BG2 tile buffers WRAM->VRAM, updates cursor bar, fades brightness (cur_bright toward tgt_bright), sets isr_done=1.
CRITICAL: Write to WRAM buffers via hiprint, NOT directly to VRAM. NMI overwrites VRAM every frame.
php/plp to save/restore processor statestore_wram_routines| Address | Name | Purpose |
|---|---|---|
$002A00 | MCU_CMD | Command register (SNES->MCU) |
$002A02 | SNES_CMD | Status register (MCU->SNES) |
$002A04 | MCU_PARAM | Parameter block (8 bytes, bidirectional) |
$002A10 | BRAM_ROUTINE | NMI hook code (installed by MCU at game launch) |
$7EA000 | BG2_TILE_BUF | BG2 tile buffer (NMI DMAs to VRAM) |
$7EB000 | BG1_TILE_BUF | BG1 tile buffer (NMI DMAs to VRAM) |
$7EF000 | WRAM_ROUTINE | FPGA reconfig routine |
$7EF200 | WRAM_WAIT_MCU | MCU wait routine |
$FF019D | CFG_BRIGHTNESS_LIMIT | Screen brightness config |
$FF1110 | ST_SNES_ADDR | SNES status flags (Ultra16, Satellaview) |
The 64tass SDK includes snes-64tass/registers.i65 with named constants for every SNES register. Use these instead of magic numbers:
lda #OBSEL_SIZE_8x8_16x16 | OBSEL_BASE_6000 ; instead of lda #$03
sta OBSEL ; instead of sta $2101
lda #TM_OBJ | TM_BG1 | TM_BG2 ; instead of lda #$13
sta TM ; instead of sta $212c
lda #VMAIN_WORD_ACCESS ; instead of lda #$80
sta VMAIN ; instead of sta $2115
Covers: PPU ($2100-$213F), WRAM port ($2180-$2183), old-style joypad ($4016-$4017), CPU control ($4200-$421F), DMA channels ($4300-$437A), OAM attributes, processor flags.