267. CPU.step() 返回周期数
让 CPU.step() 返回基本指令周期数。
第 267 / 356 · tests/chapter_04_ppu_timing_and_vblank/test_267_cpu_step_returns_cycles.py
参考资料
https://www.nesdev.org/wiki/Visual6502wiki/6502_all_256_Opcodes需要更新的文件
emulator/cpu/cpu.py此步骤存在的原因
模拟器现在已经有了独立的 OPCODE_CYCLES 表。下一步的时序桥接是让 CPU.step() 返回它所执行的操作码的基本周期数。
这为未来的 Console.step() 形态做准备:
cpu_cycles = cpu.step()
ppu.step(cpu_cycles * 3)
console.consume_nmi_if_requested()什么是基本指令周期数?基本指令周期数是指在加上任何动态惩罚周期之前,一个操作码通常需要的 CPU 周期数。
最小示例
NOP implied opcode $EA -> 2 cycles
LDA immediate opcode $A9 -> 2 cycles
JSR absolute opcode $20 -> 6 cycles常见误解
周期数和取指的字节数并不相同。JSR 占 3 个字节,却需要 6 个周期。不要在 fetch_byte() 内部计算周期数。
建议的实现示例
from emulator.cpu.opcodes import OPCODE_CYCLES, OPCODE_TABLE
class CPU:
...
def step(self) -> int:
opcode = self.fetch_byte()
handler = OPCODE_TABLE.get(opcode)
if handler is None:
raise NotImplementedError(f"Opcode {opcode:02X} not implemented")
handler(self)
return OPCODE_CYCLES[opcode]为什么这样做能避免大规模重构
OPCODE_TABLE 仍然是原有的操作码到处理函数的映射。OPCODE_CYCLES 是一张并行的元数据表。旧的指令行为测试应该能够继续通过,因为 CPU.step() 仍然执行的是同一个处理函数。
重要限制
这里只返回基本周期数。动态时序在这一步中特意排除在外:
branch taken penalties
branch page-cross penalties
indexed load page-cross penalties这些应该在之后作为单独的一步来建模。
不在本步骤范围内
- 把 OPCODE_TABLE 重构为数据类条目
- 在 fetch_byte() 中加入周期数
- Console.step()
- 按 CPU 周期数 * 3 来推进 PPU
- 动态的额外周期
运行本课
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_267_cpu_step_returns_cycles.py -v