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

Timer Completo: TIMA, TMA y TAC

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

Resumen

Se completó la implementación del subsistema Timer de la Game Boy añadiendo los registros TIMA (Timer Counter, 0xFF05), TMA (Timer Modulo, 0xFF06) y TAC (Timer Control, 0xFF07). El Timer ahora puede generar interrupciones cuando TIMA hace overflow (pasa de 255 a 0), recargándose automáticamente con el valor de TMA. Esta funcionalidad es crítica para muchos juegos que usan el Timer durante la inicialización para generar semillas aleatorias o esperar intervalos de tiempo específicos. Se crearon 21 tests completos que validan todas las frecuencias, el overflow, la recarga con TMA y la solicitud de interrupciones. Todos los tests pasan correctamente.

Concepto de Hardware

El Timer de la Game Boy es un sistema de temporización que incluye cuatro registros:

  • DIV (0xFF04): Contador continuo que incrementa a 16384 Hz (ya implementado en paso 0037).
  • TIMA (0xFF05): Contador de 8 bits que incrementa a una frecuencia configurable.
  • TMA (0xFF06): Valor de recarga cuando TIMA hace overflow.
  • TAC (0xFF07): Registro de control que activa/desactiva el Timer y selecciona la frecuencia.

TAC (Timer Control) tiene la siguiente estructura:

  • Bit 2: Enable (1 = Timer activo, 0 = Timer apagado).
  • Bits 1-0: Frecuencia de incremento de 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
  • Bits 3-7: Siempre son 1 (no se pueden escribir).

Overflow de TIMA: Cuando TIMA pasa de 255 (0xFF) a 0, ocurren dos cosas simultáneamente:

  1. TIMA se recarga automáticamente con el valor de TMA.
  2. Se activa el bit 2 del registro IF (Interrupt Flag, 0xFF0F), solicitando una interrupción Timer.

La interrupción Timer será procesada por la CPU si IME (Interrupt Master Enable) está activo y el bit 2 de IE (Interrupt Enable, 0xFFFF) también está activo. El vector de interrupción Timer es 0x0050.

Uso en juegos: Muchos juegos usan el Timer durante la inicialización para:

  • Generar semillas aleatorias (RNG) basadas en el tiempo transcurrido.
  • Esperar intervalos de tiempo específicos antes de continuar.
  • Sincronizar eventos con el tiempo del sistema.

Si el Timer no está implementado correctamente, los juegos pueden quedarse en bucles infinitos esperando que TIMA haga overflow, causando el síntoma de "pantalla blanca eterna" o congelamiento durante la inicialización.

Fuente: Pan Docs - Timer and Divider Registers

Implementación

Se extendió la clase Timer en src/io/timer.py para implementar TIMA, TMA y TAC. La implementación incluye:

Componentes creados/modificados

  • Timer (src/io/timer.py):
    • Añadidos registros internos: _tima, _tma, _tac.
    • Añadido acumulador _tima_accumulator para manejar fracciones de ciclo.
    • Añadida referencia a MMU para solicitar interrupciones.
    • Método tick() extendido para procesar TIMA según la frecuencia configurada.
    • Métodos read_tima(), write_tima(), read_tma(), write_tma(), read_tac(), write_tac().
    • Método _get_tima_threshold() para determinar el umbral de T-Cycles según la frecuencia.
    • Método _request_timer_interrupt() para activar el bit 2 de IF.
    • Método set_mmu() para conectar la MMU al Timer.
  • MMU (src/memory/mmu.py):
    • Interceptadas lecturas de TIMA (0xFF05), TMA (0xFF06) y TAC (0xFF07) en read_byte().
    • Interceptadas escrituras en TIMA, TMA y TAC en write_byte().
    • Las operaciones se delegan al Timer, similar a como se hace con DIV.
  • Viboy (src/viboy.py):
    • Añadida conexión de MMU al Timer mediante timer.set_mmu(mmu) en ambos puntos de inicialización.
    • Esto permite que el Timer solicite interrupciones cuando TIMA hace overflow.
  • Tests (tests/test_io_timer_full.py):
    • Creado archivo completo con 21 tests que validan todas las funcionalidades del Timer.
    • Tests para TIMA: inicialización, lectura/escritura, incremento en todas las frecuencias, overflow, recarga con TMA.
    • Tests para TMA: inicialización, lectura/escritura.
    • Tests para TAC: inicialización, lectura/escritura, enable/disable.
    • Tests para interrupciones: overflow genera interrupción, múltiples interrupciones.
    • Tests de integración con MMU: lectura/escritura a través de MMU, ciclo completo.

Decisiones de diseño

Acumulador de TIMA: Se usa un acumulador interno (_tima_accumulator) para manejar fracciones de ciclo. Cuando se acumulan suficientes T-Cycles para un incremento, se resta el umbral y se incrementa TIMA. Esto permite manejar correctamente múltiples incrementos en una sola llamada a tick().

Detección de overflow: Se verifica overflow ANTES de incrementar TIMA. Si TIMA es 0xFF, el siguiente incremento causará overflow, por lo que se recarga con TMA y se solicita la interrupción. Esto evita tener que verificar después del incremento y manejar el caso especial de 0xFF -> 0.

Bits no utilizados de TAC: Los bits 3-7 de TAC siempre se leen como 1, aunque solo los bits 0-2 son significativos. Esto se implementa en read_tac() retornando (tac & 0x07) | 0xF8.

Referencia circular MMU-Timer: Se evita dependencia circular usando el patrón de "setter" después de la inicialización. Primero se crea el Timer, luego se conecta a la MMU con mmu.set_timer(timer), y finalmente se conecta la MMU al Timer con timer.set_mmu(mmu). Esto permite que el Timer solicite interrupciones sin crear dependencias circulares en los constructores.

