240. Ppuaddr second write toggle

Implement PPUADDR ($2006) two-write address behavior.

Lesson 240 of 356 · tests/chapter_03_ppu_memory_and_graphics_data/test_240_ppuaddr_second_write_toggle.py

Reference

https://www.nesdev.org/wiki/PPU_registers#PPUADDR

File to update

emulator/ppu/ppu.py

State to add

vram_addr: int = 0
temp_vram_addr: int = 0
second_write_toggle: bool = False

Why this step exists

PPUADDR is the CPU-visible register at $2006. The CPU uses it to set the PPU's internal VRAM address. That internal address is later used by PPUDATA ($2007) to read/write PPU memory.

Important idea

$2006 is not just a simple one-byte storage register. It is a two-write port.

The CPU writes the address in two parts

first write  -> high byte
second write -> low byte

Example

write $20 to $2006
write $00 to $2006

Result after both writes

vram_addr == $2000

Important internal-register model

The first $2006 write updates temp_vram_addr, not vram_addr. The second $2006 write completes temp_vram_addr and copies it into vram_addr.

Why second_write_toggle: The PPU must remember whether the next $2006 write is the first or second write. In this tutorial, second_write_toggle means:

False -> next $2006 write is the first write / high byte
True  -> next $2006 write is the second write / low byte

Why the high byte is masked with 0x3F

The PPU address space is 14-bit

$0000-$3FFF

Only the lower 6 bits of the high byte are useful for this address range. So the first write should use:

(value & 0x3F) << 8

Suggested implementation pseudocode

@dataclass
class PPU:
    ...
    addr: int = 0
    vram_addr: int = 0
    temp_vram_addr: int = 0
    second_write_toggle: bool = False

    def write_register(self, addr: int, value: int) -> None:
        value = value & 0xFF

        match addr:
            ...
            case 0x2006:
                # Keep old simple register-field behavior for test compatibility.
                self.addr = value

                if not self.second_write_toggle:
                    self.temp_vram_addr = (
                        (self.temp_vram_addr & 0x00FF)
                        | ((value & 0x3F) << 8)
                    )
                    self.second_write_toggle = True
                else:
                    self.temp_vram_addr = (
                        (self.temp_vram_addr & 0x3F00)
                        | value
                    )
                    self.vram_addr = self.temp_vram_addr
                    self.second_write_toggle = False

            ...

Out of scope

  • PPUDATA ($2007) writing through PpuBus
  • PPUSTATUS ($2002) resetting the write toggle
  • PPUSCROLL ($2005) using this same toggle
  • increment-by-32 behavior from PPUCTRL

Run this lesson

uv run pytest tests/chapter_03_ppu_memory_and_graphics_data/test_240_ppuaddr_second_write_toggle.py -v