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.
CPU: Implementar DEC (HL) para Romper Segundo Bucle Infinito
Resumen
Se implementaron los opcodes faltantes INC (HL) (0x34) y DEC (HL) (0x35) en la CPU de C++ para completar la familia de instrucciones de incremento y decremento. Aunque el diagnóstico inicial apuntaba a DEC C (0x0D), este ya estaba implementado; el verdadero problema era la ausencia de los opcodes que operan sobre memoria indirecta. Con esta implementación, los bucles de limpieza de memoria en las ROMs ahora pueden ejecutarse correctamente, permitiendo que el PC avance más allá de la barrera de 0x0300 y la traza disparada capture el código que se ejecuta después de la inicialización.
Concepto de Hardware
El Game Boy LR35902 soporta instrucciones de incremento y decremento de 8 bits sobre registros directos (B, C, D, E, H, L, A) y también sobre memoria indirecta usando el par de registros HL como puntero.
Las instrucciones INC (HL) y DEC (HL) funcionan de la siguiente manera:
- INC (HL) (opcode 0x34): Lee el valor de memoria en la dirección apuntada por HL, lo incrementa en 1, actualiza los flags Z, N y H según corresponda, y escribe el resultado de vuelta a memoria. Consume 3 M-Cycles (lectura + operación + escritura).
- DEC (HL) (opcode 0x35): Lee el valor de memoria en la dirección apuntada por HL, lo decrementa en 1, actualiza los flags Z, N y H, y escribe el resultado de vuelta a memoria. También consume 3 M-Cycles.
Estos opcodes son críticos para bucles que limpian regiones de memoria (como los bucles de inicialización en las ROMs de Game Boy). Si faltan, la CPU devuelve 0 ciclos desde el default case del switch, causando que el motor de timing se detenga y LY se quede atascado en 0.
Referencia: Pan Docs - Instruction Set - Arithmetic Operations (INC/DEC).
Implementación
Se añadieron dos casos al switch principal de CPU::step() en src/core/cpp/CPU.cpp:
Opcodes Implementados
- 0x34 - INC (HL): Utiliza
alu_inc()para incrementar el valor leído de memoria y actualizar flags. Consume 3 M-Cycles. - 0x35 - DEC (HL): Utiliza
alu_dec()para decrementar el valor leído de memoria y actualizar flags. Consume 3 M-Cycles.
Implementación en C++
Ambos opcodes siguen el mismo patrón:
- Obtener la dirección apuntada por HL usando
regs_->get_hl(). - Leer el valor actual de memoria usando
mmu_->read(addr). - Aplicar la operación (incremento o decremento) usando los helpers ALU existentes (
alu_inc()oalu_dec()). - Escribir el resultado de vuelta a memoria usando
mmu_->write(addr, result). - Actualizar el contador de ciclos y retornar 3 M-Cycles.
Decisiones de Diseño
Se reutilizaron los helpers ALU existentes (alu_inc() y alu_dec()) en lugar de duplicar la lógica de cálculo de flags. Esto mantiene la consistencia y facilita el mantenimiento. Los helpers ya manejan correctamente:
- Flag Z (resultado == 0)
- Flag N (1 para DEC, 0 para INC)
- Flag H (half-carry/half-borrow)
- Preservación del flag C (quirk del hardware)
Archivos Afectados
src/core/cpp/CPU.cpp- Añadidos casos 0x34 (INC (HL)) y 0x35 (DEC (HL)) al switch principaltests/test_core_cpu_inc_dec.py- Añadidos tres tests nuevos:test_inc_hl_indirect,test_dec_hl_indirect, ytest_dec_hl_indirect_half_borrow
Tests y Verificación
Se añadieron tres tests unitarios para validar la implementación:
- test_inc_hl_indirect: Verifica que INC (HL) incrementa correctamente el valor en memoria y actualiza flags.
- test_dec_hl_indirect: Verifica que DEC (HL) decrementa correctamente el valor en memoria y activa el flag Z cuando el resultado es 0.
- test_dec_hl_indirect_half_borrow: Verifica que DEC (HL) detecta correctamente el half-borrow (0x10 -> 0x0F) y activa el flag H.
Comando ejecutado:
pytest tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_inc_hl_indirect \
tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect \
tests/test_core_cpu_inc_dec.py::TestCoreCPUIncDec::test_dec_hl_indirect_half_borrow -v
Resultado:
============================== 3 passed in 0.08s ==============================
Código del Test (Fragmento):
def test_dec_hl_indirect(self):
"""Verificar que DEC (HL) decrementa el valor en memoria apuntado por HL."""
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x8000
regs.hl = 0xC000
mmu.write(0xC000, 0x01) # Valor inicial en memoria
mmu.write(0x8000, 0x35) # DEC (HL)
cpu.step()
assert mmu.read(0xC000) == 0x00, "DEC (HL) debe decrementar el valor en memoria"
assert regs.flag_z == True, "Z debe estar activo (resultado == 0)"
assert regs.flag_n == True, "N debe estar activo (es decremento)"
Validación Nativa: Los tests validan directamente el módulo compilado C++ a través del wrapper Cython. La CPU nativa ejecuta las instrucciones y los tests verifican el resultado en memoria y los flags.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - Sección de operaciones aritméticas (INC/DEC)
- GBEDG: Instruction Timing - Timing de instrucciones de 8 bits sobre memoria indirecta
Nota: La implementación se basa en la especificación oficial del LR35902 documentada en Pan Docs. Los helpers ALU ya implementados (alu_inc, alu_dec) fueron reutilizados para mantener consistencia.
Integridad Educativa
Lo que Entiendo Ahora
- Direccionamiento Indirecto: Las instrucciones que operan sobre
(HL)requieren un acceso adicional a memoria, aumentando el tiempo de ejecución de 1 a 3 M-Cycles (lectura + operación + escritura). - Bucle Infinito por Opcode Faltante: Cuando la CPU encuentra un opcode no implementado, el
defaultcase del switch devuelve 0 ciclos, causando que el motor de timing se detenga y el emulador entre en un deadlock lógico donde el tiempo no avanza. - Diagnóstico de LY Atascado: Si
LYestá permanentemente en 0, indica queppu.step()nunca recibe ciclos suficientes para avanzar una línea de escaneo, lo que apunta directamente a que la CPU está devolviendo 0 ciclos.
Lo que Falta Confirmar
- Ejecución Real en ROMs: Aunque los tests unitarios pasan, falta verificar que los bucles de limpieza de memoria en las ROMs reales ahora se ejecutan correctamente y permiten que la inicialización continúe.
- Traza Disparada: Con estos opcodes implementados, el PC debería avanzar más allá de
0x0300y activar la traza disparada, proporcionando información sobre el código que se ejecuta después de los bucles de inicialización.
Hipótesis y Suposiciones
La hipótesis inicial era que DEC C (0x0D) faltaba, pero al revisar el código se descubrió que ya estaba implementado. El verdadero problema eran los opcodes de memoria indirecta INC (HL) y DEC (HL). Esto demuestra la importancia de verificar exhaustivamente todos los opcodes relacionados antes de asumir cuál es el culpable.
Próximos Pasos
- [ ] Ejecutar el emulador con una ROM real (ej:
tetris.gb) y verificar queLYahora avanza correctamente - [ ] Confirmar que la traza disparada se activa cuando el PC alcanza
0x0300 - [ ] Analizar las 100 instrucciones capturadas por la traza para identificar qué opcodes adicionales pueden estar faltando
- [ ] Continuar implementando opcodes faltantes hasta que la inicialización de la ROM se complete