⚠️ Clean-Room / Educativo

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)

Fecha: 2025-12-23 Step ID: 0270 Estado: Draft

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:

  1. Decrementa SP en 1 (la pila crece hacia abajo)
  2. Escribe el byte alto (MSB) en la dirección SP
  3. Decrementa SP en 1
  4. 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:

  1. Lee el byte bajo (LSB) de la dirección SP
  2. Incrementa SP en 1
  3. Lee el byte alto (MSB) de la dirección SP
  4. Incrementa SP en 1
  5. 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:

  1. El juego intenta guardar el estado de los flags usando PUSH AF.
  2. Si no está implementada, la CPU trata ese byte como un opcode no reconocido (o NOP), pero no empuja nada a la pila.
  3. El juego continúa ejecutando.
  4. De repente se encuentra un POP AF (que sí tenemos implementado).
  5. Hace POP de la pila. Pero como nunca hicimos el PUSH del PUSH AF, sacamos basura (o underflow).
  6. Los flags quedan con valores basura. El juego toma decisiones incorrectas basadas en flags corruptos.
  7. Un RET posterior puede saltar a una dirección inválida (que se lee como 0xFF), ejecutando RST 38 y 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 & 0xFFF0 para 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étodo step():
    • 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 AF era 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

Integridad Educativa

Lo que Entiendo Ahora

  • Bucle RST 38: Si el juego "descarrila" y salta a una zona vacía, lee 0xFF, ejecuta RST 38, empuja el PC a la pila, salta a 0038, lee 0xFF otra vez (si 0038 no 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 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.
  • 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 38 desaparece 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 38 desaparece
  • [ ] 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)