Este proyecto es educativo y Open Source. No se copia código de otros emuladores. Implementación basada únicamente en documentación técnica y tests permitidas.
Stack Operations Completion (DE, HL, AF)
Resumen
Este Step completa las operaciones de pila (PUSH/POP) para todos los pares de registros de la CPU. El diagnóstico del Step 0269 reveló que el Stack Pointer ya no apuntaba a la ROM (corrección exitosa), pero el CPU entraba en un bucle infinito de RST 38 (PC:0038) con el SP cayendo en picada.
La causa raíz era la falta de las instrucciones PUSH/POP para los pares DE, HL y, críticamente, AF. Pokémon usa PUSH AF y POP AF constantemente para guardar y recuperar el estado de los flags. Si estas instrucciones no están implementadas, la pila se desalinea o los registros quedan con valores basura, causando saltos a direcciones inválidas (que se leen como 0xFF, ejecutando RST 38).
Se implementaron 6 nuevas instrucciones: PUSH DE (0xD5), POP DE (0xD1), PUSH HL (0xE5), POP HL (0xE1), PUSH AF (0xF5) y POP AF (0xF1). La implementación de POP AF es especialmente crítica, ya que los 4 bits bajos del registro F siempre deben ser cero.
Concepto de Hardware
La pila (Stack) es una estructura de datos LIFO (Last In First Out) que crece hacia direcciones menores de memoria. En la Game Boy, la pila se encuentra típicamente en WRAM (0xC000-0xDFFF) o HRAM (0xFF80-0xFFFE).
Operaciones de Pila (PUSH/POP)
Las instrucciones PUSH y POP permiten guardar y recuperar valores de 16 bits (pares de registros) en la pila. Hay 4 pares de registros que pueden ser empujados/sacados de la pila:
- BC: B (byte alto) y C (byte bajo) - Ya implementado en Step 0106
- DE: D (byte alto) y E (byte bajo) - Implementado en este Step
- HL: H (byte alto) y L (byte bajo) - Implementado en este Step
- AF: A (byte alto) y F (byte bajo) - Implementado en este Step
PUSH (Empujar a la Pila)
La instrucción PUSH empuja un par de registros de 16 bits en la pila:
- Decrementa SP en 1 (la pila crece hacia abajo)
- Escribe el byte alto (MSB) en la dirección SP
- Decrementa SP en 1
- Escribe el byte bajo (LSB) en la dirección SP
Timing: 4 M-Cycles (todas las instrucciones PUSH).
POP (Sacar de la Pila)
La instrucción POP saca un par de registros de 16 bits de la pila:
- Lee el byte bajo (LSB) de la dirección SP
- Incrementa SP en 1
- Lee el byte alto (MSB) de la dirección SP
- Incrementa SP en 1
- Combina los bytes en formato Little-Endian y los guarda en el par de registros
Timing: 3 M-Cycles (todas las instrucciones POP).
POP AF - Caso Especial (CRÍTICO)
El registro F (Flags) tiene una peculiaridad hardware: los 4 bits bajos siempre son 0. Solo los bits 7, 6, 5, 4 son válidos (Z, N, H, C respectivamente).
Cuando hacemos POP AF, debemos asegurarnos de que los 4 bits bajos del registro F se limpien explícitamente. Aunque set_af() ya aplica la máscara REGISTER_F_MASK (0xF0), lo hacemos explícito con & 0xFFF0 para mayor claridad y robustez.
Implementación: regs_->set_af(pop_word() & 0xFFF0);
¿Por qué son críticas?
Si estas instrucciones faltan o están mal implementadas:
- El juego intenta guardar el estado de los flags usando
PUSH AF. - Si no está implementada, la CPU trata ese byte como un opcode no reconocido (o NOP), pero no empuja nada a la pila.
- El juego continúa ejecutando.
- De repente se encuentra un
POP AF(que sí tenemos implementado). - Hace
POPde la pila. Pero como nunca hicimos elPUSHdelPUSH AF, sacamos basura (o underflow). - Los flags quedan con valores basura. El juego toma decisiones incorrectas basadas en flags corruptos.
- Un
RETposterior puede saltar a una dirección inválida (que se lee como0xFF), ejecutandoRST 38y entrando en un bucle infinito.
Fuente: Pan Docs - "CPU Instruction Set", "Stack Operations", "Register F (Flags)"
Implementación
Se implementaron 6 nuevas instrucciones en el método step() de CPU.cpp, justo después de las instrucciones PUSH/POP BC existentes.
PUSH DE (0xD5)
case 0xD5: // PUSH DE (Push DE onto stack)
{
uint16_t de = regs_->get_de();
push_word(de);
cycles_ += 4; // PUSH DE consume 4 M-Cycles
return 4;
}
POP DE (0xD1)
case 0xD1: // POP DE (Pop from stack into DE)
{
uint16_t value = pop_word();
regs_->set_de(value);
cycles_ += 3; // POP DE consume 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 consume 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 consume 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 consume 4 M-Cycles
return 4;
}
POP AF (0xF1) - CRÍTICO
case 0xF1: // POP AF (Pop from stack into AF)
{
// CRÍTICO: Los 4 bits bajos del registro F SIEMPRE deben ser 0
// El hardware real garantiza que estos bits nunca se pueden escribir
// Nota: set_af() ya aplica REGISTER_F_MASK (0xF0), pero lo hacemos
// explícito con & 0xFFF0 para mayor claridad y robustez
uint16_t value = pop_word();
regs_->set_af(value & 0xFFF0); // Limpiar bits bajos de F explícitamente
cycles_ += 3; // POP AF consume 3 M-Cycles
return 3;
}
Decisiones de Diseño
- Limpieza explícita de bits bajos en POP AF: Aunque
set_af()ya aplica la máscara, hacemos la limpieza explícita con& 0xFFF0para mayor claridad y robustez. Esto garantiza que los 4 bits bajos del registro F siempre sean cero, como requiere el hardware. - Organización del código: Las instrucciones se agrupan juntas después de PUSH/POP BC para mantener la coherencia y facilitar el mantenimiento.
- Timing preciso: Cada instrucción retorna el número exacto de M-Cycles según Pan Docs (4 para PUSH, 3 para POP).
Archivos Afectados
src/core/cpp/CPU.cpp- Agregadas 6 nuevas instrucciones de pila en el métodostep():- 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 (con limpieza explícita de bits bajos de F)
Tests y Verificación
Validación de módulo compilado C++: Las instrucciones se implementaron directamente en C++ y requieren recompilación.
Comando de compilación:
.\rebuild_cpp.ps1
Comando de prueba:
python main.py roms/pkmn.gb
Verificaciones esperadas:
- El bucle infinito de
RST 38(PC:0038) debería desaparecer. - El Stack Pointer debería mantenerse estable (no caer en picada).
- El juego debería avanzar más allá del bucle de espera y mostrar la intro (estrellas, Game Freak, Gengar).
- Si
PUSH AFera el culpable (casi seguro), esto estabilizará el sistema definitivamente.
Nota: Los tests unitarios completos se pueden implementar en un Step futuro, siguiendo el patrón de tests existentes.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - PUSH Instructions
- Pan Docs: CPU Instruction Set - POP Instructions
- Pan Docs: CPU Instruction Set - Register F (Flags)
- Pan Docs: CPU Instruction Set - Stack Operations
Integridad Educativa
Lo que Entiendo Ahora
- Bucle RST 38: Si el juego "descarrila" y salta a una zona vacía, lee
0xFF, ejecutaRST 38, empuja el PC a la pila, salta a0038, lee0xFFotra vez (si0038no tiene código válido), vuelve a empujar... Esto causa un Stack Overflow (el SP baja hasta dar la vuelta). - PUSH/POP AF: Pokémon usa
PUSH AFyPOP AFconstantemente para guardar y recuperar el estado de los flags. Si estas instrucciones no están implementadas, la pila se desalinea o los registros quedan con valores basura, causando saltos a direcciones inválidas. - Registro F: Los 4 bits bajos del registro F siempre deben ser cero. Al hacer
POP AF, debemos limpiar esos bits explícitamente con& 0xFFF0.
Lo que Falta Confirmar
- Validación con ROMs reales: Necesitamos ejecutar el emulador con Pokémon Red y verificar que el bucle de
RST 38desaparece y que el juego avanza correctamente. - Tests unitarios: Implementar tests unitarios completos que validen el comportamiento de PUSH/POP para todos los pares de registros, especialmente el caso especial de POP AF.
Hipótesis y Suposiciones
Asumimos que la falta de PUSH/POP AF era la causa principal del bucle infinito de RST 38. Si el problema persiste después de este Step, necesitaremos investigar otras causas posibles (como otras instrucciones faltantes o problemas en la gestión de memoria).
Próximos Pasos
- [ ] Recompilar el módulo C++ con
.\rebuild_cpp.ps1 - [ ] Ejecutar el emulador con Pokémon Red y verificar que el bucle de
RST 38desaparece - [ ] Verificar que el Stack Pointer se mantiene estable (no cae en picada)
- [ ] Verificar que el juego avanza más allá del bucle de espera y muestra la intro (estrellas, Game Freak, Gengar)
- [ ] Si el problema persiste, investigar otras causas posibles (otras instrucciones faltantes, problemas en gestión de memoria, etc.)
- [ ] Implementar tests unitarios completos para las nuevas instrucciones (opcional, puede ser un Step futuro)