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

Despachador de Interrupciones

Fecha: 2025-12-17 Step ID: 0025 Estado: Verified

Resumen

Se implementó el Despachador de Interrupciones (Interrupt Service Routine - ISR) en la CPU, conectando finalmente el sistema de timing (PPU) con la CPU. Ahora la CPU puede responder a interrupciones como V-Blank, Timer, LCD STAT, Serial y Joypad. La implementación incluye el manejo correcto de prioridades (V-Blank tiene mayor prioridad que Timer, etc.), despertar de HALT cuando hay interrupciones pendientes, y la secuencia completa de hardware: desactivar IME, limpiar flag en IF, guardar PC en la pila, y saltar al vector. Esta es la funcionalidad que convierte el emulador de una "calculadora lineal" en un sistema reactivo capaz de responder a eventos del hardware. Suite completa de tests TDD (6 tests) validando todas las funcionalidades. Todos los tests pasan.

Concepto de Hardware

Las interrupciones son señales de hardware que permiten que la CPU interrumpa temporalmente la ejecución de código normal para atender eventos urgentes (como el final de un frame V-Blank o el desbordamiento de un timer). Sin interrupciones, la CPU tendría que estar constantemente verificando (polling) el estado de los periféricos, lo cual es ineficiente.

Flujo de Interrupción

Para que ocurra una interrupción real (salto al vector), deben cumplirse 3 condiciones simultáneas:

  1. IME (Interrupt Master Enable) debe ser True. Se controla con las instrucciones DI (desactivar) y EI (activar).
  2. El bit correspondiente en IE (Interrupt Enable, 0xFFFF) debe estar activo. Esto habilita tipos específicos de interrupciones.
  3. El bit correspondiente en IF (Interrupt Flag, 0xFF0F) debe estar activo. Los periféricos (PPU, Timer, etc.) ponen estos bits cuando ocurre un evento.

Secuencia de Hardware (cuando se acepta una interrupción)

Cuando se acepta una interrupción, el hardware ejecuta automáticamente esta secuencia:

  1. La CPU apaga IME automáticamente (para evitar interrupciones anidadas inmediatas).
  2. Limpia el bit correspondiente en IF (acknowledgement: "recibí tu mensaje").
  3. Hace PUSH PC (guarda la dirección actual para volver luego con RETI).
  4. Salta a la dirección del vector de interrupción (PC = vector).
  5. Consume 5 M-Cycles (20 T-Cycles) en total.

Vectores de Interrupción y Prioridades

Cada tipo de interrupción tiene un vector fijo y una prioridad específica (menor número de bit = mayor prioridad):

  • Bit 0: V-Blank → 0x0040 (Prioridad más alta)
  • Bit 1: LCD STAT → 0x0048
  • Bit 2: Timer → 0x0050
  • Bit 3: Serial → 0x0058
  • Bit 4: Joypad → 0x0060 (Prioridad más baja)

Si múltiples interrupciones están pendientes simultáneamente, se procesa primero la de mayor prioridad.

Despertar de HALT

Si la CPU está en HALT (estado de bajo consumo) y ocurre una interrupción pendiente (en IE y IF), la CPU debe despertar (halted = False), incluso si IME es False. Esto permite que el código pueda verificar manualmente las interrupciones mediante polling de IF después de HALT. Si IME está desactivado, la CPU despierta pero no salta al vector (continúa ejecutando normalmente).

Fuente: Pan Docs - Interrupts, HALT behavior, Interrupt Vectors

Implementación

Se implementó el método handle_interrupts() en la clase CPU que gestiona todo el flujo de interrupciones. Este método se llama al inicio de cada step(), antes de ejecutar cualquier instrucción, simulando el comportamiento del hardware real.

Componentes creados/modificados

  • CPU.handle_interrupts(): Método que implementa toda la lógica de interrupciones. Lee IE e IF desde la MMU, calcula interrupciones pendientes, maneja el despertar de HALT, procesa la interrupción de mayor prioridad si IME está activo, y retorna los ciclos consumidos (5 si procesó, 0 si no).
  • CPU.step(): Modificado para llamar a handle_interrupts() al principio. Si se procesó una interrupción, retorna inmediatamente sin ejecutar la instrucción normal.

Decisiones de diseño

