⚠️ 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.

Misc Instructions Implementation (DAA, CPL, SCF, CCF)

Fecha: 2025-12-24 Step ID: 0271 Estado: Draft

Resumen

Este Step implementa las instrucciones misceláneas del bloque bajo (DAA 0x27, CPL 0x2F, SCF 0x37, CCF 0x3F), los loads básicos ausentes (LD (BC/DE), A, LD A, (BC/DE), LDI/LD A, (HL±), LDH (C), A, LDH A, (C), LD (nn), A, LD A, (nn)), añade RETI (0xD9) y corrige el HALT bug (IME=0 con IF&IE≠0 no se detiene). El diagnóstico del Step 0270 reveló el bucle infinito de RST 38; con estas instrucciones el PC ya no cae en 0x0038 ni desborda la pila.

La causa raíz era la desincronización del PC debido a instrucciones faltantes. Si falta una instrucción crítica como DAA (Decimal Adjust Accumulator) o un load de memoria alta (por ejemplo LDH (C), A para los registros de E/S), el juego calcula direcciones de memoria erróneas y crashea. Pokémon usa aritmética BCD (Binary Coded Decimal) intensivamente para la salud, el dinero y los puntos, y accede constantemente a registros en 0xFF00. Si estas instrucciones no están implementadas, los cálculos salen mal, el juego hace JP HL a una dirección equivocada, aterriza en una zona vacía de memoria (llena de 0xFF), y entra en un bucle infinito de RST 38.

Se implementaron 15 instrucciones: 4 misceláneas, 10 loads críticos y RETI; además se corrigió el HALT bug.

Concepto de Hardware

Las instrucciones misceláneas son operaciones especiales que no encajan en las categorías estándar de Load, Aritmética, Control de Flujo, etc. Son "raras" pero vitales para el funcionamiento correcto de muchos juegos.

DAA (Decimal Adjust Accumulator) - 0x27

DAA es la instrucción más compleja de emular correctamente. Ajusta el registro A para que sea un número BCD (Binary Coded Decimal) válido tras una suma o resta.

¿Qué es BCD? BCD es un sistema de codificación donde cada dígito decimal (0-9) se representa con 4 bits. Por ejemplo, el número decimal 45 se representa como 0100 0101 (4 en el nibble alto, 5 en el nibble bajo).

¿Por qué es necesario DAA? Cuando sumas dos números BCD, el resultado puede no ser un BCD válido. Por ejemplo, si sumas 0x09 + 0x01 = 0x0A, el nibble bajo es 0xA (10 en decimal), que no es un dígito BCD válido. DAA ajusta el resultado sumando 0x06 al nibble bajo si es mayor que 9, o al nibble alto si es mayor que 0x99.

Lógica de DAA:

  • Después de suma (N=0):
    • Si C=1 o A > 0x99: suma 0x60 y activa C
    • Si H=1 o (A & 0x0F) > 0x09: suma 0x06
  • Después de resta (N=1):
    • Si C=1: resta 0x60
    • Si H=1: resta 0x06

Flags: Z según resultado, N (preservado), H=0 (siempre), C (actualizado si hubo overflow).

Timing: 1 M-Cycle (4 cycles).

CPL (Complement A) - 0x2F

CPL invierte todos los bits del registro A (A = ~A). Es equivalente a hacer un NOT lógico bit a bit.

Ejemplo: Si A = 0b10101010, después de CPL, A = 0b01010101.

Flags: Z (preservado), N=1, H=1, C (preservado).

Timing: 1 M-Cycle (4 cycles).

SCF (Set Carry Flag) - 0x37

SCF activa el flag Carry (C = 1). Es útil para inicializar el carry antes de operaciones ADC (Add with Carry) o SBC (Subtract with Carry).

Flags: Z (preservado), N=0, H=0, C=1.

Timing: 1 M-Cycle (4 cycles).

CCF (Complement Carry Flag) - 0x3F

CCF invierte el flag Carry (C = !C). Es útil para cambiar el estado del carry sin conocer su valor actual.

Flags: Z (preservado), N=0, H=0, C=!C.

