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

Fix: Timing de Interrupciones y Retraso de EI

Fecha: 2025-12-18 Step ID: 0091 Estado: Verified

Resumen

Corrección crítica del timing de interrupciones en la CPU para resolver cuelgues en juegos (Tetris, Pokémon, Tetris DX) y problemas de controles no responsivos. Se implementó el retraso de 1 instrucción de EI (Enable Interrupts) y se corrigió el orden de comprobación de interrupciones en el ciclo de instrucción. Las interrupciones ahora se comprueban correctamente antes de cada instrucción, y EI activa IME después de la siguiente instrucción, como en el hardware real.

Concepto de Hardware

En el hardware real de la Game Boy, el manejo de interrupciones tiene dos comportamientos críticos que deben emularse correctamente:

1. Timing de Comprobación de Interrupciones

Las interrupciones se comprueban antes de ejecutar cada instrucción, no después. Esto significa que el flujo correcto es:

  1. Comprobar si hay interrupciones pendientes (IE & IF) y si IME está activo
  2. Si hay interrupción pendiente y IME activo, saltar al vector inmediatamente (sin ejecutar la instrucción)
  3. Si no hay interrupción o IME está desactivado, ejecutar la instrucción normal

2. Retraso de 1 Instrucción de EI (Enable Interrupts)

La instrucción EI (opcode 0xFB) tiene un comportamiento especial: no activa IME inmediatamente, sino que programa su activación para después de la siguiente instrucción. Esto permite que la instrucción que sigue a EI se ejecute sin interrupciones, y luego IME se activa automáticamente.

Este comportamiento es crítico para muchos juegos que usan patrones como:

EI      ; Programa activación de IME después de la siguiente instrucción
RETI    ; Esta instrucción se ejecuta con IME aún False
        ; [Aquí IME se activa automáticamente]
        ; [En el siguiente step(), se comprueban interrupciones con IME activo]

3. HALT y Despertar por Interrupciones

Cuando la CPU está en estado HALT y hay interrupciones pendientes (en IE y IF), la CPU debe despertar incluso si IME está desactivado. Si IME está desactivado, la CPU despierta pero no salta al vector de interrupción, permitiendo que el código verifique manualmente las interrupciones mediante polling de IF.

Fuente: Pan Docs - CPU Instruction Set (EI timing), Interrupts, HALT behavior

Implementación

Se modificó el método step() de la CPU y el opcode EI para implementar el timing correcto de interrupciones:

1. Atributo ime_scheduled

Se añadió el atributo self.ime_scheduled: bool = False en el constructor de la CPU para rastrear cuando EI ha programado la activación de IME.

2. Modificación de _op_ei()

El método _op_ei() ahora programa la activación de IME en lugar de activarlo inmediatamente:

def _op_ei(self) -> int:
    # NO activar IME inmediatamente, programarlo para después de la siguiente instrucción
    self.ime_scheduled = True
    return 1

3. Modificación de step()

El método step() ahora sigue este flujo:

  1. Activar IME si está programado: Si ime_scheduled está activo, activar IME y limpiar el flag. Esto ocurre al principio del step, después de la instrucción anterior que ejecutó EI.
  2. Comprobar HALT: Si la CPU está en HALT, consumir 1 ciclo y comprobar interrupciones. Si se procesa una interrupción, retornar. Si no se procesa pero despertó (IME desactivado), continuar ejecutando la instrucción normalmente.
  3. Comprobar interrupciones: Si hay interrupción pendiente y IME activo, saltar al vector inmediatamente (sin ejecutar la instrucción).
  4. Ejecutar instrucción: Si no hay interrupción, ejecutar la instrucción normal.

Componentes modificados

  • src/cpu/core.py:
    • Añadido atributo ime_scheduled en __init__
    • Modificado _op_ei() para programar activación de IME
    • Modificado step() para activar IME al principio y comprobar interrupciones antes de ejecutar instrucción
    • Corregido manejo de HALT para que continúe ejecutando instrucción si despertó sin procesar interrupción

Archivos Afectados

  • src/cpu/core.py - Modificación del ciclo de instrucción y opcode EI

Tests y Verificación

Se ejecutaron todos los tests de interrupciones para verificar que el fix no rompió funcionalidad existente y que el comportamiento ahora es correcto:

Ejecución de Tests Unitarios

Comando ejecutado:

python -m pytest tests/test_cpu_interrupts.py -v

Entorno:

  • OS: Windows 10
  • Python: 3.13.5

Resultado:

============================= test session starts =============================
platform win32 -- Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
collected 7 items

