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