Skip to content

CPU 65C816

DoubleGate edited this page Jul 8, 2026 · 1 revision

CPU — WDC 65C816 (Ricoh 5A22)

References: docs/cpu.md, docs/scheduler.md, docs/adr/0001. Sources cited inline in the underlying spec: Super Famicom Dev wiki 65816 reference, undisbeliever 65816 opcodes, SNESdev S-CPU.

Purpose

The SNES's main CPU is a WDC 65C816 16-bit core, wrapped by Nintendo in the Ricoh 5A22 package. The 5A22 wrapper adds the multiply/divide registers, the DMA/HDMA hardware, the NMI/IRQ timers, and joypad auto-read — those live in rustysnes-core (the Bus), not the CPU crate. crates/rustysnes-cpu implements only the 65C816 instruction core itself; it has no PPU/APU/cart dependency, matching the project's one-directional crate graph.

Registers and state

Register Width Role
A 8/16 accumulator (width = M flag)
X, Y 8/16 index registers (width = X flag)
S 16 stack pointer
D 16 direct-page register
DBR 8 data bank register
PBR (PB/K) 8 program bank register
PC 16 program counter
P 8 status: N V M X D I Z C (plus the hidden E flag)

The CPU addresses a 24-bit (16 MiB) space via the bank registers — PBR for code fetches, DBR for data accesses.

Emulation vs native mode

The CPU powers on in 6502 emulation mode, behaving like a 65C02 with NMOS cycle counts; A and the index registers are locked to 8 bits. Code executes CLC : XCE to enter native mode, where the M (accumulator/memory width) and X (index width) status bits independently select 8- or 16-bit registers via REP/SEP. The E flag is exchanged with the carry flag by XCE. A hardware RESET always forces emulation mode, and emulation-mode and native-mode use separate vector tables at the top of bank 0.

Timing is not a fixed divisor

Unlike a simple fixed-cycle core, the 65C816 in the SNES is not driven by a fixed master-clock divisor. Every memory access costs 6, 8, or 12 master clocks depending on the region being accessed (the access-speed map in docs/scheduler.md); internal (I/O-only) cycles always cost 6. On top of that per-access cost, instruction cycle counts themselves vary:

  • +1 cycle if M = 0 (a 16-bit memory/accumulator access).
  • +1 cycle if the low byte of D is non-zero (direct-page misalignment).
  • +1 cycle if an indexed access crosses a page boundary.

A single opcode's total master-clock cost is therefore the sum of each access's region-dependent speed plus the internal-cycle cost — which depends on the current M/X widths and the addressing mode in use, not a static table.

RustySNES also models the sub-cycle access phase: within one CPU cycle, the bus access is not simultaneous with the clock advance. A write lands at the end of its cycle; a read lands four clocks before the cycle end; an internal cycle is a flat six-clock advance with no access. This fixes the exact point at which a register write becomes visible to the PPU/HDMA — load-bearing for hardware-exact raster timing, not just aggregate cycle counts.

Interfaces (sketch)

// rustysnes-cpu
pub trait CpuBus {
    /// Returns the value; the impl advances the master clock by the
    /// region's access speed (6/8/12) and ticks PPU/HDMA/timers.
    fn read(&mut self, addr: u32) -> u8;
    fn write(&mut self, addr: u32, value: u8);
    /// An internal (no-bus) cycle: always 6 master clocks.
    fn io(&mut self);
}

pub struct Cpu { /* A, X, Y, S, D, DBR, PBR, PC, P, E */ }
impl Cpu {
    pub fn step(&mut self, bus: &mut impl CpuBus); // one instruction
}

Edge cases the core models

  • Width changes mid-instruction: SEP/REP change M/X, which changes the byte count of subsequent accesses; the cost calculation reads the width after the flag write.
  • Emulation-mode stack wrapping: in E-mode, S is fixed to page 1 ($01xx); native mode uses the full 16-bit S.
  • Direct-page misalignment keys off D & 0xFF != 0, not the access address.
  • Page-cross penalty applies to indexed addressing modes only, and only when the effective address crosses a 256-byte boundary.
  • WRAM-refresh stalls and DMA halts (MDMAEN) freeze the CPU mid-instruction — the core is steppable at access granularity, not whole-instruction granularity, to model this correctly.

Validation

The primary oracle is SingleStepTests/65816 (per-opcode, all addressing modes, 8/16-bit, native + emulation, with a cycle-by-cycle bus-pin trace). Per docs/cpu.md's current implementation status, the full oracle run (512 opcode files × 10,000 tests = 5,120,000 tests) reaches 100.00% (5,119,999 / 5,120,000) full passes (state + RAM + cycle count). The single residual is one SBC (dp,X) emulation-mode case exercising a documented inter-reference divergence between the SingleStepTests fixture set and the bsnes reference core's direct-page-wrap behavior — deliberately not point-fixed, since matching it would regress the other 9,999 tests in that same file (docs/adr/0002's posture on documented residuals).

The gilyon/snes-tests committable layer additionally covers 65C816 opcodes across all addressing modes, emulation and native, with golden result tables.

See Testing-Strategy for how this fits the project's overall accuracy battery, and Lockstep-Scheduler for how the CPU's variable-cycle timing integrates with the PPU/APU on the shared master clock.

Clone this wiki locally