265. Console 消费 PPU 的 NMI 请求

把 Console 建模为消费 PPU NMI 请求的协调者。

265 / 356 · tests/chapter_04_ppu_timing_and_vblank/test_265_console_consumes_ppu_nmi_request.py

参考资料

https://www.nesdev.org/wiki/CPU_interrupts
https://www.nesdev.org/wiki/PPU_registers#Vblank_NMI

需要创建的文件

emulator/console.py

此步骤存在的原因

模拟器现在已经具备了实现 VBlank NMI 所需的两个独立机制

PPU mechanism:
    PPU enters VBlank and sets ppu.nmi_requested = True

CPU mechanism:
    CPU.interrupt_nmi() pushes PC/status and jumps through $FFFA/$FFFB

现在我们需要一个小型协调器,把这两个机制连接起来,同时不让 CPU 和 PPU 直接相互依赖。

什么是 Console?Console 是未来最顶层的模拟 NES 主机。随着时间推移,它可以拥有并协调如下子系统:

CPU
PPU
cartridge/mapper
controllers
APU/audio
frame stepping

在当前阶段,Console 从很小的规模开始。它的第一个任务只是消费一次 PPU 的 NMI 请求,并调用 CPU.interrupt_nmi()。

正确的职责划分

PPU:
    produces nmi_requested

CPU:
    implements interrupt_nmi()

Console:
    connects the PPU signal to the CPU mechanism

重要的架构规则

不要把 PPU 的所有权放进 CPU 里。CPU 不应该去询问 PPU 是否有 NMI 待处理,那会在 CPU 执行和视频硬件之间造成隐藏的耦合。

最小实现示例

from dataclasses import dataclass

from emulator.cpu.cpu import CPU
from emulator.ppu.ppu import PPU


@dataclass
class Console:
    cpu: CPU
    ppu: PPU

    def consume_nmi_if_requested(self) -> None:
        if not self.ppu.nmi_requested:
            return

        self.ppu.nmi_requested = False
        self.cpu.interrupt_nmi()

在之后的时序测试中会引入的未来形态

class Console:
    ...

    def step(self) -> None:
        cpu_cycles = self.cpu.step()
        self.ppu.step(cpu_cycles * 3)
        self.consume_nmi_if_requested()

本文件不测试 Console.step()。CPU/PPU 的时序集成会在 CPU.step() 返回指令周期数之后,作为单独的一步进行测试。

常见误解

由于 NMI 是由 PPU 引起的,可能会觉得让 CPU 拥有 PPU 是很自然的做法。请避免这样做。CPU 只是接收中断信号,不应该拥有视频设备。

不在本步骤范围内

  • CPU/PPU 的周期比例
  • Console.step()
  • 渲染
  • 手柄
  • APU/音频
  • 精确的 NMI 延迟

运行本课

uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_265_console_consumes_ppu_nmi_request.py -v