291. Controller capture and serial read

Capture controller buttons and read them serially.

Lesson 291 of 356 · tests/chapter_07_controller_input/test_291_controller_capture_and_serial_read.py

Files created in this step

emulator/input/controller.py

Why this step exists

The NES CPU does not receive the controller state as a whole byte from $4016. It receives one button bit at a time. Before wiring that protocol into CpuBus, the pure Controller object should know how to:

References

https://www.nesdev.org/wiki/Standard_controller
https://www.nesdev.org/wiki/Controller_reading_code

1. capture the current button booleans into a stable snapshot
2. expose the captured bits in NES serial order
3. handle strobe high vs strobe low behavior

Key term: strobe A strobe is a control signal written by the CPU. For the NES controller, strobe controls whether the controller keeps capturing live button state or advances through the captured serial bits.

Minimal example

controller.a = True
controller.write_strobe(1)
controller.write_strobe(0)
controller.read_bit()  # returns A bit

Common misconception

"The controller should return all buttons as one byte."

The emulator may store captured buttons as one byte internally, but the CPU-facing protocol reads one bit at a time.

Suggested implementation example

def capture_buttons(self) -> None:
    value = 0

    if self.a:
        value |= BUTTON_A
    if self.b:
        value |= BUTTON_B
    if self.select:
        value |= BUTTON_SELECT
    if self.start:
        value |= BUTTON_START
    if self.up:
        value |= BUTTON_UP
    if self.down:
        value |= BUTTON_DOWN
    if self.left:
        value |= BUTTON_LEFT
    if self.right:
        value |= BUTTON_RIGHT

    self.captured_buttons = value
    self.read_index = 0


def write_strobe(self, value: int) -> None:
    self.strobe = (value & 1) == 1

    if self.strobe:
        self.capture_buttons()


def read_bit(self) -> int:
    if self.strobe:
        self.capture_buttons()

    if self.read_index >= 8:
        return 1

    bit = (self.captured_buttons >> self.read_index) & 1
    self.read_index += 1
    return bit

Out of scope

  • CpuBus $4016 routing
  • pygame keyboard mapping
  • controller port 2
  • Famicom expansion controllers
  • DMC/controller read glitch behavior

Run this lesson

uv run pytest tests/chapter_07_controller_input/test_291_controller_capture_and_serial_read.py -v