This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Stack Operations Completion (DE, HL, AF)
Summary
This Step completes stack operations (PUSH/POP) for all CPU register pairs. Diagnosing Step 0269 revealed that the Stack Pointer was no longer pointing to the ROM (fix successful), but the CPU was entering an infinite loop ofRST 38 (PC:0038) with the SP plummeting.
The root cause was the lack of instructionsPUSH/POPfor couplesOF, H.L.and, critically,AF. Pokémon usaPUSH AFandPOP AFconstantly to save and recover the state of the flags. If these instructions are not implemented, the stack becomes misaligned or the registers are left with garbage values, causing jumps to invalid addresses (which are read as0xFF, executingRST 38).
They were implemented6 new instructions: PUSH(0xD5),POP OF(0xD1),PUSH HL(0xE5),POP HL(0xE1),PUSH AF(0xF5) andPOP AF(0xF1). The implementation ofPOP AFis especially critical, since the low 4 bits of the F register must always be zero.
Hardware Concept
The stack (Stack) is a LIFO (Last In First Out) data structure that grows towards smaller memory addresses. On the Game Boy, the battery is typically located in WRAM (0xC000-0xDFFF) or HRAM (0xFF80-0xFFFE).
Stack Operations (PUSH/POP)
The PUSH and POP instructions allow you to store and retrieve 16-bit values (register pairs) on the stack. There are 4 pairs of registers that can be pushed/popped from the stack:
- B.C.: B (high byte) and C (low byte) - Already implemented in Step 0106
- OF: D (high byte) and E (low byte) -Implemented in this Step
- H.L.: H (high byte) and L (low byte) -Implemented in this Step
- AF: A (high byte) and F (low byte) -Implemented in this Step
PUSH (Push to Stack)
The PUSH instruction pushes a pair of 16-bit registers onto the stack:
- Decreases SP by 1 (stack grows downwards)
- Writes the high byte (MSB) to the SP address
- Decreases SP by 1
- Write the low byte (LSB) to the SP address
Timing:4 M-Cycles (all PUSH instructions).
POP (Remove from Stack)
The POP instruction pops a pair of 16-bit registers from the stack:
- Read the low byte (LSB) of the SP address
- Increase SP by 1
- Read the high byte (MSB) of the SP address
- Increase SP by 1
- Combines the bytes in Little-Endian format and saves them in the register pair
Timing:3 M-Cycles (all POP instructions).
POP AF - Special Case (CRITICAL)
The F register (Flags) has a hardware peculiarity:the lower 4 bits are always 0. Only bits 7, 6, 5, 4 are valid (Z, N, H, C respectively).
when we doPOP AF, we need to ensure that the low 4 bits of the F register are explicitly cleared. Althoughset_af()already apply the maskREGISTER_F_MASK (0xF0), we make it explicit with& 0xFFF0for clarity and robustness.
Implementation: regs_->set_af(pop_word() & 0xFFF0);
Why are they critical?
If these instructions are missing or poorly implemented:
- The game tries to save the state of the flags using
PUSH AF. - If not implemented, the CPU treats that byte as an unrecognized opcode (or NOP),but it doesn't push anything onto the stack.
- The game continues running.
- Suddenly you find a
POP AF(which we do have implemented). - Does
P.O.P.from the stack. But since we never did thePUSHof thePUSH AF, we remove garbage (or underflow). - The flags are left with garbage values. The game makes incorrect decisions based on corrupted flags.
- A
RETlater may jump to an invalid address (read as0xFF), runningRST 38and entering an infinite loop.
Fountain:Pan Docs - "CPU Instruction Set", "Stack Operations", "Register F (Flags)"
Implementation
6 new instructions were implemented in the methodstep()ofCPU.cpp, right after the existing PUSH/POP BC instructions.
PUSH DE (0xD5)
case 0xD5: // PUSH DE (Push DE onto stack)
{
uint16_t de = regs_->get_de();
push_word(from);
cycles_ += 4; // PUSH DE consumes 4 M-Cycles
return 4;
}
POP FROM (0xD1)
case 0xD1: // POP DE (Pop from stack into DE)
{
uint16_t value = pop_word();
regs_->set_of(value);
cycles_ += 3; // POP DE consumes 3 M-Cycles
return 3;
}
PUSH HL (0xE5)
case 0xE5://PUSH HL (Push HL onto stack)
{
uint16_t hl = regs_->get_hl();
push_word(hl);
cycles_ += 4; // PUSH HL consumes 4 M-Cycles
return 4;
}
POP HL (0xE1)
case 0xE1: // POP HL (Pop from stack into HL)
{
uint16_t value = pop_word();
regs_->set_hl(value);
cycles_ += 3; // POP HL consumes 3 M-Cycles
return 3;
}
PUSH AF (0xF5)
case 0xF5: // PUSH AF (Push AF onto stack)
{
uint16_t af = regs_->get_af();
push_word(af);
cycles_ += 4; // PUSH AF consumes 4 M-Cycles
return 4;
}
POP AF (0xF1) - CRITICAL
case 0xF1: // POP AF (Pop from stack into AF)
{
// CRITICAL: The low 4 bits of the F register must ALWAYS be 0
// Real hardware guarantees that these bits can never be written
// Note: set_af() already applies REGISTER_F_MASK (0xF0), but we do it
// explicit with & 0xFFF0 for clarity and robustness
uint16_t value = pop_word();
regs_->set_af(value & 0xFFF0); // Clear low bits of F explicitly
cycles_ += 3; // POP AF consumes 3 M-Cycles
return 3;
}
Design Decisions
- Explicit low bit cleanup in POP AF:Although
set_af()already applies the mask, we do the explicit cleaning with& 0xFFF0for clarity and robustness. This ensures that the low 4 bits of the F register are always zero, as required by the hardware. - Code organization:The instructions are grouped together after PUSH/POP BC for consistency and ease of maintenance.
- Precise timing:Each instruction returns the exact number of M-Cycles according to Pan Docs (4 for PUSH, 3 for POP).
Affected Files
src/core/cpp/CPU.cpp- Added 6 new stack instructions in the methodstep():- PUSH DE (0xD5) - 4 M-Cycles
- POP DE (0xD1) - 3 M-Cycles
- PUSH HL (0xE5) - 4 M-Cycles
- POP HL (0xE1) - 3 M-Cycles
- PUSH AF (0xF5) - 4 M-Cycles
- POP AF (0xF1) - 3 M-Cycles (with explicit clearing of low F bits)
Tests and Verification
Compiled C++ module validation:The instructions were implemented directly in C++ and require recompilation.
Compile command:
.\rebuild_cpp.ps1
Test command:
python main.py roms/pkmn.gb
Expected verifications:
- The infinite loop of
RST 38(PC:0038) should disappear. - The Stack Pointer should remain stable (not plummet).
- The game should advance past the waiting loop and show the intro (stars, Game Freak, Gengar).
- Yeah
PUSH AFwas the culprit (almost certainly), this will stabilize the system permanently.
Note:Complete unit tests can be implemented in a future Step, following the pattern of existing tests.
Sources consulted
- Bread Docs:CPU Instruction Set - PUSH Instructions
- Bread Docs:CPU Instruction Set - POP Instructions
- Bread Docs:CPU Instruction Set - Register F (Flags)
- Bread Docs:CPU Instruction Set - Stack Operations
Educational Integrity
What I Understand Now
- RST loop 38:If the game "derails" and jumps to an empty area, read
0xFF, executeRST 38, push the PC to the stack, jump to0038, read0xFFagain (yes0038does not have valid code), push again... This causes a Stack Overflow (the SP goes down until it turns around). - PUSH/POP AF:Pokémon usa
PUSH AFandPOP AFconstantly to save and recover the state of the flags. If these instructions are not implemented, the stack becomes misaligned or the registers are left with garbage values, causing jumps to invalid addresses. - Register F:The low 4 bits of the F register must always be zero. When doing
POP AF, we must clear those bits explicitly with& 0xFFF0.
What remains to be confirmed
- Validation with real ROMs:We need to run the emulator with Pokémon Red and verify that the loop
RST 38disappears and the game progresses correctly. - Unit tests:Implement complete unit tests that validate PUSH/POP behavior for all record pairs, especially the special case of POP AF.
Hypotheses and Assumptions
We assumed that the lack of PUSH/POP AF was the main cause of the infinite loop ofRST 38. If the problem persists after this Step, we will need to investigate other possible causes (such as other missing instructions or memory management problems).
Next Steps
- [ ] Recompile the C++ module with
.\rebuild_cpp.ps1 - [ ] Run the emulator with Pokémon Red and verify that the loop
RST 38disappear - [ ] Verify that the Stack Pointer remains stable (does not plummet)
- [ ] Verify that the game progresses past the waiting loop and shows the intro (stars, Game Freak, Gengar)
- [ ] If the problem persists, investigate other possible causes (other missing instructions, memory management problems, etc.)
- [ ] Implement full unit tests for the new instructions (optional, may be a future Step)