Prioridad de interrupciones: Se implementó usando una serie de if-elif que comprueba los bits en orden de prioridad (bit 0 primero, luego bit 1, etc.). Esta implementación es clara y fácil de entender, aunque no es la más eficiente. Para un emulador de producción, se podría optimizar usando operaciones bitwise más avanzadas, pero para un emulador educativo, la claridad es más importante que la eficiencia.

Despertar de HALT: Se decidió que handle_interrupts() maneje tanto el despertar de HALT como el procesamiento de interrupciones. Esto simplifica la lógica de step() y mantiene toda la lógica de interrupciones en un solo lugar.

Limpieza de IF: Cuando se procesa una interrupción, se limpia únicamente el bit correspondiente, preservando los demás bits. Esto permite que otras interrupciones pendientes se procesen en el siguiente ciclo.

Archivos Afectados

  • src/cpu/core.py - Añadido método handle_interrupts(), modificado step() para integrar manejo de interrupciones
  • tests/test_cpu_interrupts.py - Nuevo archivo con suite completa de tests TDD (6 tests) validando interrupciones, prioridades, despertar de HALT, y todos los vectores

Tests y Verificación

Se ejecutó la suite completa de tests TDD para validar todas las funcionalidades del despachador de interrupciones:

Comando ejecutado

python3 -m pytest tests/test_cpu_interrupts.py -v

Entorno

  • OS: macOS (darwin 21.6.0)
  • Python: 3.9.6
  • pytest: 8.4.2

Resultado

============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-8.4.2, pluggy-1.6.0
collected 6 items

