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

El Corazón del Tiempo: Implementación del Timer Completo (TIMA, TMA, TAC)

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

Resumen

Tras el éxito del Step 0185, donde el emulador renderizó exitosamente el logo de Nintendo, surgió un diagnóstico crítico: aunque el emulador está estable y el registro LY cicla correctamente, la pantalla permanece en blanco porque la CPU nunca llega a la instrucción que activa el renderizado del fondo. El diagnóstico apuntó a que la CPU está atrapada en un bucle de retardo de tiempo, esperando una interrupción del Timer que nuestro núcleo C++ aún no podía generar.

Este Step implementa el subsistema completo del Timer programable (TIMA, TMA, TAC) en C++, completando la funcionalidad que solo tenía el registro DIV. Con esta implementación, el emulador puede ejecutar rutinas de temporización precisas, permitiendo que los juegos completen sus secuencias de arranque y, finalmente, den la orden de dibujar la pantalla de título.

🔍 Diagnóstico Definitivo: La paradoja de la precisión. Nuestra PPU, ahora precisa, ve que el Bit 0 del LCDC es 0 y, obedientemente, se niega a dibujar la capa de fondo. El problema no es nuestro emulador; es que el juego nunca llega a la instrucción que cambia LCDC de 0x80 a 0x91 porque está esperando una interrupción del Timer que aún no podíamos generar.

Concepto de Hardware: El Temporizador Programable

Además del registro DIV (Divider, 0xFF04), la Game Boy tiene un temporizador configurable por el juego, controlado por 3 registros:

  • TAC (Timer Control - 0xFF07):
    • Bit 2: Timer Enable (1 = ON, 0 = OFF).
    • Bits 1-0: Input Clock Select. Selecciona la frecuencia a la que se incrementa TIMA:
      • 00: 4096 Hz (1024 T-Cycles por incremento)
      • 01: 262144 Hz (16 T-Cycles por incremento)
      • 10: 65536 Hz (64 T-Cycles por incremento)
      • 11: 16384 Hz (256 T-Cycles por incremento)
  • TIMA (Timer Counter - 0xFF05): Es un contador de 8 bits que se incrementa a la frecuencia seleccionada en TAC. Cuando TIMA se desborda (pasa de 0xFF a 0x00), se recarga automáticamente con el valor de TMA y se solicita una interrupción de Timer (Bit 2 del registro IF).
  • TMA (Timer Modulo - 0xFF06): Cuando TIMA se desborda, se recarga automáticamente con el valor de TMA.

El BIOS y los juegos usan este sistema para crear retardos precisos. Si no se implementa, cualquier juego que configure el Timer y luego use HALT para esperar la interrupción se quedará congelado para siempre.

Fuente: Pan Docs - Timer and Divider Register, Timer Control, Timer Counter, Timer Modulo.

Implementación

1. Expansión de la Clase Timer en C++

Se expandió la clase Timer existente para incluir los registros TIMA, TMA y TAC, junto con la lógica de desbordamiento e interrupciones.

Modificaciones en Timer.hpp:

  • Agregado puntero a MMU para solicitar interrupciones cuando TIMA desborda
  • Agregados miembros privados: tima_counter_, tima_, tma_, tac_
  • Agregados métodos públicos: read_tima(), write_tima(), read_tma(), write_tma(), read_tac(), write_tac()
  • Agregado método privado: get_tima_threshold() para calcular el número de T-Cycles según la frecuencia configurada
  • Modificado el constructor para aceptar un puntero a MMU

Modificaciones en Timer.cpp:

  • Actualizado el método step() para manejar el incremento de TIMA según la configuración de TAC
  • Implementada la lógica de desbordamiento: cuando TIMA pasa de 0xFF a 0x00, se recarga con TMA y se solicita una interrupción mediante mmu_->request_interrupt(2)
  • Implementado get_tima_threshold() que retorna el número de T-Cycles necesario según los bits 1-0 de TAC

2. Integración en la MMU

Se actualizó MMU.cpp para manejar las lecturas y escrituras de los registros del Timer:

  • 0xFF05 (TIMA): Lectura/escritura mediante Timer::read_tima() / Timer::write_tima()
  • 0xFF06 (TMA): Lectura/escritura mediante Timer::read_tma() / Timer::write_tma()
  • 0xFF07 (TAC): Lectura/escritura mediante Timer::read_tac() / Timer::write_tac()

3. Actualización de Wrappers Cython

Se actualizaron los wrappers Cython para exponer la nueva funcionalidad:

  • timer.pxd: Agregadas declaraciones para los nuevos métodos y modificado el constructor para aceptar MMU*
  • timer.pyx: Implementados métodos Python para leer/escribir TIMA, TMA y TAC, y modificado el constructor para aceptar un PyMMU opcional
  • mmu.pyx: Agregado método get_cpp_ptr() para mantener consistencia con otros wrappers

4. Actualización del Sistema Principal

Se actualizó viboy.py para pasar la instancia de MMU al constructor de PyTimer, permitiendo que el Timer solicite interrupciones correctamente.

Decisiones de Diseño

¿Por qué el Timer necesita un puntero a MMU? Cuando TIMA desborda, el hardware solicita una interrupción de Timer escribiendo en el bit 2 del registro IF (0xFF0F). Como la MMU es la dueña de la memoria y maneja el registro IF, el Timer necesita un puntero a la MMU para poder solicitar interrupciones.

Manejo de múltiples incrementos: El método step() maneja correctamente casos donde una instrucción consume muchos T-Cycles, usando un bucle while para procesar múltiples incrementos de TIMA si es necesario.