Archivos Afectados

  • src/io/timer.py - Implementación completa de TIMA, TMA y TAC con lógica de overflow e interrupciones
  • src/memory/mmu.py - Interceptación de lecturas/escrituras de TIMA, TMA y TAC
  • src/viboy.py - Conexión de MMU al Timer para solicitar interrupciones
  • tests/test_io_timer_full.py - Suite completa de 21 tests para validar todas las funcionalidades

Tests y Verificación

Se ejecutaron los tests completos del Timer para validar la implementación:

  • Comando ejecutado: pytest tests/test_io_timer_full.py -v
  • Entorno: Windows, Python 3.13.5
  • Resultado: 21 tests PASSED en 0.08s
  • Qué valida:
    • Inicialización correcta de TIMA, TMA y TAC
    • Lectura/escritura de todos los registros
    • Incremento de TIMA en las 4 frecuencias configuradas (4096Hz, 262144Hz, 65536Hz, 16384Hz)
    • Overflow de TIMA y recarga automática con TMA
    • Solicitud de interrupción Timer (bit 2 de IF) cuando TIMA hace overflow
    • Integración completa con MMU para lectura/escritura a través de direcciones de memoria

Código del test (ejemplo: overflow e interrupción):

def test_tima_overflow_interrupt(self) -> None:
    """Test: Verificar que se solicita interrupción cuando TIMA hace overflow"""
    mmu = MMU(None)
    timer = Timer()
    timer.set_mmu(mmu)
    
    # Configurar Timer
    timer.write_tma(0x42)
    timer.write_tac(0x04)  # Enable=1, Freq=00 (4096Hz)
    timer.write_tima(0xFF)
    
    # Verificar que IF bit 2 está desactivado inicialmente
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x04) == 0
    
    # Avanzar hasta overflow
    timer.tick(1024)
    
    # Verificar que IF bit 2 se activó
    if_val = mmu.read_byte(IO_IF)
    assert (if_val & 0x04) != 0

Este test demuestra que cuando TIMA hace overflow (pasa de 0xFF a 0), el hardware automáticamente activa el bit 2 del registro IF, solicitando una interrupción Timer. La CPU procesará esta interrupción si IME está activo y el bit 2 de IE también está activo.

También se ejecutaron los tests existentes del Timer (DIV) para verificar que no se rompió funcionalidad previa:

  • Comando ejecutado: pytest tests/test_io_timer.py -v
  • Resultado: 10 tests PASSED en 0.06s
  • Validación: Todos los tests de DIV siguen funcionando correctamente

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • Timer como sistema de temporización: El Timer de la Game Boy es un sistema complejo que incluye un contador continuo (DIV) y un contador configurable (TIMA) que puede generar interrupciones. Ambos funcionan independientemente pero comparten el mismo reloj base del sistema (4.194304 MHz).
  • Overflow y recarga: Cuando TIMA hace overflow, el hardware automáticamente recarga TIMA con TMA y solicita una interrupción. Esto es un comportamiento atómico del hardware: ambas cosas ocurren simultáneamente.
  • Frecuencias del Timer: Las 4 frecuencias disponibles (4096Hz, 262144Hz, 65536Hz, 16384Hz) se calculan dividiendo la frecuencia del sistema (4.194304 MHz) por el umbral de T-Cycles. Esto permite que los juegos seleccionen la velocidad de incremento de TIMA según sus necesidades.
  • Importancia para juegos: Muchos juegos dependen del Timer durante la inicialización. Si el Timer no está implementado, los juegos pueden quedarse en bucles infinitos esperando que TIMA haga overflow, causando congelamiento o pantalla blanca.

Lo que Falta Confirmar

  • Timing exacto del overflow: Según la documentación, escribir en TIMA durante el ciclo en que hace overflow puede tener comportamiento especial. Por ahora, implementamos escritura directa, pero esto podría necesitar refinamiento si encontramos juegos que dependen de este comportamiento específico.
  • Comportamiento al desactivar/reactivar TAC: Cuando se desactiva el Timer (bit 2 de TAC pasa de 1 a 0), el acumulador de TIMA se mantiene. Si se reactiva, TIMA continúa desde donde estaba. Esto está implementado, pero no está completamente verificado con ROMs reales.

Hipótesis y Suposiciones

Acumulador de TIMA: Usamos un acumulador interno para manejar fracciones de ciclo. Esto permite que múltiples incrementos ocurran en una sola llamada a tick() si se pasan muchos T-Cycles. Esta implementación es razonable y debería comportarse igual que el hardware real, pero no está completamente verificada con documentación técnica específica sobre el comportamiento interno del acumulador.

Bits no utilizados de TAC: Los bits 3-7 de TAC siempre se leen como 1. Esto está documentado en Pan Docs, pero la implementación asume que esto es un comportamiento del hardware y no un efecto secundario de la lectura. Si encontramos juegos que dependen de este comportamiento, lo validaremos con tests adicionales.

Próximos Pasos

  • [ ] Probar el emulador con ROMs reales (ej. Tetris DX) para verificar que el Timer resuelve el problema de congelamiento durante la inicialización
  • [ ] Si el Timer funciona correctamente, el juego debería salir del bucle de espera y encender la pantalla (LCDC != 0)
  • [ ] Verificar que las interrupciones Timer se procesan correctamente cuando IME está activo
  • [ ] Si hay problemas de timing, investigar el comportamiento exacto del overflow y la escritura en TIMA durante el overflow