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)
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.
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 enTAC. CuandoTIMAse desborda (pasa de0xFFa0x00), se recarga automáticamente con el valor deTMAy se solicita una interrupción de Timer (Bit 2 del registroIF). -
TMA(Timer Modulo - 0xFF06): CuandoTIMAse desborda, se recarga automáticamente con el valor deTMA.
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
MMUpara solicitar interrupciones cuandoTIMAdesborda - 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 deTIMAsegún la configuración deTAC - Implementada la lógica de desbordamiento: cuando
TIMApasa de0xFFa0x00, se recarga conTMAy se solicita una interrupción mediantemmu_->request_interrupt(2) - Implementado
get_tima_threshold()que retorna el número de T-Cycles necesario según los bits 1-0 deTAC
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 medianteTimer::read_tima()/Timer::write_tima()0xFF06(TMA): Lectura/escritura medianteTimer::read_tma()/Timer::write_tma()0xFF07(TAC): Lectura/escritura medianteTimer::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 aceptarMMU*timer.pyx: Implementados métodos Python para leer/escribirTIMA,TMAyTAC, y modificado el constructor para aceptar unPyMMUopcionalmmu.pyx: Agregado métodoget_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, TACsrc/core/cpp/Timer.cpp- Implementación de lógica de desbordamiento e interrupcionessrc/core/cpp/MMU.cpp- Manejo de lecturas/escrituras de 0xFF05, 0xFF06, 0xFF07src/core/cython/timer.pxd- Declaraciones Cython actualizadassrc/core/cython/timer.pyx- Wrapper Python actualizado con nuevos métodossrc/core/cython/mmu.pyx- Agregado método get_cpp_ptr()src/viboy.py- Actualizado para pasar MMU al constructor de PyTimertests/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:
- ✅ Lectura/escritura de
TIMA,TMAyTAC - ✅ Incremento de
TIMAcuando el Timer está activado - ✅
TIMANO se incrementa cuando el Timer está desactivado - ✅
TIMAse incrementa a las frecuencias correctas (4096Hz, 262144Hz, 65536Hz, 16384Hz) - ✅ Cuando
TIMAdesborda, se recarga con el valor deTMA - ✅ Cuando
TIMAdesborda, se solicita una interrupción de Timer (bit 2 de IF) - ✅ 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ó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