Archivos Afectados

  • src/core/cpp/Timer.hpp - Expansión para incluir TIMA, TMA, TAC
  • src/core/cpp/Timer.cpp - Implementación de lógica de desbordamiento e interrupciones
  • src/core/cpp/MMU.cpp - Manejo de lecturas/escrituras de 0xFF05, 0xFF06, 0xFF07
  • src/core/cython/timer.pxd - Declaraciones Cython actualizadas
  • src/core/cython/timer.pyx - Wrapper Python actualizado con nuevos métodos
  • src/core/cython/mmu.pyx - Agregado método get_cpp_ptr()
  • src/viboy.py - Actualizado para pasar MMU al constructor de PyTimer
  • tests/test_core_timer.py - Tests completos para TIMA, TMA, TAC

Tests y Verificación

Comando ejecutado:

python -m pytest tests/test_core_timer.py -v

Resultado: 16 tests pasados en 0.07s

Los tests validan:

  1. ✅ Lectura/escritura de TIMA, TMA y TAC
  2. ✅ Incremento de TIMA cuando el Timer está activado
  3. TIMA NO se incrementa cuando el Timer está desactivado
  4. TIMA se incrementa a las frecuencias correctas (4096Hz, 262144Hz, 65536Hz, 16384Hz)
  5. ✅ Cuando TIMA desborda, se recarga con el valor de TMA
  6. ✅ Cuando TIMA desborda, se solicita una interrupción de Timer (bit 2 de IF)
  7. ✅ Manejo correcto de múltiples incrementos en un solo step()

Código del Test Clave:

def test_tima_overflow_requests_interrupt(self):
    """Verifica que cuando TIMA desborda, se solicita una interrupción de Timer."""
    mmu = PyMMU()
    timer = PyTimer(mmu)
    mmu.set_timer(timer)  # Conectar Timer a MMU para interrupciones
    
    # Configurar TIMA cerca del desbordamiento
    timer.write_tima(0xFF)
    timer.write_tac(0x04)  # Timer ON, frecuencia 4096 Hz
    
    # Avanzar 1024 T-Cycles (debería causar desbordamiento)
    timer.step(1024)
    
    # Verificar que se solicitó una interrupción de Timer (bit 2 del registro IF)
    if_reg = mmu.read(0xFF0F)
    assert (if_reg & 0x04) != 0, "Se debería haber solicitado una interrupción de Timer (bit 2 de IF)"

Validación Nativa: Validación de módulo compilado C++ con tests unitarios exhaustivos.

Fuentes Consultadas

  • Pan Docs: Timer and Divider Register (0xFF04)
  • Pan Docs: Timer Counter (TIMA - 0xFF05)
  • Pan Docs: Timer Modulo (TMA - 0xFF06)
  • Pan Docs: Timer Control (TAC - 0xFF07)
  • Pan Docs: Interrupts, Interrupt Flag Register (IF - 0xFF0F)

Integridad Educativa

Lo que Entiendo Ahora

  • El Timer programable: Además del DIV (que es un contador continuo), la Game Boy tiene un Timer programable que permite a los juegos crear retardos precisos y generar interrupciones de tiempo.
  • Las frecuencias del Timer: El Timer puede funcionar a 4 frecuencias diferentes (4096Hz, 262144Hz, 65536Hz, 16384Hz), seleccionadas mediante los bits 1-0 de TAC.
  • El desbordamiento y la recarga: Cuando TIMA desborda (0xFF → 0x00), se recarga automáticamente con el valor de TMA y se solicita una interrupción de Timer.
  • La importancia para la secuencia de arranque: El BIOS y los juegos usan el Timer para crear retardos precisos durante la inicialización. Sin esta funcionalidad, los juegos se quedarían congelados esperando interrupciones que nunca llegan.

Lo que Falta Confirmar

  • Verificación con ROMs reales: El próximo paso será ejecutar el emulador con una ROM real para verificar que el Timer completo permite que los juegos completen sus rutinas de temporización y finalmente dibujen la pantalla de título.
  • Sincronización con HALT: Verificar que cuando la CPU ejecuta HALT y el Timer genera una interrupción, la CPU se despierta correctamente y procesa la interrupción.

Hipótesis y Suposiciones

No hay suposiciones críticas. La implementación está basada directamente en Pan Docs, que especifica claramente el comportamiento de cada registro y la relación entre TIMA, TMA y TAC.

Resultado Final

Después de esta implementación, el emulador ahora tiene:

  • Timer completo funcional: Todos los registros del Timer (DIV, TIMA, TMA, TAC) están implementados y funcionando correctamente
  • Interrupciones de Timer: Cuando TIMA desborda, se solicita correctamente una interrupción de Timer (bit 2 de IF)
  • Frecuencias precisas: El Timer se incrementa a las frecuencias correctas según la configuración de TAC
  • Tests exhaustivos: 16 tests unitarios validan toda la funcionalidad del Timer
🎯 Próximo Hito Esperado: Con el Timer completo implementado, el emulador debería ser capaz de ejecutar las rutinas de temporización de los juegos, permitiendo que completen sus secuencias de arranque y finalmente dibujen la pantalla de título. El diagnóstico del Step 0185 apuntaba a que la CPU estaba atrapada esperando una interrupción del Timer. Ahora que el Timer puede generar esas interrupciones, esperamos ver el logo de Nintendo aparecer en pantalla.

Próximos Pasos

  • [ ] Ejecutar el emulador con una ROM real para verificar que el Timer completo permite que los juegos completen sus rutinas de temporización
  • [ ] Verificar que las interrupciones de Timer despiertan correctamente a la CPU cuando está en estado HALT
  • [ ] Si todo funciona correctamente, deberíamos ver finalmente el logo de Nintendo en pantalla