Skip to main content

rustysnes_cpu/
bus.rs

1//! The CPU-side bus trait — the SNES port of RustyNES's `rustynes-cpu::Bus`.
2//!
3//! The 65C816 has a 24-bit address space (`(bank << 16) | addr`). It borrows `&mut impl Bus`
4//! for the duration of an instruction; the bus fans the access out to WRAM, the PPU/APU
5//! register windows, the controllers, the DMA/HDMA registers, and the cartridge board. The
6//! concrete impl is in `rustysnes-core`; a tiny [`NullBus`] here lets the CPU be unit-tested
7//! in isolation (the one-directional graph is what makes that possible).
8
9/// Address-space bus seen by the 65C816.
10///
11/// Timing follows ares `sfc/cpu/memory.cpp`: the CPU asks the bus how many master clocks an
12/// access costs ([`Bus::access_cycles`], ares `wait`), then interleaves the clock advance
13/// ([`Bus::advance`], ares `step`) around the access so it lands at the hardware-exact instant —
14/// a write at the END of its cycle (advance the full cost, then write), a read four clocks before
15/// the end (advance cost−4, read, advance 4). This phase matters: it decides the exact hcounter
16/// at which a register write becomes visible to the PPU/HDMA (e.g. the HDMAEN mid-scanline latch).
17pub trait Bus {
18    /// Read a byte at a 24-bit address (no clock advance — the CPU sequences timing via
19    /// [`Bus::advance`] around this call).
20    fn read24(&mut self, addr24: u32) -> u8;
21
22    /// Write a byte at a 24-bit address (no clock advance — see [`Bus::read24`]).
23    fn write24(&mut self, addr24: u32, val: u8);
24
25    /// Master clocks this access costs (ares `CPU::wait`): the region-variable access speed
26    /// (FastROM vs SlowROM, WRAM, I/O). Defaults to the SlowROM/internal-cycle cost of 6.
27    fn access_cycles(&self, _addr24: u32) -> u32 {
28        6
29    }
30
31    /// Advance the system clock by `clocks` master ticks (ares `CPU::step`), ticking the
32    /// PPU/APU/coprocessor and HDMA in lockstep. Default no-op for buses whose timebase is
33    /// charged elsewhere (the SA-1 second CPU) or that don't model timing (unit-test buses).
34    fn advance(&mut self, _clocks: u32) {}
35
36    /// Edge-triggered NMI poll (PPU vblank → CPU). Returns `true` once per high→low edge.
37    fn poll_nmi(&mut self) -> bool {
38        false
39    }
40
41    /// Level-sensitive IRQ poll (PPU HV-IRQ, on-cart coprocessor, APU timer). Honored only
42    /// when the CPU's I flag is clear.
43    fn poll_irq(&mut self) -> bool {
44        false
45    }
46}
47
48/// A no-op [`Bus`] (all reads open, writes dropped) for unit-testing the CPU in isolation.
49#[derive(Debug, Default)]
50pub struct NullBus;
51
52impl Bus for NullBus {
53    fn read24(&mut self, _addr24: u32) -> u8 {
54        0
55    }
56    fn write24(&mut self, _addr24: u32, _val: u8) {}
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn null_bus_defaults() {
65        let mut bus = NullBus;
66        assert_eq!(bus.read24(0x00_8000), 0);
67        bus.write24(0x00_8000, 0xFF);
68        assert!(!bus.poll_nmi());
69        assert!(!bus.poll_irq());
70        assert_eq!(bus.access_cycles(0x00_8000), 6);
71        bus.advance(6);
72    }
73}