264. Cpu interrupt nmi
Implement CPU-side NMI interrupt mechanics.
Lesson 264 of 356 · tests/chapter_04_ppu_timing_and_vblank/test_264_cpu_interrupt_nmi.py
References
https://www.nesdev.org/wiki/CPU_interrupts
https://www.nesdev.org/wiki/PPU_registers#Vblank_NMIFiles to update
emulator/cpu/cpu.pyWhy this step exists
The PPU can already produce an nmi_requested signal. Before connecting that signal to the CPU through a system/console coordinator, the CPU must know how to perform the NMI sequence by itself.
What is NMI? NMI means Non-Maskable Interrupt. On the NES, the PPU can request NMI at VBlank so game code can run its vertical blank handler.
Intuitive model
NMI is like a hardware emergency jump. The CPU pauses its current path, saves enough state to return later, then jumps to the address stored in the NMI vector.
Mechanistic model
When NMI is accepted, the CPU performs this sequence
1. Push PC high byte
2. Push PC low byte
3. Push status with:
ONE_FLAG set
B_FLAG clear
4. Set INTERRUPT_FLAG in CPU status
5. Read low byte from $FFFA
6. Read high byte from $FFFB
7. Set PC = high << 8 | lowImportant distinction
NMI does not write to $FFFA/$FFFB. The CPU reads those addresses. In a real ROM, the vector bytes already exist in PRG ROM. In these tests, FakeROM lets us prepare those bytes as test setup.
Example implementation
NMI_VECTOR_LOW = 0xFFFA
NMI_VECTOR_HIGH = 0xFFFB
class CPU:
...
def interrupt_nmi(self) -> None:
# Save the current PC so RTI can restore it later.
pc_high = (self.pc >> 8) & 0xFF
pc_low = self.pc & 0xFF
self.push_stack(pc_high)
self.push_stack(pc_low)
# Hardware interrupts push status with B clear and bit 5 set.
status_to_push = self.p | ONE_FLAG
status_to_push &= ~B_FLAG
self.push_stack(status_to_push)
# After accepting an interrupt, set the interrupt-disable flag.
self.p |= INTERRUPT_FLAG
# NMI vector bytes are read from PRG space. NMI does not write them.
low = self.bus.read(NMI_VECTOR_LOW)
high = self.bus.read(NMI_VECTOR_HIGH)
self.pc = low | (high << 8)Concrete runtime example
FakeROM setup:
$FFFA = $00
$FFFB = $C0
CPU before NMI:
PC = $8123
S = $FD
CPU.interrupt_nmi()
CPU after NMI:
stack contains return PC/status
PC = $C000Stack example with S = $FD and PC = $8123:
write $81 to $01FD, S becomes $FC
write $23 to $01FC, S becomes $FB
write status to $01FB, S becomes $FACommon misconception
The B flag is not set for hardware interrupts. BRK/PHP push status with B set, but NMI pushes status with B clear. Bit 5, ONE_FLAG, is still set in the pushed status byte.
Out of scope
- PPU calling CPU.interrupt_nmi()
- clearing ppu.nmi_requested
- exact interrupt latency/cycle counts
- IRQ/APU/mapper interrupts
- RTI behavior, which was tested earlier in the CPU chapter
Run this lesson
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_264_cpu_interrupt_nmi.py -v