223. Validation cpu executes tiny ines rom

VALIDATION TEST: CPU executes a tiny generated iNES ROM.

Lesson 223 of 356 · tests/chapter_02_rom_loading/test_223_VALIDATION_cpu_executes_tiny_ines_rom.py

This is not a student implementation step.

Prerequisites under validation

- this file: make_tiny_ines_rom and
  test_cpu_executes_tiny_generated_ines_rom_from_reset_vector
- emulator/cartridge/ines.py: PRG_ROM_BANK_SIZE, CHR_ROM_BANK_SIZE,
  parse_ines_header, and parse_ines_rom
- emulator/cartridge/cartridge.py: Cartridge.from_ines_bytes
- emulator/cartridge/mapper_factory.py: create_mapper
- emulator/cartridge/mapper000.py: Mapper000.read_prg
- emulator/bus/cpu_bus.py: CpuBus.__post_init__ and CpuBus.read
- emulator/cpu/cpu.py: CPU.reset, CPU.fetch_byte, and CPU.step
- emulator/cpu/opcodes.py: OPCODE_TABLE entry $A9
- emulator/cpu/instructions.py: lda

Validation sequence

1. make_tiny_ines_rom builds a 16-byte iNES header declaring mapper 0, one
   16KB PRG bank, one 8KB CHR bank, and no trainer.
2. It writes A9 42 (LDA #$42) and EA (NOP) at PRG offsets $0000-$0002,
   corresponding to CPU $8000-$8002.
3. It writes reset-vector bytes 00 80 at PRG offsets $3FFC-$3FFD. Mapper000's
   16KB mirror makes CPU reads $FFFC-$FFFD observe those bytes.
4. Cartridge.from_ines_bytes parses and slices the generated bytes.
5. CpuBus(cartridge=cartridge) creates Mapper000 through create_mapper.
6. CPU.reset reads the vector through CpuBus and Mapper000, producing
   PC=$8000.
7. One CPU.step fetches $A9 through the same path, dispatches lda, fetches
   operand $42, and leaves A=$42 and PC=$8002. The trailing NOP is fixture
   data and is not executed by this test.

Run exactly this validation with

pytest -q tests/chapter_02_rom_loading/test_223_VALIDATION_cpu_executes_tiny_ines_rom.py

The full boundary under validation is

iNES bytes
    -> Cartridge.from_ines_bytes(data)
    -> CpuBus(cartridge=cartridge)
    -> create_mapper(cartridge)
    -> Mapper000
    -> CPU.reset()
    -> CPU.step()

Why this step exists

Validation exists because unit tests can prove each part in isolation while bugs still appear at component boundaries. This test adds no production implementation; it proves that lessons 212-222 work together so the CPU can fetch and execute an instruction from cartridge-backed PRG ROM using the reset vector.

The ROM is generated inside the test. It is intentionally tiny and does not use PPU, APU, controllers, interrupts, or any feature outside the current emulator scope.

Tiny program placed at CPU address $8000:

A9 42    LDA #$42
EA       NOP

Reset vector

CPU $FFFC = $00
CPU $FFFD = $80

So after reset

PC = $8000

Important Mapper000 detail

For a 16KB NROM cartridge, CPU $FFFC-$FFFD maps to PRG ROM offsets $3FFC-$3FFD because $C000-$FFFF mirrors the 16KB PRG bank.

Invariants

The iNES payload is complete and in-memory; mapper number and bank sizes survive parsing; reset-vector reads and opcode/operand fetches all use cartridge-backed PRG routing; reset selects $8000; and one instruction consumes exactly two bytes. A common misconception is to place the vector at offset $7FFC as with a 32KB/FakeROM layout, or to claim the NOP was validated after only one step.

Out of scope

Do not add production code for this validation. PPU construction/register routing (the following Chapter 3 transition), APU, controllers, interrupts, timing, PRG writes, CHR/PPU reads, external ROM files, and later console/PPU integration are intentionally not exercised or implied here.

Run this lesson

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