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_NMI

Archivos a actualizar

emulator/cpu/cpu.py

Por 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 | low

Distinció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 = $C000

Ejemplo 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 $FA

Concepto 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