This tutorial covers the SA-1 coprocessor — a second 65816 CPU clocked at 10.74 MHz inside the cartridge, running in parallel with the main CPU.
The SA-1 is a 65c816 clone clocked at 10.74 MHz (vs 3.58 MHz for the main CPU). It shares the same instruction set, so you already know how to program it — in assembly: the SDK does not run C on it, because the C runtime lives in work RAM, which the SA-1 cannot see (table below).
How fast it really runs depends on memory, not on the clock. The SA-1 Speed Test of the higan test ROMs was photographed running on a real console (one console, one board: 1L8B-10). Read off those photographs:
| Main CPU runs from | SA-1 runs from | SA-1 effective speed |
|---|---|---|
| WRAM | I-RAM | 10.74 MHz |
| I-RAM | I-RAM | 3.72 MHz |
| WRAM | ROM | 10.07 MHz (7.67 across a JMP) |
| ROM | ROM | 5.04 MHz — the two CPUs share the ROM bus |
| BW-RAM | BW-RAM | 2.69 MHz |
The summary table in that test's README gives about 5.4 MHz for ROM / ROM and is the figure most often repeated; the photographs of the same run say 5.04, and the measurement is what this page follows. No reference manual states these speeds: they are measurements, on one machine.
So "3× the main CPU" holds only for SA-1 code that keeps off the ROM while the main CPU is on it. The table gives the rate while both CPUs are in a case; a real program moves between cases, so its effective speed is a mix, and a property of the scene rather than of the cartridge. Measure it. examples/chips/sa1_starfield builds both ways: by default its SA-1 copies its loop and sine table into I-RAM at boot and runs there; built with make clean && make SA1_CODE_IN=ROM they stay in ROM.
From ROM, 86 % of the SA-1's accesses are ROM, but the penalty only applies when the main CPU is on ROM at the same moment, so it loses a fifth of its clocks, not half: about 8.5 MHz, 2.4× the main CPU. Code in ROM also pays one more cycle on every jump, call and return into ROM, so a loop that jumps often runs slower than its conflict share alone says (luna v1.30.0, from ares, checked against a console on the SNES-SA1 Speed Test). From I-RAM it keeps almost all of them: 10.7 MHz, 3×. The remaining 0.4 % is the main CPU touching I-RAM too (it reads the positions there and polls the sync flag). Both builds draw the same frames; the I-RAM one simply finishes each frame's work sooner (luna v1.30.0, whose SA-1 timing matches a console on the SNES-SA1 Speed Test; the instruction count includes the SA-1's wait for the next frame, so it grows with the spare time).
The I-RAM version costs 289 bytes of code and 256 of table out of the 2 KB, and a loop written to run anywhere: brl instead of jmp, and the table read at its I-RAM address (sa1_boot.asm). Games like Kirby Super Star, Super Mario RPG, and Kirby's Dream Land 3 used it to handle AI, physics, and decompression that the main CPU couldn't keep up with.
| Feature | Main CPU | SA-1 |
|---|---|---|
| Clock | 3.58 MHz | 10.74 MHz (effective speed: see above) |
| Instruction set | 65c816 | 65c816 (identical) |
| WRAM access | Yes (128 KB) | No |
| PPU access | Yes | No |
| APU access | Yes | No |
| I-RAM access | Yes ($3000-$37FF) | Yes ($0000-$07FF, mirrors $3000) |
| ROM access | Yes | Yes (separate bus, no conflicts) |
| BW-RAM access | Yes (via $6000 window) | Yes (256 KB max) |
The key constraint: SA-1 cannot touch WRAM, PPU, or APU. It can only read ROM and read/write I-RAM and BW-RAM. All communication with the main CPU happens through the 2 KB shared I-RAM.
I-RAM is 2 KB of fast SRAM at $3000-$37FF (both CPUs see this address). The SA-1 also sees it at $0000-$07FF (its direct page region).
This is how the two CPUs communicate: the main CPU writes data to I-RAM, the SA-1 reads it, computes results, writes them back, and signals "done."
Write protection (resolved polarity): Both CPUs have independent write-protection registers (SIWP at $2229, CIWP at $222A). Each bit is a write-enable flag for one 256-byte I-RAM page — bit=1 enables writing, $FF = fully writable. This is what fullsnes, the official Nintendo development manual (§4.1.25) and nocash's hardware debugging all state, and how Mesen2, snes9x and luna behave. The Super Famicom Dev Wiki page documents the opposite polarity for $2229/$222A — that page is wrong (and inconsistent with its own $2226/$2227 entries). OpenSNES writes $FF; the crt0 I-RAM self-test catches any regression at runtime (sa1IsReady() returns false). Details in KNOWN_LIMITATIONS.md.
USE_SA1 := 1 does three things:
Create sa1_boot.asm in your example directory (not in templates/). This code runs on the SA-1:
Why sta.l everywhere? The SA-1's data bank register (DB) is undefined at boot. Using sta.l (long absolute, opcode $8F) bypasses DB entirely. This is the safest pattern for SA-1 code.
sa1IsReady() returns 1 if the SA-1 wrote the $A5 magic byte to I-RAM $3000 within the timeout. The boot sequence (in crt0.asm) handles all the register setup: reset vector, I-RAM write protection, and SA-1 release.
The SA-1 computes data, sets a flag. The main CPU reads the data, clears the flag. Used by the sa1_starfield example.
SA-1 side (assembly):
Main CPU side (C):
The SA-1 increments a value continuously. The main CPU reads it whenever needed. No synchronization required — just atomic reads. See sa1_hello for register readback examples.
We recommend this layout for SA-1 projects:
| Address | Size | Purpose |
|---|---|---|
| $3000 | 1 | Magic/status byte ($A5 = ready) |
| $3001 | 1 | Sync flag (0 = idle, 1 = data ready) |
| $3002-$300F | 14 | Control variables (counters, params) |
| $3010-$37FF | 2032 | Data buffer (up to 508 entries × 4 bytes) |
Both CPUs must agree on this layout. Define constants in both your C code and assembly:
The SA-1's DB register is undefined at boot. Don't use sta addr (absolute) — use sta.l addr (long absolute) to specify the full 24-bit address.
To read data tables in ROM from the SA-1, use lda.l table,x (opcode $BF). This is a long indexed read — no DB dependency:
The 65816 has no stx.l instruction. To save/restore X via I-RAM:
WLA-DX loses accumulator width tracking after branch merges. Always add explicit .ACCU 8/.ACCU 16 directives:
luna, the SDK's emulator, runs the SA-1 natively:
A GUI emulator with an SA-1 debugger (Mesen2, bsnes-plus) remains useful for stepping the SA-1 side by hand.
Common issues:
| Example | What it demonstrates |
|---|---|
| sa1_hello | Boot diagnostic — verifies SA-1 initialization |
| sa1_starfield | 128 sprites with sine-wave Lissajous patterns |
The SA-1 is a compute accelerator. The main CPU remains the "director" — it reads input, talks to the PPU/APU, and delegates heavy math to the SA-1.