This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Implementation of the Interrupt System in C++
Summary
Implemented the complete interrupt system in C++, adding the ability of the CPU to react to external hardware (V-Blank, Timer, LCD STAT, Serial, Joypad). Implemented 3 new critical opcodes: DI (0xF3), EI (0xFB) and HALT (0x76), along with the interrupt dispatcher that runs before each instruction. The system Correctly handles interrupt priority, EI delay, and wakeup by HALT. All tests pass, validating the precise behavior of the real hardware.
Hardware Concept
The Game Boy's interrupt system allows external hardware to "interrupt" normal execution of the CPU to execute critical service routines. This mechanism It is essential for the synchronization of components such as the PPU (V-Blank), the Timer and the Joypad with the game code.
Components of the Interruption System
- IME (Interrupt Master Enable): An internal CPU flag that enables or globally disable all interrupts. DI deactivates it immediately, EI activates it with a delay of 1 instruction (real hardware behavior).
- IE (Interrupt Enable, 0xFFFF): Memory register that contains a bit mask indicating what types of interrupts are enabled (bits 0-4).
- IF (Interrupt Flag, 0xFF0F): Memory register containing flags of pending interruptions. These bits are set by the hardware when an event occurs, and the CPU clears them when processing the interrupt.
Types of Interruptions and Vectors
The Game Boy has 5 types of interruptions, each with its own interruption vector (address the CPU jumps to when the interrupt is processed):
- Bit 0 - V-Blank (0x0040): Occurs at the end of each frame, when the PPU finishes drawing the screen. Highest priority.
- Bit 1 - LCD STAT (0x0048): Occurs when the PPU reaches certain states (H-Blank mode, V-Blank, OAM Search, etc.).
- Bit 2 - Timer (0x0050): Occurs when the timer counter is overflows
- Bit 3 - Serial (0x0058): Occurs when a transfer is completed serial (link cable).
- Bit 4 - Joypad (0x0060): Occurs when a key is pressed. Lowest priority.
Interrupt Processing Flow
Interrupt checking occursbeforeof each instruction, not after. This is critical for timing accuracy. The flow is:
- CPU reads IE (0xFFFF) and IF (0xFF0F).
- Calculate pending interruptions:
pending = IE & IF & 0x1F. - If CPU is HALT and interrupt is pending, wake up (halted = false).
- If IME is active and interrupts are pending:
- Disables IME (prevents immediate nested interrupts).
- Find the bit with the lowest weight (highest priority).
- Clear the bit in IF (acknowledgement).
- Save PC on the stack (return address).
- Jump to the interrupt vector (PC = vector).
- Consumes 5 M-Cycles.
HALT and Awakening
The HALT instruction puts the CPU into a low-power state. CPU stops executing instructions until:
- An interrupt occurs (if IME is active, it is processed immediately).
- Or there is a pending interrupt on IF (even without IME), in which case the CPU wakes up but does not process the interrupt (allows manual IF polling).
C++ Optimization: Bitwise Magic
In C++, interrupt checking is optimized using bitwise operations:
uint8_t fired = ie & if_reg & 0x1F;: Calculates pending interrupts in a single operation.- Searching for the lowest weight (priority) bit uses an if-else cascade that the compiler optimizes with branch prediction.
- The method
handle_interrupts()runs 4 million times per second (once per theoretical clock cycle), so every nanosecond counts.
Fountain:Pan Docs - Interrupts, HALT behavior, Interrupt Vectors
Implementation
Added 3 private members to the CPU class to manage interrupt state:ime_(Interrupt Master Enable),halted_(HALT state) andime_scheduled_(flag for EI delay). The methodhandle_interrupts()runs at the beginning of eachstep(), before fetch, ensuring that the
Interruptions are processed with the highest priority.
Components created/modified
- CPU.hpp: Added members
ime_,halted_,ime_scheduled_and public methodsget_ime()andget_halted(). Declared private methodhandle_interrupts(). - CPU.cpp: Implemented
handle_interrupts()with logic HALT priority and wakeup. Modifiedstep()to integrate check of interruptions, HALT management and EI delay. Implemented DI opcodes (0xF3), EI (0xFB) and HALT (0x76). - cpu.pxd: Added declarations
get_ime()andget_halted()for exposure to Cython. - cpu.pyx: Added properties
imeandhaltedfor access from Python. - tests/test_core_cpu_interrupts.py: Complete suite of 7 tests to validate DI, EI, HALT and interrupt dispatcher.
Design decisions
- Check before fetch: Interrupts are checked before reading the opcode, not after. This ensures that an interrupt can interrupt even an instruction that is about to be executed, replicating hardware behavior real.
- EI delay: EI activates IME after the next instruction, not immediately. This allows the instruction following EI to be executed without interruptions, a critical behavior of the actual hardware used by many games.
- Wake up from HALT without IME: If the CPU is in HALT and there is an interruption pending (even without IME), the CPU wakes up but does not process the interrupt. This allows code to manually poll IF after HALT.
- Priority per least significant bit: The search for the least significant bit (LSB) ensures that V-Blank (bit 0) always has the highest priority, as in real hardware.
- IF Atomic Cleanup: When processing an interrupt, it is only cleared the corresponding bit in IF, not all the bits. This allows other interruptions pending are processed in the next cycle.
Keycode: handle_interrupts()
The methodhandle_interrupts()It is the heart of the system. It runs millions
of times per second, so it is optimized for maximum performance:
uint8_t CPU::handle_interrupts() {
constexpr uint16_t ADDR_IF = 0xFF0F;
constexpr uint16_t ADDR_IE = 0xFFFF;
uint8_t if_reg = mmu_->read(ADDR_IF) & 0x1F;
uint8_t ie_reg = mmu_->read(ADDR_IE) & 0x1F;
uint8_t pending = ie_reg & if_reg;
if (halted_ && pending != 0) {
halted_ = false; // Wake up HALT
}
if (ime_ && pending != 0) {
ime_ = false; // Disable IME
// Find least significant bit (priority)
// ...process interrupt...
return 5; // 5 M-Cycles consumed
}
return 0; // No interruptions
}
Affected Files
src/core/cpp/CPU.hpp- Added interrupt status members and public methodssrc/core/cpp/CPU.cpp- Implemented handle_interrupts() and DI/EI/HALT opcodessrc/core/cython/cpu.pxd- Added get_ime() and get_halted() declarationssrc/core/cython/cpu.pyx- Added ime and halted properties for Pythontests/test_core_cpu_interrupts.py- Complete suite of tests (7 tests, all passing)
Tests and Verification
A complete suite of 7 tests was created that validate all aspects of the control system. interruptions:
- test_di_disables_ime: Verify that DI disables IME immediately.
- test_ei_delayed_activation: Verifies that EI activates IME after the next instruction (1 op delay).
- test_halt_stops_execution: Verifies that HALT stops execution and PC does not change.
- test_halt_wakeup_on_interrupt: Verifies that HALT wakes up when there is interruption pending, even without DT.
- test_interrupt_dispatch_vblank: Verifies that a V-Blank interrupt is processed successfully (PC jumps to 0x0040, previous PC on stack, IF is cleared, IME is deactivated).
- test_interrupt_priority: Verifies that interrupts are processed according to priority (V-Blank has priority over Timer).
- test_all_interrupt_vectors: Verifies that all 5 vectors of interrupt are correct (0x0040, 0x0048, 0x0050, 0x0058, 0x0060).
Result:All tests pass correctly. The interruption system works with hardware precision, processing interrupts in nanoseconds thanks to C++ optimization.
Sources consulted
- Pan Docs - Interrupts: Complete description of the interrupt system, vectors, priority and IME behavior.
- Pan Docs - HALT behavior: Behavior of HALT and wake up with/without IME.
- Pan Docs - Interrupt Vectors: Interrupt vectors and memory addresses (IE at 0xFFFF, IF at 0xFF0F).
- GBEDG (Game Boy Emulation Development Guide): Implementation details interruptions and timing.
Educational Integrity
What I Understand Now
- Interrupt system: Interrupt checking occurs before each instruction, not after. This is critical for timing accuracy and allows Interrupts interrupt even instructions that are about to be executed.
- EI delay: EI activates IME after the next instruction, not immediately. This real hardware behavior is used by many games to ensure that certain critical instructions are executed without interruptions.
- HALT and wake up: HALT can wake up even without active IME if there is pending interruptions. This allows manual IF polling, a common technique in games for precise timing.
- Interrupt Priority: Priority is determined by the lower weight (LSB). V-Blank (bit 0) always has the highest priority, ensuring so that the screen refresh never delays.
- C++ Optimization: The handle_interrupts() method is executed millions of times. times per second. In C++, bitwise operations compile directly to instructions of machine, eliminating the overhead of Python and allowing real-time performance.
What remains to be confirmed
- Nested interrupts: Although IME is automatically disabled when process an interrupt, it is not completely clear whether the interrupt code you can reactivate IME immediately (with EI) to allow nested interrupts. This It will be validated with real test ROMs.
- Exact HALT Timing: The exact behavior of HALT when there is multiple pending interrupts needs validation with real hardware or test ROMs specialized.
Hypotheses and Assumptions
Interrupt checking is assumed to occur exactly before fetch, not during fetch. This assumption is based on the typical behavior of similar CPUs (Z80/8080) and the Pan Docs documentation, but needs validation with real hardware for confirmation absolute.
Next Steps
- [ ] Implement more CPU opcodes (LD indirect, 16-bit operations, etc.)
- [ ] Integrate the interruption system with the PPU (V-Blank) and the Timer
- [ ] Validate the interrupt system with real test ROMs
- [ ] Optimize handle_interrupts() with direct pointers to IE/IF if necessary
- [ ] Implement RETI (Return from Interrupt) that reactivates IME automatically