264. Прерывание NMI в CPU

Реализовать механику прерывания NMI со стороны CPU.

Урок 264 из 356 · tests/chapter_04_ppu_timing_and_vblank/test_264_cpu_interrupt_nmi.py

Справочные материалы

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

Файлы для обновления

emulator/cpu/cpu.py

Зачем нужен этот шаг

PPU уже умеет вырабатывать сигнал nmi_requested. Прежде чем подключать этот сигнал к CPU через координатор системы/консоли, CPU должен знать, как самостоятельно выполнить последовательность NMI.

Что такое NMI? NMI означает Non-Maskable Interrupt (немаскируемое прерывание). На NES PPU может запросить NMI при VBlank, чтобы игровой код мог выполнить свой обработчик вертикального гашения.

Интуитивная модель

NMI похож на аппаратный аварийный переход. CPU приостанавливает текущий путь выполнения, сохраняет достаточно состояния, чтобы вернуться позже, и переходит по адресу, хранящемуся в векторе NMI.

Механистическая модель

Когда NMI принимается, CPU выполняет следующую последовательность

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

Важное различие

NMI не выполняет запись в $FFFA/$FFFB. CPU читает эти адреса. В настоящем ROM байты вектора уже существуют в PRG ROM. В этих тестах FakeROM позволяет подготовить эти байты как часть настройки теста.

Пример реализации

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)

Конкретный пример выполнения

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

Пример стека при S = $FD и PC = $8123:

write $81 to $01FD, S becomes $FC
write $23 to $01FC, S becomes $FB
write status to $01FB, S becomes $FA

Распространённое заблуждение

Флаг B не устанавливается при аппаратных прерываниях. BRK/PHP заносит регистр состояния с установленным флагом B, а NMI заносит регистр состояния со сброшенным B. Бит 5, ONE_FLAG, в заносимом байте состояния всё равно установлен.

Выходит за рамки

  • вызов CPU.interrupt_nmi() со стороны PPU
  • сброс ppu.nmi_requested
  • точная задержка и число тактов прерывания
  • прерывания IRQ/APU/маппера
  • поведение RTI, которое тестировалось ранее в главе о CPU

Запустить этот урок

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