222. Cpu bus cartridge integration part2
Read cartridge PRG ROM through CpuBus, part 2.
Lesson 222 of 356 · tests/chapter_02_rom_loading/test_222_cpu_bus_cartridge_integration_part2.py
Prerequisite
Lesson 221's cartridge field, __post_init__, and mapper factory wiring must be complete before changing the read path here.
File to update
emulator/bus/cpu_bus.pySymbol to update
emulator.bus.cpu_bus.CpuBus.readWhat this part implements
- CpuBus routes CPU reads in $8000-$FFFF to mapper.read_prg(addr) when a
cartridge-backed mapper exists
- CpuBus keeps the old program_rom=FakeROM path working
- CpuBus fails loudly if no PRG source is attachedWhy this step exists
The CPU sees cartridge program ROM at CPU addresses
$8000-$FFFFBut cartridge PRG ROM bytes are stored from offset 0. The mapper owns that translation. CpuBus should only decide that addresses in $8000-$FFFF belong to the cartridge PRG area, then delegate to the mapper.
Correct cartridge path
CpuBus.read($8000)
-> mapper.read_prg($8000)
-> Mapper000 translates to PRG offset $0000Important difference from the old FakeROM path
program_rom.read(addr - $8000)uses an offset because FakeROM is a simple testing device, not a mapper.
mapper.read_prg(addr)uses the full CPU address because mappers implement CPU-address translation.
Common mistake
Do not call mapper.read_prg(addr - 0x8000). That would pass an offset to code that expects a CPU address in $8000-$FFFF.
Suggested implementation
def read(self, addr: int) -> int:
# Read from CPU Bus.
if 0x0 <= addr <= 0x1FFF:
return self.ram.read(addr & 0x07FF)
if 0x8000 <= addr <= 0xFFFF:
if self.mapper is not None:
return self.mapper.read_prg(addr)
if self.program_rom is not None:
return self.program_rom.read(addr - 0x8000)
raise ValueError("No program ROM or cartridge attached")
raise ValueError(f"Unsupported CPU bus read: {addr:04X}")Rationale and invariants
CpuBus owns address-range routing while Mapper000.read_prg owns NROM address translation. Internal RAM mirroring remains unchanged. Cartridge reads receive the full CPU address; the legacy MemoryDevice receives a zero-based offset; and a configured source is required throughout the inclusive $8000-$FFFF range. The mapper path takes priority only because part 1 forbids both sources from being configured, so there is never an ambiguous valid construction.
Another common misconception is to copy Mapper000's 16KB modulo rule into CpuBus. The $C000 mirror assertion is evidence that delegation works, not a new bus responsibility.
Out of scope
1. PPU register reads at $2000-$3FFF belong to Chapter 3.
2. PRG and mapper writes are not added here.
3. PPU/CHR routing, timing, and later behavior must not be anticipated.Run this lesson
uv run pytest tests/chapter_02_rom_loading/test_222_cpu_bus_cartridge_integration_part2.py -v