CpuBus

Trait CpuBus 

Source
pub trait CpuBus {
    // Required methods
    fn read(&mut self, addr: u16) -> u8;
    fn write(&mut self, addr: u16, value: u8);
    fn on_cpu_cycle(&mut self);

    // Provided methods
    fn peek(&self, addr: u16) -> u8 { ... }
    fn read_u16(&mut self, addr: u16) -> u16 { ... }
    fn read_u16_wrap(&mut self, addr: u16) -> u16 { ... }
}
Expand description

Cycle-accurate bus interface for sub-cycle PPU/APU synchronization

This trait extends the basic Bus functionality with cycle callbacks that are invoked BEFORE each memory access. This enables accurate emulation of the NES timing where PPU advances 3 dots per CPU cycle and APU advances 1 cycle.

§Architecture

The key insight from accurate emulators (like Pinky) is that on_cpu_cycle() must be called at the START of each memory access, BEFORE the actual read/write. This ensures that when the CPU reads $2002 (PPUSTATUS), the PPU has already advanced to the correct state for that exact CPU cycle.

§Timing Model

CPU Cycle:  |-------- read --------|
PPU Cycles: |--1--|--2--|--3--|
             ^ on_cpu_cycle() called here (before read)

§Examples

§Implementing cycle-accurate bus for NES

use rustynes_cpu::CpuBus;

struct CycleAccurateBus {
    ram: [u8; 0x800],
    ppu: Ppu,
    apu: Apu,
}

impl CpuBus for CycleAccurateBus {
    fn read(&mut self, addr: u16) -> u8 {
        match addr {
            0x0000..=0x1FFF => self.ram[(addr & 0x07FF) as usize],
            0x2000..=0x3FFF => self.ppu.read_register(addr & 0x0007),
            _ => 0,
        }
    }

    fn write(&mut self, addr: u16, value: u8) {
        match addr {
            0x0000..=0x1FFF => self.ram[(addr & 0x07FF) as usize] = value,
            0x2000..=0x3FFF => self.ppu.write_register(addr & 0x0007, value),
            _ => {}
        }
    }

    fn on_cpu_cycle(&mut self) {
        // Step APU once per CPU cycle
        self.apu.step();

        // Step PPU 3 times per CPU cycle (3:1 ratio)
        for _ in 0..3 {
            self.ppu.step();
        }
    }
}

§Implementation Notes

  • The CPU’s read_cycle() and write_cycle() methods call on_cpu_cycle() before performing the actual memory access
  • This trait is essential for passing VBlank timing tests that require +/- 2 cycle accuracy when reading $2002 during VBlank transitions
  • Dummy reads for page boundary crossings also trigger on_cpu_cycle()

Required Methods§

Source

fn read(&mut self, addr: u16) -> u8

Read a byte from memory

This is the raw memory read without cycle callback. For cycle-accurate emulation, use CPU’s read_cycle() method instead which calls on_cpu_cycle() before this method.

§Arguments
  • addr - 16-bit memory address to read from
§Returns

The 8-bit value at the specified address

Source

fn write(&mut self, addr: u16, value: u8)

Write a byte to memory

This is the raw memory write without cycle callback. For cycle-accurate emulation, use CPU’s write_cycle() method instead which calls on_cpu_cycle() before this method.

§Arguments
  • addr - 16-bit memory address to write to
  • value - 8-bit value to write
Source

fn on_cpu_cycle(&mut self)

Called BEFORE each memory access to synchronize PPU/APU

This callback is the heart of cycle-accurate emulation. It must:

  1. Step the APU once (1:1 ratio with CPU)
  2. Step the PPU three times (3:1 ratio with CPU)
  3. Handle any cycle-based events (IRQ timing, etc.)
§Critical Timing

This method is called BEFORE the memory access occurs. This is essential for accurate $2002 reads during VBlank, where the PPU state must be updated to the exact cycle before the CPU observes the flags.

§Example
fn on_cpu_cycle(&mut self) {
    // Step APU
    self.apu.step();

    // Step PPU 3 times
    for _ in 0..3 {
        self.ppu.step();
    }
}

Provided Methods§

Source

fn peek(&self, addr: u16) -> u8

Read a byte without side effects (for debugging/disassembly)

Default implementation calls read(). Override for proper debugging support where hardware register reads have side effects.

§Notes
  • This should NOT call on_cpu_cycle()
  • This should NOT modify any state (e.g., don’t clear IRQ flags)
  • Used by debuggers and disassemblers
Source

fn read_u16(&mut self, addr: u16) -> u16

Read a 16-bit value in little-endian format

Reads two consecutive bytes and combines them into a 16-bit value. Note: This does NOT call on_cpu_cycle() - use CPU’s cycle-aware methods.

§Arguments
  • addr - Address of the low byte
§Returns

16-bit value: (high << 8) | low

Source

fn read_u16_wrap(&mut self, addr: u16) -> u16

Read a 16-bit value with page wrap (for JMP indirect bug)

The 6502 has a bug in JMP indirect where if the low byte is $FF, the high byte is read from $xx00 instead of $(xx+1)00. Note: This does NOT call on_cpu_cycle() - use CPU’s cycle-aware methods.

Implementors§