⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.

Interrupt Dispatcher

Date:2025-12-17 StepID:0025 State: Verified

Summary

It was implementedInterrupt Dispatcher (Interrupt Service Routine - ISR)on the CPU, finally connecting the timing system (PPU) with the CPU. The CPU can now respond to interrupts such as V-Blank, Timer, LCD STAT, Serial and Joypad. Implementation includes correct management of priorities (V-Blank has higher priority than Timer, etc.), wake up from HALT when interrupts are pending, and the complete hardware sequence: disable IME, clear flag in IF, save PC on stack, and jump to vector. This is the functionality that turns the emulator from a "linear calculator" into a reactive system capable of respond to hardware events. Complete TDD test suite (6 tests) validating all functionalities. All tests pass.

Hardware Concept

Theinterruptionsare hardware signals that allow the CPU to temporarily interrupt normal code execution to handle urgent events (such as the end of a V-Blank frame or overflow of a timer). Without interruptions, the CPU would have to be constantly polling the status of the peripherals, which is inefficient.

Interruption Flow

For a real interrupt to occur (jump to vector),3 simultaneous conditions:

  1. IME (Interrupt Master Enable)must be True. It is controlled with the instructions DI (deactivate) and EI (activate).
  2. The corresponding bit inIE (Interrupt Enable, 0xFFFF)must be active. This enables specific types of interrupts.
  3. The corresponding bit inIF (Interrupt Flag, 0xFF0F)must be active. The peripherals (PPU, Timer, etc.) set these bits when an event occurs.

Hardware Sequence (when an interrupt is accepted)

When an interrupt is accepted, the hardware automatically executes this sequence:

  1. CPU turns offIMEautomatically (to avoid immediate nested interruptions).
  2. Clear the corresponding bit inI.F.(acknowledgement: "I received your message").
  3. DoesPUSH PC(save the current address to return later with RETI).
  4. Jump to the address ofinterruption vector(PC = vector).
  5. consume5 M-Cycles(20 T-Cycles) in total.

Disruption Vectors and Priorities

Each type of interrupt has a fixed vector and a specific priority (lower bit number = higher priority):

  • Bit 0:V-Blank →0x0040(Highest priority)
  • Bit 1:STAT LCD →0x0048
  • Bit 2:Timer →0x0050
  • Bit 3:Serial →0x0058
  • Bit 4:Joypad →0x0060(Lowest priority)

If multiple interrupts are pending simultaneously, the one with the highest priority is processed first.

HALT Awakening

If the CPU is inHALT(low power state) and a pending interrupt occurs (in IE and IF), the CPU must wake up (halted = False), even if IME is False. This allows the code to manually check for interrupts by polling IF after HALT. If IME is disabled, the CPU wakes up but does not jump to vector (it continues executing normally).

Fountain:Pan Docs - Interrupts, HALT behavior, Interrupt Vectors

Implementation

The method was implementedhandle_interrupts()in classCPUthat manages all the flow of interruptions. This method is called at the beginning of eachstep(), before executing any instruction, simulating the behavior of real hardware.

Components created/modified

  • CPU.handle_interrupts(): Method that implements all interrupt logic. Reads IE and IF from the MMU, calculates pending interrupts, handles HALT wakeup, processes the highest priority interrupt if IME is active, and returns the cycles consumed (5 if processed, 0 if not).
  • CPU.step(): Modified to callhandle_interrupts()at first. If an interrupt was processed, it returns immediately without executing the normal instruction.

Design decisions

Interrupt priority:It was implemented using a series ofif-elifthat checks the bits in priority order (bit 0 first, then bit 1, etc.). This implementation is clear and easy to understand, although it is not the most efficient. For a production emulator, it could be optimized using more bitwise operations. advanced, but for an educational emulator, clarity is more important than efficiency.

HALT Awakening:It was decided thathandle_interrupts()handle both HALT awakening such as interrupt processing. This simplifies the logic ofstep()and maintains all the logic of interruptions in one place.

IF Cleanup:When an interrupt is processed, only the corresponding bit is cleared, preserving the other bits. This allows other pending interrupts to be processed in the next cycle.

Affected Files

  • src/cpu/core.py- Added handle_interrupts() method, modified step() to integrate interrupt handling
  • tests/test_cpu_interrupts.py- New file with complete TDD test suite (6 tests) validating interrupts, priorities, HALT wake-up, and all vectors

Tests and Verification

The complete suite of TDD tests was run to validate all the functionalities of the interrupt dispatcher:

Command executed

python3 -m pytest tests/test_cpu_interrupts.py -v

Around

  • YOU:macOS (darwin 21.6.0)
  • Python:3.9.6
  • pytest:8.4.2

Result

============================== test session starts ==============================
platform darwin -- Python 3.9.6, pytest-8.4.2, pluggy-1.6.0
collected 6 items

