264. Interrupción NMI de la CPU
Implementar la mecánica de interrupción NMI del lado de la CPU.
Lección 264 de 356 · tests/chapter_04_ppu_timing_and_vblank/test_264_cpu_interrupt_nmi.py
Referencias
https://www.nesdev.org/wiki/CPU_interrupts
https://www.nesdev.org/wiki/PPU_registers#Vblank_NMIArchivos a actualizar
emulator/cpu/cpu.pyPor qué existe este paso
La PPU ya puede producir una señal nmi_requested. Antes de conectar esa señal a la CPU a través de un coordinador de sistema/consola, la CPU debe saber cómo ejecutar la secuencia NMI por sí misma.
¿Qué es NMI? NMI significa Non-Maskable Interrupt (interrupción no enmascarable). En la NES, la PPU puede solicitar NMI en el VBlank para que el código del juego pueda ejecutar su rutina de apagado vertical.
Modelo intuitivo
NMI es como un salto de emergencia por hardware. La CPU pausa su camino actual, guarda el estado suficiente para volver después, y luego salta a la dirección almacenada en el vector NMI.
Modelo mecanicista
Cuando se acepta la NMI, la CPU realiza esta secuencia
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 | lowDistinción importante
NMI no escribe en $FFFA/$FFFB. La CPU lee esas direcciones. En una ROM real, los bytes del vector ya existen en la PRG ROM. En estas pruebas, FakeROM nos permite preparar esos bytes como configuración de prueba.
Ejemplo de implementación
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)Ejemplo concreto de ejecución
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 = $C000Ejemplo de pila con S = $FD y PC = $8123:
write $81 to $01FD, S becomes $FC
write $23 to $01FC, S becomes $FB
write status to $01FB, S becomes $FAConcepto erróneo común
El flag B no se activa para interrupciones de hardware. BRK/PHP apilan el estado con B activado, pero NMI apila el estado con B desactivado. El bit 5, ONE_FLAG, sigue activado en el byte de estado apilado.
Fuera de alcance
- La PPU llamando a CPU.interrupt_nmi()
- borrado de ppu.nmi_requested
- latencia/conteo de ciclos exacto de la interrupción
- interrupciones IRQ/APU/mapper
- comportamiento de RTI, que se probó anteriormente en el capítulo de la CPU
Ejecutar esta lección
uv run pytest tests/chapter_04_ppu_timing_and_vblank/test_264_cpu_interrupt_nmi.py -v