291. 控制器捕获与串行读取

捕获控制器按钮并串行读取。

291 / 356 · tests/chapter_07_controller_input/test_291_controller_capture_and_serial_read.py

本步骤中创建的文件

emulator/input/controller.py

为什么需要这一步

NES CPU 不会从 $4016 一次性获取完整的控制器状态字节。它每次只接收一个按钮位。在将该协议接入 CpuBus 之前,纯 Controller 对象应当能够:

参考资料

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

关键术语:选通信号 选通信号(strobe)是由 CPU 写入的控制信号。对于 NES 控制器,选通信号控制控制器是继续捕获实时按钮状态,还是逐步读取已捕获的串行位。

最小示例

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

常见误解

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

模拟器内部可能将捕获的按钮状态存储为一个字节,但面向 CPU 的协议每次只读取一位。

建议的实现示例

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

超出本步骤范围

  • CpuBus $4016 路由
  • pygame 键盘映射
  • 控制器端口 2
  • Famicom 扩展控制器
  • DMC/控制器读取毛刺行为

运行本课

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