263. Cpu bus prg write routing

Route CPU PRG-space writes through the mapper before CPU NMI tests.

Lesson 263 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_263_cpu_bus_prg_write_routing.py

Files to update

emulator/cartridge/mapper_interface.py
emulator/cartridge/mapper000.py
emulator/bus/cpu_bus.py

Why this step exists

The next CPU interrupt test will need to place the NMI vector bytes at

$FFFA = NMI vector low byte
$FFFB = NMI vector high byte

Those addresses live inside the CPU PRG area

$8000-$FFFF

Before testing CPU.interrupt_nmi(), we need the CPU bus write behavior in that range to be explicit and safe.

What is PRG write routing? PRG write routing means CpuBus does not mutate cartridge ROM bytes directly. Instead, writes in $8000-$FFFF are forwarded to the cartridge mapper:

CpuBus.write($8000-$FFFF, value)
    -> mapper.write_prg(addr, value)

Minimal example

CPU writes $8000 = $12
CpuBus calls mapper.write_prg($8000, $12)
Mapper000/NROM ignores it because it has no PRG write registers
Later mappers may treat the same write as a bank-switch command

Common misconception

Writing to $8000 does not mean the CPU changes ROM bytes. On real cartridges, writes in this range are usually ignored by simple boards or interpreted as mapper control signals by more advanced boards.

Important split

Real cartridge path:
    CpuBus.write($8000-$FFFF) -> mapper.write_prg(...)

Tutorial FakeROM path:
    CpuBus.write($8000-$FFFF) -> FakeROM.write(addr - $8000, value)

The FakeROM path is only a test setup convenience. It lets upcoming interrupt tests install vector bytes at $FFFA/$FFFB without making real Mapper000 PRG ROM writable.

Suggested implementation examples

class MapperInterface(Protocol):
    def read_prg(self, addr: int) -> int:
        ...

    def write_prg(self, addr: int, value: int) -> None:
        ...

    def read_chr(self, addr: int) -> int:
        ...

    def write_chr(self, addr: int, value: int) -> None:
        ...

class Mapper000:
    def write_prg(self, addr: int, value: int) -> None:
        if not (PRG_ROM_START <= addr <= PRG_ROM_END):
            raise ValueError(f"Address out of PRG ROM range: {addr:04X}")

        # Mapper000/NROM has no writable PRG registers.
        # Real hardware ignores writes to PRG ROM space, so we ignore them too.
        return

class CpuBus:
    def write(self, addr: int, value: int) -> None:
        ...

        if 0x8000 <= addr <= 0xFFFF:
            if self.mapper is not None:
                self.mapper.write_prg(addr, value)
                return

            if self.program_rom is not None:
                self.program_rom.write(addr - 0x8000, value)
                return

        raise ValueError(f"Unsupported CPU bus write: {addr:04X}")

Out of scope

  • CPU.interrupt_nmi() behavior
  • PPU requesting NMI consumption by the CPU
  • Mapper001/MMC bank switching
  • Making real PRG ROM writable

Run this lesson

uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_263_cpu_bus_prg_write_routing.py -v