-
Notifications
You must be signed in to change notification settings - Fork 0
CPU 65C816
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.
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.
| 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.
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.
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.
// 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
}-
Width changes mid-instruction:
SEP/REPchange 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.
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.
RustySNES is a cycle-accurate Super Nintendo / Super Famicom emulator written in pure Rust. Licensed under MIT OR Apache-2.0. | GitHub Repository | Web Demo | API Docs