tests/test_cpu_interrupts.py::TestCPUInterrupts::test_vblank_interrupt PASSED [ 16%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_interrupt_priority PASSED [ 33%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_halt_wakeup PASSED [ 50%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_no_interrupt_if_ime_disabled PASSED [ 66%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_timer_interrupt_vector PASSED [ 83%]
tests/test_cpu_interrupts.py::TestCPUInterrupts::test_all_interrupt_vectors PASSED [100%]

============================== 6 passed in 0.49s ===============================

Qué valida

  • test_vblank_interrupt: Interrupción V-Blank se procesa correctamente: salta a 0x0040, desactiva IME, limpia bit 0 de IF, guarda PC en la pila, consume 5 M-Cycles.
  • test_interrupt_priority: Si múltiples interrupciones están pendientes (V-Blank y Timer), se procesa primero V-Blank (mayor prioridad). El bit de Timer sigue activo para el siguiente ciclo.
  • test_halt_wakeup: Si la CPU está en HALT y hay interrupciones pendientes, la CPU despierta incluso si IME está desactivado. Después de despertar, continúa ejecutando normalmente.
  • test_no_interrupt_if_ime_disabled: Si IME está desactivado, las interrupciones no se procesan aunque IE e IF tengan bits activos. La CPU ejecuta instrucciones normalmente.
  • test_timer_interrupt_vector: Interrupción Timer salta al vector correcto (0x0050) y limpia el bit 2 de IF.
  • test_all_interrupt_vectors: Todos los vectores de interrupción son correctos: V-Blank (0x0040), LCD STAT (0x0048), Timer (0x0050), Serial (0x0058), Joypad (0x0060).

Código del test (ejemplo crítico: V-Blank)

def test_vblank_interrupt(self) -> None:
    """Test: Interrupción V-Blank se procesa correctamente."""
    mmu = MMU(None)
    cpu = CPU(mmu)
    
    # Configurar estado inicial
    cpu.registers.set_pc(0x1234)  # PC inicial
    cpu.registers.set_sp(0xFFFE)  # Stack Pointer inicial
    cpu.ime = True  # IME activado
    
    # Habilitar interrupción V-Blank en IE (bit 0)
    mmu.write_byte(IO_IE, 0x01)
    
    # Activar flag V-Blank en IF (bit 0)
    mmu.write_byte(IO_IF, 0x01)
    
    # Ejecutar step (debe procesar la interrupción)
    cycles = cpu.step()
    
    # Verificaciones
    assert cycles == 5, "La interrupción debe consumir 5 M-Cycles"
    assert cpu.registers.get_pc() == 0x0040, "PC debe saltar al vector V-Blank"
    assert cpu.ime is False, "IME debe desactivarse automáticamente"
    
    # Verificar que el bit 0 de IF se limpió
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x01) == 0, "El bit 0 de IF debe estar limpio"
    
    # Verificar que PC se guardó en la pila (Little-Endian)
    assert cpu.registers.get_sp() == 0xFFFC, "SP debe decrementarse en 2"
    saved_pc_low = mmu.read_byte(0xFFFC)
    saved_pc_high = mmu.read_byte(0xFFFD)
    saved_pc = (saved_pc_high << 8) | saved_pc_low
    assert saved_pc == 0x1234, "PC debe guardarse correctamente en la pila"

Por qué este test demuestra algo del hardware: Este test valida que la secuencia completa de interrupción se ejecuta correctamente: lectura de IE e IF, desactivación de IME, limpieza de flag, guardado de PC en la pila, y salto al vector. Sin esta funcionalidad, la CPU no puede responder a eventos del hardware como V-Blank, lo que significa que los juegos no pueden actualizar la pantalla correctamente.

Fuentes Consultadas

Nota: Implementación basada en documentación técnica oficial. No se consultó código de otros emuladores.

Integridad Educativa

Lo que Entiendo Ahora

  • Interrupciones como mecanismo de coordinación: Las interrupciones permiten que la CPU y los periféricos (PPU, Timer, etc.) se coordinen sin necesidad de polling constante. El hardware "grita" cuando necesita atención, y la CPU responde cuando puede.
  • Prioridad de interrupciones es crítica: Si múltiples interrupciones ocurren simultáneamente, el hardware procesa primero la de mayor prioridad (menor número de bit). Esto es importante para garantizar que eventos críticos (como V-Blank) se atiendan antes que eventos menos urgentes.
  • HALT y despertar: El estado HALT permite que la CPU entre en bajo consumo, pero debe despertar cuando hay interrupciones pendientes, incluso si IME está desactivado. Esto permite que el código pueda hacer polling manual de IF después de despertar.
  • IME como interruptor maestro: IME es el "interruptor maestro" que permite o bloquea todas las interrupciones. Cuando se procesa una interrupción, IME se desactiva automáticamente para evitar interrupciones anidadas inmediatas. El código debe reactivar IME explícitamente con EI cuando esté listo.
  • La secuencia de hardware es automática: Cuando se acepta una interrupción, el hardware ejecuta automáticamente la secuencia (desactivar IME, limpiar IF, PUSH PC, saltar). No hay instrucciones explícitas, es todo automático.

Lo que Falta Confirmar

  • Timing exacto de la secuencia de interrupción: La documentación indica que procesar una interrupción consume 5 M-Cycles, pero no está completamente claro cómo se distribuyen estos ciclos entre las diferentes operaciones (PUSH PC, limpieza de IF, etc.). Por ahora, contamos 5 ciclos en total.
  • Interrupciones anidadas: Si el código de una rutina de interrupción ejecuta EI, ¿puede ser interrumpida por otra interrupción? La documentación sugiere que sí, pero no está completamente claro el comportamiento exacto del hardware real. Por ahora, implementamos el comportamiento básico: IME se desactiva automáticamente y el código debe reactivarlo explícitamente.
  • HALT y timing: Cuando la CPU está en HALT, consume 1 ciclo por cada step(). ¿Este ciclo cuenta para el timing de otros componentes (PPU, Timer)? Esto podría afectar la sincronización precisa. Por ahora, implementamos el comportamiento básico: HALT consume 1 ciclo y no ejecuta instrucciones.

Hipótesis y Suposiciones

Procesamiento de interrupciones entre instrucciones: Asumimos que las interrupciones se comprueban entre cada instrucción, antes del fetch del siguiente opcode. Esto es consistente con la documentación, pero podría haber sutilezas de timing en el hardware real que no estamos modelando (por ejemplo, ¿qué pasa si una interrupción ocurre durante la ejecución de una instrucción de múltiples ciclos?).

Prioridad de interrupciones: Implementamos la prioridad usando una serie de if-elif que comprueba los bits en orden. Esto es correcto según la documentación, pero no estamos 100% seguros de que el hardware real use exactamente esta lógica (aunque el comportamiento observable debería ser el mismo).

Próximos Pasos

  • [ ] Implementar instrucción RETI (Return from Interrupt) que restaura PC desde la pila y reactiva IME automáticamente
  • [ ] Validar con una ROM real (Tetris DX) que las interrupciones V-Blank se procesan correctamente y el juego avanza
  • [ ] Implementar interrupciones de Timer (cuando se complete el subsistema de Timer)
  • [ ] Implementar interrupciones LCD STAT (cuando se complete el subsistema de PPU con modos de renderizado)
  • [ ] Investigar y documentar el timing exacto de la secuencia de interrupción