tests/test_cpu_interrupts.py::TestCPUInterrupts::test_vblank_interrupt PASSED [ 16%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_interrupt_priority PASSED [ 33%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_halt_wakeup PASSED [ 50%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_no_interrupt_if_ime_disabled PASSED [ 66%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_timer_interrupt_vector PASSED [ 83%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_all_interrupt_vectors PASSED [100%]

============================== 6 passed in 0.49s ==============================

How valid

  • test_vblank_interrupt:Interrupt V-Blank is processed correctly: jumps to 0x0040, disables IME, clears IF bit 0, saves PC on stack, consumes 5 M-Cycles.
  • test_interrupt_priority:If multiple interrupts are pending (V-Blank and Timer), V-Blank (highest priority) is processed first. The Timer bit remains active for the next cycle.
  • test_halt_wakeup:If the CPU is HALT and interrupts are pending, the CPU wakes up even if IME is disabled. After waking up, continue running normally.
  • test_no_interrupt_if_ime_disabled:If IME is disabled, interrupts are not processed even if IE and IF have active bits. The CPU executes instructions normally.
  • test_timer_interrupt_vector:Interrupt Timer jumps to the correct vector (0x0050) and clears IF bit 2.
  • test_all_interrupt_vectors:All interrupt vectors are correct: V-Blank (0x0040), LCD STAT (0x0048), Timer (0x0050), Serial (0x0058), Joypad (0x0060).

Test code (critical example: V-Blank)

def test_vblank_interrupt(self) -> None:
    """Test: V-Blank interrupt is processed correctly."""
    mmu = MMU(None)
    cpu = CPU(mmu)
    
    # Set initial state
    cpu.registers.set_pc(0x1234) # Initial PC
    cpu.registers.set_sp(0xFFFE) # Initial stack pointer
    cpu.ime = True # IME activated
    
    # Enable V-Blank interrupt in IE (bit 0)
    mmu.write_byte(IO_IE, 0x01)
    
    # Activate V-Blank flag in IF (bit 0)
    mmu.write_byte(IO_IF, 0x01)
    
    # Execute step (must process the interrupt)
    cycles = cpu.step()
    
    # Verifications
    assert cycles == 5, "The interrupt must consume 5 M-Cycles"
    assert cpu.registers.get_pc() == 0x0040, "PC must jump to V-Blank vector"
    assert cpu.ime is False, "IME should be disabled automatically"
    
    # Verify that IF bit 0 is cleared
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x01) == 0, "IF bit 0 must be clear"
    
    # Verify which PC was saved on the stack (Little-Endian)
    assert cpu.registers.get_sp() == 0xFFFC, "SP must be decremented by 2"
    saved_pc_low = mmu.read_byte(0xFFFC)
    saved_pc_high = mmu.read_byte(0xFFFD)
    saved_pc = (saved_pc_high<< 8) | saved_pc_low
    assert saved_pc == 0x1234, "PC debe guardarse correctamente en la pila"

Why this test demonstrates something about the hardware:This test validates that the complete sequence interrupt is executed correctly: reading IE and IF, deactivating IME, clearing flag, PC saved on the stack, and jump to the vector. Without this functionality, the CPU cannot respond to events hardware like V-Blank, which means games can't refresh the screen properly.

Sources consulted

Note: Implementation based on official technical documentation. Code from other emulators was not consulted.

Educational Integrity

What I Understand Now

  • Interruptions as a coordination mechanism:Interrupts allow the CPU and peripherals (PPU, Timer, etc.) to coordinate without the need for constant polling. The hardware "screams" when it needs attention, and the CPU responds when it can.
  • Interrupt priority is critical:If multiple interrupts occur simultaneously, the hardware processes the one with the highest priority (lowest bit number) first. This is important to ensure that critical events (such as V-Blank) are attended to before less urgent events.
  • HALT and wake up:The HALT state allows the CPU to go into low power, but must wake up when interrupts are pending, even if IME is disabled. This allows the code to do manual IF polling after waking up.
  • IME as master switch:IME is the "master switch" that allows or blocks all interrupts. When an interrupt is processed, IME is automatically disabled to prevent immediate nested interrupts. The code must explicitly reactivate IME with EI when ready.
  • The hardware sequence is automatic:When an interrupt is accepted, the hardware automatically executes the sequence (disable IME, clear IF, PUSH PC, jump). There are no explicit instructions, it's all automatic.

What remains to be confirmed

  • Exact timing of the interruption sequence:The documentation indicates that processing an interrupt consumes 5 M-Cycles, but it is not completely clear how these cycles are distributed among the different operations (PUSH PC, IF cleanup, etc.). For now, we count 5 cycles in total.
  • Nested interrupts:If the code of an interrupt routine executes EI, can it be interrupted by another interrupt? The documentation suggests yes, but the exact behavior of the actual hardware is not completely clear. For now, we implement the basic behavior: IME is automatically disabled and the code must explicitly reactivate it.
  • HALT and timing:When the CPU is in HALT, it consumes 1 cycle for each step(). Does this cycle count for the timing of other components (PPU, Timer)? This could affect precise timing. For now, we implement the basic behavior: HALT consumes 1 cycle and does not execute instructions.

Hypotheses and Assumptions

Interrupt processing between instructions:We assume that interruptions are checked between each instruction, before the fetch of the next opcode. This is consistent with the documentation, but There could be timing subtleties in the real hardware that we are not modeling (for example, what happens if a interrupt occurs during the execution of a multi-loop instruction?).

Interrupt priority:We implement priority using a series of if-elifs that check the bits in order. This is correct according to the documentation, but we are not 100% sure that the actual hardware use exactly this logic (although the observable behavior should be the same).

Next Steps

  • [ ] Implement RETI (Return from Interrupt) instruction that restores PC from stack and reactivates IME automatically
  • [ ] Validate with a real ROM (Tetris DX) that V-Blank interrupts are processed correctly and the game progresses
  • [ ] Implement Timer interrupts (when Timer subsystem completes)
  • [ ] Implement LCD STAT interrupts (when the PPU subsystem is completed with rendering modes)
  • [ ] Investigate and document the exact timing of the interruption sequence