Timing: 1 M-Cycle (4 cycles).

¿Por qué son críticas?

Si estas instrucciones faltan o están mal implementadas:

  1. El juego intenta hacer cálculos BCD usando DAA.
  2. Si no está implementada, la CPU trata ese byte como un opcode no reconocido (o NOP), pero no ajusta A.
  3. El juego continúa ejecutando con valores incorrectos en A.
  4. Los cálculos de direcciones de memoria salen mal (por ejemplo, JP HL salta a una dirección equivocada).
  5. El juego aterriza en una zona vacía de memoria (llena de 0xFF).
  6. Lee 0xFF, ejecuta RST 38, empuja el PC a la pila, salta a 0038, lee 0xFF otra vez... Bucle infinito de RST 38.

Fuente: Pan Docs - "CPU Instruction Set", "DAA Instruction", "CPL Instruction", "SCF Instruction", "CCF Instruction"

Implementación

Se implementaron 15 instrucciones en el método step() de CPU.cpp: las 4 misceláneas (DAA, CPL, SCF, CCF), 10 loads que estaban ausentes (BC/DE directos, LDI/LDD A, HL±, LDH (C), A / LDH A, (C), LD (nn), A y LD A, (nn)) y RETI. Se añadió también el fix del HALT bug (IME=0 con interrupción pendiente no se detiene).

DAA (0x27)

case 0x27:  // DAA (Decimal Adjust Accumulator)
{
    uint16_t a = regs_->a;
    bool n = regs_->get_flag_n();
    bool h = regs_->get_flag_h();
    bool c = regs_->get_flag_c();
    
    if (!n) {  // Después de suma
        if (c || a > 0x99) {
            a += 0x60;
            regs_->set_flag_c(true);
        }
        if (h || (a & 0x0F) > 0x09) {
            a += 0x06;
        }
    } else {  // Después de resta
        if (c) {
            a -= 0x60;
        }
        if (h) {
            a -= 0x06;
        }
    }
    
    regs_->a = static_cast<uint8_t>(a);
    regs_->set_flag_z(regs_->a == 0);
    regs_->set_flag_h(false);  // H siempre se limpia en DAA
    // C se mantiene o se setea si hubo overflow en el ajuste (ya se actualizó arriba)
    
    cycles_ += 1;  // DAA consume 1 M-Cycle
    return 1;
}

CPL (0x2F)

case 0x2F:  // CPL (Complement A)
{
    regs_->a = ~regs_->a;
    // Flags: Z (preservado), N=1, H=1, C (preservado)
    regs_->set_flag_n(true);
    regs_->set_flag_h(true);
    // Z y C no se modifican
    cycles_ += 1;  // CPL consume 1 M-Cycle
    return 1;
}

SCF (0x37)

case 0x37:  // SCF (Set Carry Flag)
{
    // Flags: Z (preservado), N=0, H=0, C=1
    regs_->set_flag_n(false);
    regs_->set_flag_h(false);
    regs_->set_flag_c(true);
    // Z no se modifica
    cycles_ += 1;  // SCF consume 1 M-Cycle
    return 1;
}

CCF (0x3F)

case 0x3F:  // CCF (Complement Carry Flag)
{
    // Flags: Z (preservado), N=0, H=0, C=!C
    regs_->set_flag_n(false);
    regs_->set_flag_h(false);
    regs_->set_flag_c(!regs_->get_flag_c());
    // Z no se modifica
    cycles_ += 1;  // CCF consume 1 M-Cycle
    return 1;
}

Loads 8-bit críticos

Incorporados para habilitar accesos tempranos a registros y memoria alta:

  • LD (BC), A / LD (DE), A y LD A, (BC) / LD A, (DE) (2 M-Cycles).
  • LDI/LD A, (HL±) (incremento/decremento automático de HL).
  • LDH (C), A y LDH A, (C) para 0xFF00 + C (registros de E/S), 2 M-Cycles.
  • LD (nn), A y LD A, (nn) para direcciones absolutas de 16 bits, 4 M-Cycles.

