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.
Arquitectura de HALT (Fase 2): El Despertador de Interrupciones
Resumen
El emulador se estaba bloqueando debido a una implementación incompleta de la lógica de HALT en el bucle principal. Aunque la CPU entraba correctamente en estado de bajo consumo, nuestro orquestador de Python no le daba la oportunidad de despertar con las interrupciones, creando un deadlock en el que el tiempo avanzaba pero la CPU permanecía dormida eternamente. Este Step corrige el bucle principal para que, mientras la CPU está en HALT, siga llamando a cpu.step() en cada ciclo de tiempo, permitiendo que el mecanismo de interrupciones interno de la CPU la despierte.
Concepto de Hardware: Despertando de HALT
Una CPU en estado HALT no está muerta, está en espera. Sigue conectada al bus de interrupciones. El hardware real funciona así:
- La CPU ejecuta
HALT. El PC deja de avanzar. - El resto del sistema (PPU, Timer) sigue funcionando.
- La PPU llega a V-Blank y levanta una bandera en el registro
IF(Interrupt Flag). - En el siguiente ciclo de reloj, la CPU comprueba sus pines de interrupción. Detecta que hay una interrupción pendiente (
(IE & IF) != 0). - La CPU se despierta (
halted = false), y siIMEestá activo, procesa la interrupción. Si no, simplemente continúa con la siguiente instrucción después deHALT.
El Problema de Nuestra Implementación Anterior
En el Step 0172, implementamos un "avance rápido" cuando la CPU entraba en HALT. El código detectaba que m_cycles == -1 y avanzaba el tiempo hasta el final de la scanline. Sin embargo, había un problema crítico:
if m_cycles == -1:
# Avanzar tiempo hasta el final de la scanline
remaining_cycles = CYCLES_PER_SCANLINE - cycles_this_scanline
cycles_this_scanline += remaining_cycles
# ❌ PROBLEMA: No volvemos a llamar a cpu.step()
En la siguiente iteración del bucle de scanline, la CPU sigue en estado halted. Nuestro código no vuelve a llamar a cpu.step() para darle la oportunidad de "despertar". Simplemente vuelve a ver que está en HALT y avanza el tiempo de nuevo. La CPU se queda dormida para siempre. Nunca ejecuta handle_interrupts() que es el único mecanismo que puede despertarla.
La Analogía: Hemos puesto al trabajador a dormir, y en lugar de ponerle un despertador (la interrupción), simplemente adelantamos el reloj de la pared una y otra vez mientras él sigue en la cama.
Fuente: Pan Docs - HALT behavior, Interrupts, System Clock
Implementación
La corrección es simple pero crítica: siempre debemos llamar a cpu.step(), incluso cuando la CPU está en HALT. El método step() internamente llama a handle_interrupts(), que es el único mecanismo que puede despertar la CPU.
A. Corregir el Bucle Principal en viboy.py
Reemplazamos la lógica dentro del bucle de scanline para que siempre llame a cpu.step() pero maneje el tiempo de forma diferente si está en HALT:
while cycles_this_scanline < CYCLES_PER_SCANLINE:
# Siempre llamamos a step() para que la CPU pueda procesar interrupciones y despertar.
m_cycles = self._cpu.step()
# Verificar si la CPU está en HALT usando el flag (no el código de retorno)
if self._use_cpp:
is_halted = self._cpu.get_halted()
else:
is_halted = self._cpu.halted
if is_halted:
# Si la CPU está en HALT, no consumió ciclos de instrucción,
# pero el tiempo debe avanzar. Avanzamos en la unidad mínima
# de tiempo (1 M-Cycle = 4 T-Cycles).
# cpu.step() ya se ha encargado de comprobar si debe despertar.
t_cycles = 4
else:
# Si no está en HALT, la instrucción consumió ciclos reales.
t_cycles = m_cycles * 4
cycles_this_scanline += t_cycles
Cambios clave:
- Eliminamos la lógica de
m_cycles == -1. Ya no es necesaria. - Siempre llamamos a
cpu.step()en cada iteración del bucle. - Usamos el flag
cpu.halted(ocpu.get_halted()en C++) para determinar cómo manejar el tiempo. - Si está en
HALT, avanzamos 4 T-Cycles (1 M-Cycle) por iteración, permitiendo quehandle_interrupts()se ejecute en cada ciclo.
B. Actualizar el Código C++ para Consistencia
Modificamos CPU::step() para que devuelva 1 en lugar de -1 cuando está en HALT, ya que ahora usamos el flag halted_ directamente:
// ========== FASE 2: Gestión de HALT ==========
// Si la CPU está en HALT, no ejecutar instrucciones
// Consumimos 1 M-Cycle (el reloj sigue funcionando) y retornamos 1.
// El orquestador debe usar el flag halted_ (get_halted()) para determinar
// cómo manejar el tiempo, no el código de retorno.
if (halted_) {
cycles_ += 1;
return 1; // HALT consume 1 M-Cycle por tick (espera activa)
}
También actualizamos el caso del opcode 0x76 (HALT) para que devuelva 1 en lugar de -1.
Componentes Modificados
src/viboy.py: Corregido el bucle principal enrun()para que siempre llame acpu.step()y use el flaghaltedpara manejar el tiempo.src/core/cpp/CPU.cpp: Actualizado para devolver1en lugar de-1cuando está enHALT.
Archivos Afectados
src/viboy.py- Corregido el bucle principal para manejar HALT correctamentesrc/core/cpp/CPU.cpp- Actualizado para devolver 1 en lugar de -1 en HALTtests/test_emulator_halt_wakeup.py- Nuevo test de integración para validar el ciclo completo de HALT
Tests y Verificación
Se creó un nuevo test de integración que verifica el ciclo completo: HALT → Interrupción → Despertar.
Test de Integración: test_halt_wakeup_integration
Este test valida que:
- La CPU ejecuta
HALTy entra en estado de bajo consumo. - La PPU genera una interrupción V-Blank.
- La CPU se despierta del estado
HALTcuando detecta la interrupción.
def test_halt_wakeup_integration():
"""
Step 0173: Test de integración que verifica el ciclo completo:
1. CPU ejecuta HALT.
2. PPU genera una interrupción V-Blank.
3. La CPU se despierta del estado HALT.
"""
# Inicializar emulador
viboy = Viboy(rom_path=None, use_cpp_core=True)
cpu = viboy.get_cpu()
mmu = viboy.get_mmu()
# Configurar interrupciones
mmu.write(0xFFFF, 0x01) # Habilitar V-Blank
cpu.ime = True
# Escribir programa: HALT
mmu.write(0x0100, 0x76) # HALT
regs = viboy.registers
regs.pc = 0x0100
# Ejecutar HALT
viboy.tick()
assert cpu.get_halted() == 1, "CPU debe estar en HALT"
# Simular ejecución hasta V-Blank
for _ in range(CYCLES_PER_FRAME):
viboy.tick()
if cpu.get_halted() == 0:
break
# Verificar que la CPU se despertó
assert cpu.get_halted() == 0, "CPU debe haberse despertado"
Comando ejecutado: pytest tests/test_emulator_halt_wakeup.py -v
Resultado esperado: Todos los tests pasan, validando que el ciclo completo de HALT funciona correctamente.
Validación Nativa
Este test valida el módulo compilado C++ y la integración completa entre CPU, MMU, PPU y el orquestador de Python.
Fuentes Consultadas
Integridad Educativa
Lo que Entiendo Ahora
- HALT no es "muerte": La CPU en HALT sigue conectada al bus de interrupciones y debe comprobar interrupciones en cada ciclo de reloj.
- El orquestador es crítico: El bucle principal debe siempre llamar a
cpu.step(), incluso cuando la CPU está en HALT, para permitir quehandle_interrupts()se ejecute. - El flag vs. el código de retorno: Es mejor usar el flag
halteddirectamente en lugar de códigos de retorno especiales, ya que es más explícito y menos propenso a errores.
Lo que Falta Confirmar
- Rendimiento: Verificar que el nuevo bucle no introduce overhead significativo cuando la CPU está en HALT.
- Comportamiento con múltiples interrupciones: Validar que el despertar funciona correctamente cuando hay múltiples interrupciones pendientes.
Hipótesis y Suposiciones
Asumimos que el comportamiento de handle_interrupts() en el código C++ es correcto y que despierta la CPU cuando hay interrupciones pendientes, incluso si IME está desactivado. Esto está basado en la documentación de Pan Docs, pero debe validarse con tests adicionales.
Próximos Pasos
- [ ] Ejecutar el emulador con una ROM real (ej:
tetris.gb) y verificar que el logo de Nintendo aparece correctamente - [ ] Validar que el rendimiento no se degrada con el nuevo bucle de HALT
- [ ] Añadir tests adicionales para casos edge (múltiples interrupciones, IME desactivado, etc.)