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 Latido del Tiempo: Implementación del Timer (DIV) en C++
Resumen
¡Hito alcanzado! La arquitectura de bucle nativo ha resuelto todos los deadlocks de sincronización y LY cicla correctamente. Sin embargo, la pantalla permanece en blanco porque la VRAM está vacía. El diagnóstico de la PPU revela que la CPU nunca copia los datos gráficos a la VRAM, probablemente porque está atrapada en un bucle de retardo de tiempo esperando al Timer, que aún no estaba implementado en el núcleo C++.
Este Step implementa el subsistema del Timer (inicialmente solo el registro DIV) en C++ e integra su actualización en el bucle de emulación nativo para permitir que la CPU supere los bucles de retardo de tiempo. Con el Timer funcionando, la CPU podrá avanzar en la secuencia de arranque y eventualmente copiar los datos del logo de Nintendo a la VRAM.
Concepto de Hardware: El Registro DIV (El Metrónomo del Sistema)
El Timer es un componente de hardware independiente que se usa para temporización y generación de números aleatorios. Su componente más básico es el registro DIV (Divider, en 0xFF04).
Características del registro DIV según Pan Docs:
- Frecuencia de Incremento: Es un contador que se incrementa constantemente a una frecuencia fija de 16384 Hz.
- Cálculo de Ciclos: Dado que el reloj principal es de 4.194304 MHz,
DIVse incrementa cada 256 T-Cycles (4194304 / 16384 = 256). - Lectura: Es de solo lectura desde la perspectiva del juego. El valor leído es los 8 bits altos del contador interno de 16 bits.
- Escritura: Escribir CUALQUIER valor en
0xFF04tiene el efecto secundario de resetear el contador a0. El valor escrito es ignorado. - Uso en el BIOS: El BIOS lo usa para generar retardos de tiempo precisos durante el arranque. Sin un
DIVque avance, la CPU se queda en un bucle infinito esperando que el contador alcance un valor específico.
¿Por qué es crítico para el arranque?
Durante la secuencia de arranque, el BIOS y los juegos usan bucles de retardo de tiempo basados en el registro DIV para la sincronización. Un ejemplo típico sería:
; Pseudocódigo del BIOS
wait_for_timer:
ld a, (0xFF04) ; Lee DIV
cp 0x10 ; Compara con valor objetivo
jr c, wait_for_timer ; Si DIV < 0x10, espera más
Si DIV nunca avanza (porque el Timer no está implementado), este bucle nunca termina y la CPU queda atrapada, impidiendo que continúe con la siguiente parte de la inicialización (como copiar los datos del logo a la VRAM).
Implementación
Se implementó el subsistema del Timer en C++ con la clase Timer, que mantiene un contador interno de T-Cycles y expone el registro DIV a través de la MMU. El Timer se integra en el bucle de emulación nativo mediante inyección de dependencias, permitiendo que la CPU y la MMU accedan a él sin acoplamiento directo.
Componentes Creados
Timer.hppyTimer.cpp: Clase C++ que implementa el registro DIV con un contador interno de 16 bits que se incrementa con cada llamada astep().timer.pyxytimer.pxd: Wrappers de Cython que exponen la claseTimera Python comoPyTimer.
Modificaciones en Componentes Existentes
CPU.hppyCPU.cpp: Agregado métodosetTimer()y actualización del Timer enrun_scanline()después de cada instrucción.MMU.hppyMMU.cpp: Agregado métodosetTimer()y manejo especial de lectura/escritura en0xFF04para acceder al Timer dinámicamente.cpu.pyxymmu.pyx: Agregados métodosset_timer()en los wrappers de Cython para conectar el Timer desde Python.viboy.py: Creación de instancia dePyTimery conexión a CPU y MMU durante la inicialización.setup.py: AgregadoTimer.cppa la lista de fuentes para compilación.
Decisiones de Diseño
- Inyección de Dependencias: El Timer se pasa como puntero a CPU y MMU, manteniendo el desacoplamiento y permitiendo que los componentes funcionen independientemente.
- Actualización en el Bucle Principal: El Timer se actualiza en
run_scanline()después de cada instrucción, garantizando sincronización precisa con el tiempo emulado. - Acceso Dinámico desde MMU: La MMU lee/escribe el registro DIV directamente desde el Timer, no desde memoria estática, permitiendo que el valor se actualice en tiempo real.
- Contador Interno de 16 bits: Se usa un contador interno de 16 bits para mantener precisión, y solo se exponen los 8 bits altos como DIV, simulando el comportamiento del hardware real.
Código Clave
Implementación del Timer en C++:
// Timer.cpp
void Timer::step(int t_cycles) {
div_counter_ += t_cycles;
}
uint8_t Timer::read_div() const {
return (div_counter_ >> 8) & 0xFF;
}
void Timer::write_div() {
div_counter_ = 0;
}
Integración en el bucle de emulación:
// CPU.cpp - run_scanline()
// Actualizar el Timer con los T-Cycles consumidos
if (timer_ != nullptr) {
timer_->step(t_cycles);
}
Acceso dinámico desde MMU:
// MMU.cpp - read()
if (addr == 0xFF04) {
if (timer_ != nullptr) {
return timer_->read_div();
}
return 0x00;
}
// MMU.cpp - write()
if (addr == 0xFF04) {
if (timer_ != nullptr) {
timer_->write_div();
}
return; // No escribimos en memoria
}
Archivos Afectados
src/core/cpp/Timer.hpp- Definición de la clase Timersrc/core/cpp/Timer.cpp- Implementación del Timersrc/core/cpp/CPU.hpp- Agregado método setTimer() y puntero a Timersrc/core/cpp/CPU.cpp- Integración del Timer en run_scanline()src/core/cpp/MMU.hpp- Agregado método setTimer() y puntero a Timersrc/core/cpp/MMU.cpp- Manejo de lectura/escritura de 0xFF04src/core/cython/timer.pxd- Definición Cython de Timersrc/core/cython/timer.pyx- Wrapper Python de Timersrc/core/cython/cpu.pxd- Agregado método setTimer()src/core/cython/cpu.pyx- Implementación de set_timer()src/core/cython/mmu.pxd- Agregado método setTimer()src/core/cython/mmu.pyx- Implementación de set_timer()src/core/cython/native_core.pyx- Inclusión de timer.pyxsrc/viboy.py- Creación y conexión del Timersetup.py- Agregado Timer.cpp a las fuentestests/test_core_timer.py- Tests unitarios del Timer
Tests y Verificación
Se crearon tests unitarios exhaustivos para validar la implementación del Timer:
- Comando ejecutado:
pytest tests/test_core_timer.py -v - Resultado esperado: Todos los tests pasan, validando:
- Valor inicial de DIV (0)
- Incremento cada 256 T-Cycles
- Reset al escribir en DIV
- Wrap-around después de 0xFF
- Frecuencia correcta (16384 Hz)
Código del Test (Fragmento Clave):
def test_div_increment_after_256_cycles():
"""Verifica que DIV se incrementa cada 256 T-Cycles."""
timer = PyTimer()
# DIV debe ser 0 después de 255 ciclos
timer.step(255)
assert timer.read_div() == 0
# DIV debe ser 1 después de 1 ciclo más (256 total)
timer.step(1)
assert timer.read_div() == 1
# DIV debe ser 2 después de 256 ciclos más (512 total)
timer.step(256)
assert timer.read_div() == 2
Validación de Módulo Compilado C++: Los tests validan directamente la implementación C++ a través del wrapper de Cython, confirmando que el Timer funciona correctamente en el código nativo.
Fuentes Consultadas
- Pan Docs: Sección "Timer and Divider Register" - Descripción del registro DIV y su comportamiento
- Pan Docs: Sección "System Clock" - Frecuencia del reloj principal (4.194304 MHz) y cálculo de ciclos
- Diagnóstico del Renderizador Ciego: Análisis que identificó que la CPU está atrapada en bucles de retardo de tiempo
Integridad Educativa
Lo que Entiendo Ahora
- Registro DIV: Es un contador de 16 bits interno que se incrementa continuamente. Solo los 8 bits altos son accesibles como DIV a través de
0xFF04. - Frecuencia de Incremento: DIV se incrementa cada 256 T-Cycles porque el reloj principal (4.194304 MHz) dividido por la frecuencia objetivo (16384 Hz) es 256.
- Reset por Escritura: Cualquier escritura en
0xFF04resetea el contador a 0, independientemente del valor escrito. Este es un efecto secundario del hardware. - Rol en el Arranque: El BIOS y los juegos usan DIV para generar retardos de tiempo precisos. Sin DIV funcionando, la CPU queda atrapada en bucles infinitos.
Lo que Falta Confirmar
- Valor Inicial de DIV: En una Game Boy real, DIV inicia en un valor aleatorio. Por simplicidad, lo inicializamos a 0. Esto debería verificarse con hardware real o documentación más detallada.
- Comportamiento con Wrap-around: El contador interno puede desbordarse (wrap-around) después de 65535. Esto es correcto y esperado, pero debería validarse con tests de larga duración.
Hipótesis y Suposiciones
Hipótesis Principal: Con el Timer implementado, la CPU podrá salir de los bucles de retardo de tiempo y avanzar en la secuencia de arranque, eventualmente copiando los datos del logo de Nintendo a la VRAM. Esto debería resultar en la aparición del logo en la pantalla.
Suposición: Asumimos que el valor inicial de DIV es 0, aunque en hardware real puede ser aleatorio. Esto no debería afectar el comportamiento una vez que el Timer comience a avanzar.
Próximos Pasos
- [ ] Recompilar el módulo C++ con
python setup.py build_ext --inplace - [ ] Ejecutar los tests del Timer para validar la implementación
- [ ] Ejecutar el emulador con una ROM y verificar que la CPU avanza más allá de los bucles de retardo
- [ ] Verificar que la VRAM se llena con datos del logo (usando logs de depuración si es necesario)
- [ ] Confirmar que el logo de Nintendo aparece en la pantalla
- [ ] Si el logo aparece, celebrar la victoria y documentar el hito
- [ ] Si aún hay problemas, investigar otros componentes faltantes (otros registros del Timer, interrupciones, etc.)