This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Complete Final CPU Opcodes
Summary
Critical opcode implementedLD SP, HL(0xF9) that was missing from the CPU instruction set. This opcode is essential for configuring stack frames and context switching in complex routines. It was also verified that the opcodesJP (HL)(0xE9) andRETI(0xD9) were correctly implemented. The emulator had advanced running Pokémon Red and hit the unimplemented 0xF9 opcode, indicating that we are very close to completing the instruction set. 8 unit tests were created that validate the correct behavior of these three critical opcodes.
Hardware Concept
The LR35902 CPU provides three critical instructions for stack manipulation and flow control:
- LD SP, HL (0xF9): Loads the value of the HL register pair into the Stack Pointer (SP). This instruction is useful for resetting the stack, switching context (switching stack frames), or configuring the stack at the beginning of a routine. It consumes 2 M-Cycles and DOES NOT modify flags. It is the inverse operation of
RH HL, SP+r8(0xF8), but without offset. - JP (HL) (0xE9): Indirect jump using the value of the HL register pair as the destination address. It is equivalent to
JP HL, but the official syntax isJP (HL). This statement is useful for implementing jump tables or function calls using pointers. Consumes 1 M-Cycle (register reading only, not memory). - RETI (0xD9): Returns from an interruption routine (ISR - Interrupt Service Routine). It is the same as
RETbut it also reactivates IME (Interrupt Master Enable). When an interrupt is processed, IME is automatically disabled to prevent nested interrupts. RETI reactivates IME to allow interrupts to resume operation after exiting the routine. Consumes 4 M-Cycles.
Use in games:These instructions are essential for:
LD SP, HL: Configure stack frames in complex routines, especially in menu or combat systems where context switching is needed.JP (HL): Implement jump tables where different values in HL point to different routines. It is also used for indirect function calls.RETI: Correct interrupt handling, especially V-Blank, which is critical for graphics rendering.
Implementation
Implemented opcode 0xF9 (LD SP, HL) and verified that opcodes 0xE9 and 0xD9 were correctly implemented.
Opcode 0xF9: LD SP, HL
Implemented in_op_ld_sp_hl():
- Read the HL value using
self.registers.get_hl(). - Set SP to the value of HL using
self.registers.set_sp(hl_value). - It DOES NOT modify flags (unlike other LD instructions).
- Consumes 2 M-Cycles (fetch opcode + reading registers).
The implementation is simple but critical: it allows games to configure the stack dynamically based on values calculated in HL.
Verification of Existing Opcodes
The following opcodes were verified to be correctly implemented:
- 0xE9 (JP HL): Already implemented in
_op_jp_hl(). Jumps to the address in HL without reading from memory. - 0xD9 (RETI): Already implemented in
_op_reti(). POPs the return address and reactivates IME.
Design decisions
Simplicity:The implementation ofLD SP, HLIt is direct because it does not require complex calculations or modification of flags. It remains consistent with other 16-bit LD instructions such asLD SP, d16(0x31).
Comprehensive tests:Tests were created to cover edge cases such as wrap-around, zero values, and verification that flags are not modified.
Affected Files
src/cpu/core.py- Added opcode 0xF9 to the dispatch table and implemented function_op_ld_sp_hl()tests/test_cpu_final_ops.py- Created new file with 8 unit tests to validate 0xF9, 0xE9 and 0xD9
Tests and Verification
8 unit tests were created intests/test_cpu_final_ops.pythat validate the correct behavior of the three opcodes:
Tests for LD SP, HL (0xF9)
- test_ld_sp_hl_basic: Verify basic HL load in SP and that it consumes 2 M-Cycles.
- test_ld_sp_hl_wraparound: Verifies correct handling of wrap-around with HL = 0xFFFF.
- test_ld_sp_hl_zero: Verify operation with HL = 0x0000.
- test_ld_sp_hl_no_flags: Verify that the flags are NOT modified (critical).
Tests for JP (HL) (0xE9)
- test_jp_hl_basic: Check basic jump to address in HL and that it consumes 1 M-Cycle.
- test_jp_hl_jump_table: Verifies use as jump table (uses register value, does not read from memory).
Tests for RETI (0xD9)
- test_reti_basic: Checks interrupt return, IME reactivation and that it consumes 4 M-Cycles.
- test_reti_vs_ret: Verifies that RETI reactivates IME while RET does not.
Test Execution
Command executed: pytest tests/test_cpu_final_ops.py -v
Around:Windows 10, Python 3.13.5
Result: ✅ 8 passedin 0.06s
What is valid:
- That
LD SP, HLcorrectly loads the HL value in SP without modifying flags. - That
JP (HL)correctly jumps to the address in HL using only the register value. - That
RETIcorrectly returns from interruptions and reactivates IME (key difference with RET).
Test Code (Essential Fragment)
def test_ld_sp_hl_basic(self):
"""Test: Verify that LD SP, HL loads the HL value into SP."""
mmu = MMU()
cpu = CPU(mmu)
cpu.registers.set_pc(0x0100)
cpu.registers.set_hl(0x1234)
cpu.registers.set_sp(0x0000)
mmu.write_byte(0x0100, 0xF9) # LD SP, HL
cycles = cpu.step()
assert cpu.registers.get_sp() == 0x1234, "SP must be 0x1234"
assert cpu.registers.get_hl() == 0x1234, "HL should not change"
assert cycles == 2, "LD SP, HL must consume 2 M-Cycles"
Why this test demonstrates something about the hardware:The test verifies that the CPU can transfer the value of a pair of registers (HL) directly to the Stack Pointer without performing calculations or modifying flags. This is essential for setting stack frames dynamically, a common operation in complex game code.
Sources consulted
- Bread Docs:CPU Instruction Set- Reference for LD SP, HL (0xF9), JP (HL) (0xE9) and RETI (0xD9)
Note: Implementation based on official technical documentation of the LR35902 CPU. Code from other emulators was not consulted.
Educational Integrity
What I Understand Now
- LD SP, HL is critical for stack frames:Games use this instruction to configure the stack dynamically, especially in complex routines like menu systems or combat where context switching is needed.
- JP (HL) uses the register value, it does not read from memory:Unlike instructions like
JP (nn)who read by heart,JP (HL)directly uses the value of the HL register as the destination address. This is useful for jump boards. - RETI reactivates IME automatically:The key difference between RET and RETI is that RETI reactivates IME after returning, allowing interrupts to work again. This is essential for correct interrupt handling.
What remains to be confirmed
- Remaining Opcodes:Although we have implemented the most common opcodes, there may be some "ghost" or rarely used opcodes that are not yet implemented. It will be validated by running full test ROMs.
- Behavior in edge cases:Although the tests cover basic and wrap-around cases, actual behavior with commercial ROMs may reveal additional edge cases.
Hypotheses and Assumptions
LD SP, HL does not modify flags:This assumption is supported by Pan Docs documentation and was validated with tests. However, if in the future we find unexpected behavior with real ROMs, we will have to review it.
JP (HL) does not read from memory:This assumption is supported by documentation and was validated with tests. The instruction directly uses the value of the HL register as the destination address.
Next Steps
- [ ] Run Pokémon Red/Blue to verify that the emulator advances beyond opcode 0xF9
- [ ] Identify any other missing opcodes that may appear during execution
- [ ] Validate that the emulator can run complex code such as menu or combat systems
- [ ] Continue with rendering and sync improvements if necessary