221. Cpu bus cartridge integration part1

Add optional Cartridge support to CpuBus, part 1.

Lesson 221 of 356 · tests/chapter_02_rom_loading/test_221_cpu_bus_cartridge_integration_part1.py

Prerequisites

  • Lesson 217 provides Cartridge.
  • Lessons 218-219 provide Mapper000.
  • Lesson 220 provides create_mapper.

File to update

emulator/bus/cpu_bus.py

Symbols to update

emulator.bus.cpu_bus.CpuBus.cartridge
emulator.bus.cpu_bus.CpuBus.__post_init__

Required imports in that file

from emulator.cartridge.cartridge import Cartridge
from emulator.cartridge.mapper_factory import create_mapper

What this part implements

  • CpuBus accepts an optional cartridge
  • CpuBus creates a mapper from that cartridge in __post_init__
  • CpuBus rejects attaching both program_rom and cartridge

What this part does NOT implement yet

  • CPU reads from cartridge PRG ROM

That read behavior is tested in part 2.

Why this step exists

Adding cartridge support has two separate responsibilities

1. construction-time wiring
2. read-time routing

Keeping them separate makes the tutorial easier to follow. Students first learn how the bus receives a cartridge and turns it into a mapper. Only after that do they change the read path for $8000-$FFFF.

Architecture model

Cartridge
    stores PRG ROM, CHR ROM, and mapper number

create_mapper(cartridge)
    chooses the correct mapper implementation

Mapper000
    translates CPU PRG addresses into PRG ROM offsets

CpuBus
    owns routing decisions

Important boundary

CpuBus should not implement Mapper000 mirroring rules directly. It should create or hold a mapper, then later delegate PRG reads to mapper.read_prg(addr).

Expected implementation shape

@dataclass
class CpuBus:
    program_rom: Optional[MemoryDevice] = None
    cartridge: Optional[Cartridge] = None
    ram: RAM = field(default_factory=RAM)

    def __post_init__(self):
        if self.program_rom is not None and self.cartridge is not None:
            raise ValueError("Cannot attach both program_rom and cartridge")

        self.mapper = None

        if self.cartridge is not None:
            self.mapper = create_mapper(self.cartridge)

Invariants

- program_rom remains the writable MemoryDevice seam used by CPU tests
- cartridge and program_rom are mutually exclusive PRG sources
- mapper is always initialized, to None without a cartridge and to the
  factory result with one
- Cartridge PRG and CHR bytes reach the mapper unchanged through
  create_mapper; CpuBus neither parses nor remaps those bytes

Common misconception

The cartridge is not itself a MemoryDevice replacement for program_rom. It is metadata plus ROM payloads; create_mapper(cartridge) supplies the address-aware object that the bus will use.

Out of scope for this step

1. Lesson 222 changes the $8000-$FFFF read path.
2. PPU register routing and PPU construction belong to Chapter 3.
3. Mapper writes, CHR/PPU bus routing, and later behavior are not added here.

Run this lesson

uv run pytest tests/chapter_02_rom_loading/test_221_cpu_bus_cartridge_integration_part1.py -v