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

Arquitectura de HALT (Fase 2): El Despertador de Interrupciones

Fecha: 2025-12-20 Step ID: 0173 Estado: ✅ VERIFIED

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í:

  1. La CPU ejecuta HALT. El PC deja de avanzar.
  2. El resto del sistema (PPU, Timer) sigue funcionando.
  3. La PPU llega a V-Blank y levanta una bandera en el registro IF (Interrupt Flag).
  4. En el siguiente ciclo de reloj, la CPU comprueba sus pines de interrupción. Detecta que hay una interrupción pendiente ((IE & IF) != 0).
  5. La CPU se despierta (halted = false), y si IME está activo, procesa la interrupción. Si no, simplemente continúa con la siguiente instrucción después de HALT.

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 (o cpu.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 que handle_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 en run() para que siempre llame a cpu.step() y use el flag halted para manejar el tiempo.
  • src/core/cpp/CPU.cpp: Actualizado para devolver 1 en lugar de -1 cuando está en HALT.

Archivos Afectados

  • src/viboy.py - Corregido el bucle principal para manejar HALT correctamente
  • src/core/cpp/CPU.cpp - Actualizado para devolver 1 en lugar de -1 en HALT
  • tests/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:

  1. La CPU ejecuta HALT y entra en estado de bajo consumo.
  2. La PPU genera una interrupción V-Blank.
  3. La CPU se despierta del estado HALT cuando 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 que handle_interrupts() se ejecute.
  • El flag vs. el código de retorno: Es mejor usar el flag halted directamente 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.)