tests/test_cpu_interrupts.py::TestCPUInterrupts::test_vblank_interrupt PASSED [ 14%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_interrupt_priority PASSED [ 28%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_halt_wakeup PASSED [ 42%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_no_interrupt_if_ime_disabled PASSED [ 57%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_timer_interrupt_vector PASSED [ 71%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_all_interrupt_vectors PASSED [ 85%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_reti_reactivates_ime PASSED [100%]

============================== 7 passed in 0.05s ==============================

Qué valida:

  • test_vblank_interrupt: Verifica que las interrupciones V-Blank se procesan correctamente cuando IME está activo, saltando al vector 0x0040, desactivando IME, limpiando IF y consumiendo 5 M-Cycles. Este test demuestra que el timing de comprobación de interrupciones es correcto (se comprueban antes de ejecutar la instrucción).
  • test_interrupt_priority: Verifica que cuando múltiples interrupciones están pendientes, se procesa primero la de mayor prioridad (V-Blank sobre Timer).
  • test_halt_wakeup: Verifica que HALT se despierta con interrupciones pendientes incluso si IME está desactivado, y que después de despertar ejecuta la instrucción normalmente (no salta al vector si IME está desactivado). Este test demuestra que el manejo de HALT es correcto y que la CPU continúa ejecutando después de despertar.
  • test_no_interrupt_if_ime_disabled: Verifica que las interrupciones no se procesan si IME está desactivado.
  • test_timer_interrupt_vector: Verifica que la interrupción Timer salta al vector correcto (0x0050).
  • test_all_interrupt_vectors: Verifica que todos los vectores de interrupción son correctos.
  • test_reti_reactivates_ime: Verifica que RETI reactiva IME correctamente.

Código de Tests Relevantes

El test test_halt_wakeup es especialmente relevante porque valida el comportamiento corregido de HALT:

def test_halt_wakeup(self) -> None:
    """
    Test: HALT se despierta con interrupciones pendientes, incluso si IME es False.
    
    Si la CPU está en HALT y hay interrupciones pendientes (en IE y IF),
    la CPU debe despertar (halted = False), pero NO saltar al vector
    si IME está desactivado. Después de despertar, la CPU continúa ejecutando
    normalmente (avanza PC con la siguiente instrucción).
    """
    mmu = MMU(None)
    cpu = CPU(mmu)
    
    cpu.registers.set_pc(0x3000)
    cpu.halted = True  # CPU en HALT
    cpu.ime = False  # IME desactivado
    
    # Habilitar V-Blank en IE y activar flag en IF
    mmu.write_byte(IO_IE, 0x01)
    mmu.write_byte(IO_IF, 0x01)
    
    # Poner un NOP en 0x3000
    mmu.write_byte(0x3000, 0x00)  # NOP
    
    # Ejecutar step
    cycles = cpu.step()
    
    # Debe despertar pero NO saltar a interrupción (IME desactivado)
    assert cpu.halted is False, "CPU debe despertar de HALT"
    # Después de despertar, ejecuta la instrucción normal (NOP)
    assert cpu.registers.get_pc() == 0x3001, "PC debe avanzar (ejecutó NOP)"
    assert cycles == 1, "Debe consumir 1 ciclo (NOP), no ciclos de interrupción"

Este test demuestra que el comportamiento corregido permite que HALT despierte y continúe ejecutando la instrucción cuando IME está desactivado, lo cual es crítico para muchos juegos que usan polling manual de interrupciones después de HALT.

Pruebas con ROMs (Pendiente)

Estado: Draft

El fix está implementado y los tests unitarios pasan, pero aún no se ha probado con ROMs reales (Tetris, Pokémon, Tetris DX) para verificar que resuelve los cuelgues reportados. Se recomienda probar con estas ROMs para validar el comportamiento en condiciones reales.

Notas legales: Las ROMs mencionadas (Tetris, Pokémon, Tetris DX) son aportadas por el usuario para pruebas locales y no se distribuyen en el repositorio.

Fuentes Consultadas

  • Pan Docs - CPU Instruction Set (EI timing behavior)
  • Pan Docs - Interrupts (timing de comprobación de interrupciones)
  • Pan Docs - HALT behavior (despertar por interrupciones)

Integridad Educativa

Lo que Entiendo Ahora

  • Timing de interrupciones: Las interrupciones se comprueban antes de ejecutar cada instrucción, no después. Esto es crítico para el comportamiento correcto del hardware.
  • Retraso de EI: La instrucción EI no activa IME inmediatamente, sino que programa su activación para después de la siguiente instrucción. Esto permite que la instrucción que sigue a EI se ejecute sin interrupciones.
  • HALT y despertar: Cuando la CPU está en HALT y hay interrupciones pendientes, la CPU debe despertar incluso si IME está desactivado. Si IME está desactivado, la CPU despierta pero no salta al vector, permitiendo polling manual.

Lo que Falta Confirmar

  • Validación con ROMs reales: Aunque los tests unitarios pasan, falta verificar que el fix resuelve los cuelgues reportados en Tetris, Pokémon y Tetris DX. Se recomienda probar con estas ROMs para validar el comportamiento en condiciones reales.
  • Comportamiento de EI con interrupciones pendientes: Si EI se ejecuta y hay interrupciones pendientes, ¿se procesan inmediatamente después de la siguiente instrucción o hay algún otro comportamiento especial? Los tests actuales no cubren este caso específico.

Hipótesis y Suposiciones

La implementación actual asume que el comportamiento del hardware es exactamente como se describe en Pan Docs: interrupciones se comprueban antes de cada instrucción, y EI activa IME después de la siguiente instrucción. Si los cuelgues persisten después de este fix, podría haber otros problemas de timing o comportamiento del hardware que no están documentados o que requieren una implementación más precisa.

Próximos Pasos

  • [ ] Probar con ROMs reales (Tetris, Pokémon, Tetris DX) para verificar que el fix resuelve los cuelgues
  • [ ] Si los cuelgues persisten, investigar otros problemas de timing (por ejemplo, timing de V-Blank, Timer, Joypad)
  • [ ] Añadir tests adicionales para casos edge de EI con interrupciones pendientes
  • [ ] Documentar cualquier comportamiento adicional descubierto durante las pruebas con ROMs