292. CpuBus 控制器 4016

将 CpuBus $4016 的读写路由到控制器端口 1。

292 / 356 · tests/chapter_07_controller_input/test_292_cpubus_controller_4016.py

需要更新的文件

emulator/bus/cpu_bus.py

为什么需要这一步

前面的控制器步骤创建了纯 Controller 对象。现在 CPU 总线必须通过 NES 内存映射的控制器端口暴露该控制器:

$4016 = controller port 1

标准 NES 轮询序列

write 1 to $4016
write 0 to $4016
read $4016 eight times

这八次读取返回

A, B, Select, Start, Up, Down, Left, Right

参考资料

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

建议的实现示例

from emulator.input.controller import Controller


@dataclass
class CpuBus:
    ...
    controller_1: Controller = field(default_factory=Controller)

    def read(self, addr: int) -> int:
        ...

        # Controller port 1
        if addr == 0x4016:
            return self.controller_1.read_bit()

        # Controller port 2 / expansion input is out of scope for now.
        if addr == 0x4017:
            return 0

        ...

    def write(self, addr: int, value: int) -> None:
        ...

        # Controller port 1 strobe
        if addr == 0x4016:
            self.controller_1.write_strobe(value)
            return

        # $4017 writes are APU frame-counter writes, no-op for now.
        if addr == 0x4017:
            return

        ...

重要区别

$4016 read/write belongs to controller port 1.
$4017 read is controller port 2 / expansion input, out of scope for now.
$4017 write is APU frame counter, out of scope for now.

常见误解

"Controller input should be handled by pygame directly in CpuBus."

不是。CpuBus 只应与纯 Controller 对象通信。pygame 键盘映射将在后续通过手动/前端入口点更新 Controller 的按钮布尔值。

超出本步骤范围

  • pygame 键盘映射
  • 控制器端口 2 实现
  • Famicom 扩展控制器
  • DMC/控制器读取毛刺行为
  • 开放总线高位行为

运行本课

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