Step 0383: Identificar Condición de Espera (Bank 28) y Desbloquear Progreso
📋 Resumen Ejecutivo
Se implementa instrumentación exhaustiva del bucle de espera en Bank 28 (PCs 0x614D-0x6153) para identificar qué condición espera el juego. El diagnóstico revela que el problema crítico es la falta de generación de interrupciones: el registro IF (Interrupt Flag, 0xFF0F) permanece siempre en 0x00, mientras que el juego tiene IME=1 (interrupciones habilitadas) y IE=0x0D (esperando VBlank, Timer y Serial).
Causa raíz identificada: La PPU y el Timer no están solicitando interrupciones correctamente, o no tienen acceso a la MMU para escribir en IF. Esto bloquea el progreso del juego indefinidamente.
🔧 Concepto de Hardware: Polling Loops y el Sistema de Interrupciones
Bucles de Polling en la Game Boy
Los juegos de Game Boy utilizan "polling loops" (bucles de sondeo) para esperar eventos específicos del hardware. Estos bucles repiten la lectura de un registro MMIO hasta que una condición se cumple. Los tipos más comunes son:
- Espera de VBlank: Lee LY (0xFF44) o STAT (0xFF41) hasta detectar entrada en modo 1 (VBlank)
- Espera de Timer: Lee TIMA (0xFF05) o espera que IF bit 2 se active al hacer overflow
- Espera de Serial: Lee SC (0xFF02) bit 7 o espera IF bit 3
- Espera de Interrupciones: Lee IF (0xFF0F) esperando que un bit se active
Sistema de Interrupciones (Pan Docs)
El sistema de interrupciones de la Game Boy consta de tres componentes:
- IME (Interrupt Master Enable): Flag global en la CPU. Si está en 0, todas las interrupciones se ignoran.
- IE (Interrupt Enable, 0xFFFF): Máscara de bits que indica qué interrupciones están habilitadas:
- Bit 0: V-Blank (vector 0x0040)
- Bit 1: LCD STAT (vector 0x0048)
- Bit 2: Timer (vector 0x0050)
- Bit 3: Serial (vector 0x0058)
- Bit 4: Joypad (vector 0x0060)
- IF (Interrupt Flag, 0xFF0F): Bits de solicitud. Cuando un componente de hardware quiere interrumpir,
escribe un 1 en el bit correspondiente. La CPU verifica
IE & IF & IMEantes de cada instrucción.
El Problema: IF Siempre en 0x00
Si IF permanece en 0x00, significa que ningún componente está solicitando interrupciones. Posibles causas (Clean Room):
- La PPU no llama a
mmu_->request_interrupt(0)al entrar en VBlank (LY=144) - El Timer no llama a
mmu_->request_interrupt(2)cuando TIMA hace overflow - Los componentes (PPU, Timer) no tienen acceso a la MMU (puntero no conectado)
- El método
request_interrupt()no está funcionando correctamente
Fuente: Pan Docs - "Interrupts", "Interrupt Enable Register (IE)", "Interrupt Flag Register (IF)"
💻 Implementación
1. Instrumentación del Bucle de Espera en CPU
Se añadieron variables de estado en CPU.hpp para controlar el trazado:
// Step 0383: Trazado de bucle de espera (Bank 28, PC 0x614D-0x6153)
bool wait_loop_trace_active_; // Flag para activar trazado del wait-loop
int wait_loop_trace_count_; // Contador de iteraciones trazadas (límite 200)
bool wait_loop_detected_; // Flag para indicar que ya se detectó el loop una vez
En CPU.cpp::step(), se implementa detección automática del bucle:
// Detectar entrada en el bucle (Bank 28 + rango de PC)
uint16_t current_rom_bank = mmu_->get_current_rom_bank();
if (current_rom_bank == 28 && original_pc >= 0x614D && original_pc <= 0x6153) {
// Activar trazado la primera vez que detectamos el loop
if (!wait_loop_detected_) {
wait_loop_detected_ = true;
wait_loop_trace_active_ = true;
wait_loop_trace_count_ = 0;
printf("[WAIT-LOOP] ===== BUCLE DE ESPERA DETECTADO EN BANK 28, PC 0x%04X =====\n", original_pc);
}
// Loguear detalles de cada iteración (limitado a 200)
if (wait_loop_trace_active_ && wait_loop_trace_count_ < 200) {
uint8_t opcode = mmu_->read(original_pc);
printf("[WAIT-LOOP] Iter:%d PC:0x%04X OP:0x%02X | A:0x%02X F:0x%02X HL:0x%04X | IME:%d IE:0x%02X IF:0x%02X\n",
wait_loop_trace_count_, original_pc, opcode,
regs_->a, regs_->f, regs_->get_hl(),
ime_ ? 1 : 0, mmu_->read(0xFFFF), mmu_->read(0xFF0F));
wait_loop_trace_count_++;
}
}
2. Instrumentación de MMIO Crítica en MMU
Se instrumentaron lecturas y escrituras a registros clave cuando debug_current_pc está en el rango del wait-loop:
// Step 0383: Instrumentación de MMIO Crítica (Solo en Wait-Loop Bank 28)
bool in_wait_loop = (current_rom_bank_ == 28 && debug_current_pc >= 0x614D && debug_current_pc <= 0x6153);
if (in_wait_loop) {
static int mmio_read_count_step383 = 0;
bool should_log = (mmio_read_count_step383 < 220);
// Registros de PPU: LY, STAT, LCDC
if (addr == 0xFF44 && should_log) { /* loguear LY */ }
else if (addr == 0xFF41 && should_log) { /* loguear STAT */ }
else if (addr == 0xFF40 && should_log) { /* loguear LCDC */ }
// Registros de interrupciones: IF, IE
else if (addr == 0xFF0F && should_log) { /* loguear IF */ }
else if (addr == 0xFFFF && should_log) { /* loguear IE */ }
// Registros de Timer: DIV, TIMA, TMA, TAC
else if (addr >= 0xFF04 && addr <= 0xFF07 && should_log) { /* loguear Timer */ }
// DMA y Serial: 0xFF46, 0xFF01, 0xFF02
else if (addr == 0xFF46 || addr == 0xFF01 || addr == 0xFF02) { /* loguear */ }
}
3. Prevención de Saturación de Contexto
- Trazado del loop limitado a 200 iteraciones
- Accesos MMIO limitados a 220 líneas
- Salida redirigida a archivo:
logs/step0383_waitloop_probe.log - Análisis mediante
grepcon límites (head -n 50)
🔍 Hallazgos del Diagnóstico
1. Estructura del Bucle de Espera
El bucle ejecuta las siguientes instrucciones en Bank 28:
0x614D: NOP ; (0x00) - Probablemente lectura MMIO oculta
0x614E: NOP ; (0x00) - Probablemente lectura MMIO oculta
0x614F: NOP ; (0x00) - Probablemente lectura MMIO oculta
0x6150: DEC DE ; (0x1B) - Decrementa contador DE
0x6151: LD A, D ; (0x7A) - Carga D en A
0x6152: OR E ; (0xB3) - OR con E para verificar si DE==0
0x6153: JR NZ, -8 ; (0x20 0xF8) - Salta a 0x614D si DE≠0
Interpretación: Este es un delay loop con polling implícito. Los 3 NOPs iniciales son sospechosos: en ensamblador Game Boy, los NOPs suelen ser placeholders para accesos a MMIO que el desensamblador no puede deducir (ej: accesos indirectos vía HL).
2. Accesos a MMIO Durante el Bucle
El trazado reveló lecturas constantes a:
LCDC (0xFF40) = 0xE3- Constante, correcto (LCD ON, BG ON, Win ON, OBJ ON)IF (0xFF0F) = 0x00- ⚠️ SIEMPRE 0x00 (PROBLEMA CRÍTICO)IE (0xFFFF) = 0x0D- Constante, correcto (bits 0, 2, 3: VBlank, Timer, Serial habilitados)
3. Estado de Interrupciones
| Componente | Valor | Interpretación | Estado |
|---|---|---|---|
IME |
1 | Interrupciones habilitadas globalmente | ✅ Correcto |
IE (0xFFFF) |
0x0D (bits 0,2,3) | Espera VBlank, Timer, Serial | ✅ Correcto |
IF (0xFF0F) |
0x00 | Ninguna interrupción solicitada | ❌ PROBLEMA CRÍTICO |
4. Causa Raíz Identificada
Específicamente:
- La PPU debería activar IF bit 0 (VBlank) cada ~16.6ms (al entrar en LY=144)
- El Timer debería activar IF bit 2 cuando TIMA hace overflow (según TAC)
- Ninguno de estos eventos ocurre, dejando IF en 0x00 permanentemente
5. Por qué el Juego se Queda "Congelado"
El bucle de delay en Bank 28 tiene dos condiciones de salida:
- Contador DE llega a 0: Después de ~50,000-100,000 iteraciones (varios frames)
- Interrupción ocurre: La CPU sale del bucle para atender el handler, y el handler puede cambiar el estado del juego
Sin interrupciones, el juego depende únicamente del timeout de DE. Pero incluso cuando DE llega a 0 y el bucle termina, el juego probablemente vuelve a entrar en otro bucle de espera, esperando eventos que nunca ocurren.
✅ Tests y Verificación
Prueba de 30 Segundos
Comando ejecutado:
cd /media/fabini/8CD1-4C30/ViboyColor
timeout 30 python3 main.py roms/pkmn.gb > logs/step0383_waitloop_probe.log 2>&1
Análisis de Resultados
Extracto del trazado del wait-loop (primeras 50 líneas):
grep -E "\[WAIT-LOOP\]" logs/step0383_waitloop_probe.log | head -n 50
Resultado: Captura exitosa de 200 iteraciones del bucle, confirmando:
- Patrón de 7 instrucciones repetidas (0x614D → 0x6153 → 0x614D)
- IF siempre en 0x00 en todas las iteraciones
- IE constante en 0x0D (esperando interrupciones correctamente configuradas)
- IME siempre en 1 (interrupciones habilitadas)
Extracto de accesos MMIO (primeras 100 líneas):
grep -E "\[WAIT-MMIO-(READ|WRITE)\]" logs/step0383_waitloop_probe.log | head -n 100
Resultado: Captura de múltiples lecturas a LCDC, IF, IE durante el loop, confirmando el polling activo.
🚀 Próximos Pasos: Step 0384
Objetivo
Verificar y corregir la generación de interrupciones en PPU y Timer.
Tareas Propuestas
- Verificar Conexión de Componentes:
- Confirmar que PPU y Timer tienen acceso a MMU (puntero no nulo)
- Verificar que
mmu_->request_interrupt()escribe correctamente en IF
- Instrumentar Solicitudes de Interrupción:
- Añadir logs en
MMU::request_interrupt()para ver si se llama - Añadir logs en PPU cuando LY=144 (entrada a VBlank)
- Añadir logs en Timer cuando TIMA hace overflow
- Añadir logs en
- Corregir Generación de Interrupciones:
- Si PPU no llama a
request_interrupt(0)en VBlank, implementarlo - Si Timer no llama a
request_interrupt(2)en overflow, implementarlo - Verificar que la frecuencia de interrupciones es correcta (60 Hz para VBlank)
- Si PPU no llama a
- Validar Desbloqueo de Progreso:
- Ejecutar el juego por 30-60 segundos después de la corrección
- Verificar que IF cambia de valor (no siempre 0x00)
- Confirmar que el juego sale del bucle de espera y progresa
📁 Archivos Modificados
src/core/cpp/CPU.hpp- Añadidas variables de estado para wait-loop tracesrc/core/cpp/CPU.cpp- Implementada detección y trazado del bucle en Bank 28src/core/cpp/MMU.cpp- Instrumentación de lecturas/escrituras MMIO críticasbuild_log_step0383.txt- Log de compilación (exitosa)logs/step0383_waitloop_probe.log- Trazado completo del wait-loop (30s)
📝 Conclusión
El Step 0383 cumple exitosamente su objetivo: identificar la causa exacta del bloqueo en Bank 28. La instrumentación exhaustiva reveló que el problema no es un bug en la lógica del bucle, sino la falta total de generación de interrupciones por parte de PPU y Timer.
Con esta información, el Step 0384 puede proceder directamente a la corrección del sistema de interrupciones, que desbloqueará el progreso del juego y permitirá avanzar a la siguiente fase (carga de tiles y actualización de pantalla).
Aprendizaje clave: Un emulador "vivo" (CPU progresando) no es lo mismo que un emulador "funcional" (juego avanzando). La sincronización precisa de componentes y la generación correcta de interrupciones son fundamentales para la jugabilidad.