Decisiones de Diseño

  • Implementación de DAA: La lógica de DAA es compleja y depende del flag N (si fue suma o resta) y de los flags H y C. Se implementó siguiendo exactamente la especificación de Pan Docs para garantizar compatibilidad con juegos que usan BCD.
  • Preservación de flags: CPL, SCF y CCF preservan el flag Z, lo cual es crítico para mantener el estado correcto de las comparaciones anteriores.
  • Organización del código: Las instrucciones se insertaron en los lugares apropiados del switch según sus opcodes para mantener la coherencia y facilitar el mantenimiento.
  • Timing preciso: Todas las instrucciones consumen 1 M-Cycle (4 cycles) según Pan Docs.
  • HALT bug: Si IME=0 y existe una interrupción pendiente (IE & IF != 0), HALT no detiene la CPU (se comporta como NOP) para evitar loops con IME desactivado.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Agregadas 15 instrucciones al método step() y fix de HALT bug:
    • Misceláneas: DAA (0x27), CPL (0x2F), SCF (0x37), CCF (0x3F).
    • Interrupciones: RETI (0xD9) reactiva IME al salir de la rutina.
    • Loads básicos: LD (BC), A / LD (DE), A / LD A, (BC) / LD A, (DE).
    • Loads con auto-incremento/decremento: LDI/LD A, (HL±).
    • High memory I/O: LDH (C), A y LDH A, (C).
    • Dirección absoluta: LD (nn), A y LD A, (nn).

Tests y Verificación

Validación de módulo compilado C++: Las instrucciones se implementaron directamente en C++ y requieren recompilación.

Comandos ejecutados:

.\rebuild_cpp.ps1
python main.py roms/pkmn.gb
python main.py roms/pkmn.gb --verbose

Resultados:

  • El bucle de RST 38 desapareció; SP estable, sin desbordes.
  • PC oscila en 0x614D–0x6151 con IME activo; interrupciones atendidas (RETI implementado).
  • Pantalla aún verde: flujo en rutina 0x613C–0x6153; falta inspeccionar HRAM/WRAM (p.ej. 0xD732) para destrabar.

Validación de módulo compilado C++: Sí (rebuild_cpp.ps1).

Pendiente: Tests unitarios para DAA y loads/LDH.

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).
  • DAA y BCD: Pokémon usa aritmética BCD intensivamente para la salud, el dinero y los puntos. Si DAA no está implementada, los cálculos salen mal, el juego hace JP HL a una dirección equivocada, y aterriza en una zona vacía de memoria (llena de 0xFF).
  • Desincronización del PC: Cuando falta una instrucción, la CPU puede "descarrilarse" (desincronizarse del flujo de instrucciones correcto). Esto ocurre cuando el juego espera que una instrucción haga algo específico (como ajustar A para BCD), pero como no está implementada, actúa como NOP, causando que los cálculos posteriores salgan mal.

Lo que Falta Confirmar

  • HRAM/WRAM en el bucle 0x613C–0x6153: Inspeccionar bytes como 0xD732 para confirmar la condición de salida del bucle que mantiene la pantalla verde.
  • Tests unitarios: Implementar tests para DAA con combinaciones de flags/valores y para loads/LDH.

Hipótesis y Suposiciones

Asumimos que la falta de DAA, loads y RETI explicaba el bucle RST 38. Ahora el PC progresa pero la pantalla sigue verde; la hipótesis es un flag en HRAM/WRAM que no cumple la condición de salida. Próximo paso: leer 0xD732 y confirmar la lógica del handler de VBlank/STAT.

Próximos Pasos

  • [x] Recompilar el módulo C++ con .\rebuild_cpp.ps1
  • [x] Ejecutar el emulador con Pokémon Red y confirmar que el bucle de RST 38 desaparece
  • [ ] Instrumentar/leer HRAM/WRAM (p.ej. 0xD732) en el bucle 0x614D–0x6151 para destrabar la pantalla verde
  • [ ] Verificar visualmente la intro (logo/Game Freak) tras la lectura del flag
  • [ ] Implementar tests unitarios completos para DAA y loads/LDH