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.
Opcodes LD Indirect (0x0A, 0x1A, 0x3A)
Resumen
Se implementaron tres opcodes de carga indirecta que faltaban en el emulador: LD A, (BC) (0x0A), LD A, (DE) (0x1A) y LD A, (HL-) (0x3A). Estos opcodes son esenciales para que Tetris DX pueda ejecutarse correctamente, ya que el juego los utiliza frecuentemente para leer datos de memoria usando diferentes registros como punteros. Se crearon 5 tests para validar el comportamiento de estos opcodes, todos pasando correctamente.
Concepto de Hardware
La CPU LR35902 de la Game Boy soporta múltiples formas de acceder a memoria usando registros de 16 bits como punteros. Los registros BC, DE y HL pueden usarse como punteros para leer y escribir en memoria, proporcionando flexibilidad en las operaciones de transferencia de datos.
LD A, (BC) (0x0A): Lee un byte de la dirección de memoria apuntada por BC y lo carga en A. Es el gemelo
de LD (BC), A (0x02): mientras que 0x02 escribe A en memoria, 0x0A lee de memoria y lo guarda en A.
LD A, (DE) (0x1A): Similar a LD A, (BC), pero usando DE como puntero. Es el gemelo de LD (DE), A (0x12).
Útil para leer datos usando DE como puntero de origen en operaciones de copia.
LD A, (HL-) (0x3A): Lee un byte de la dirección apuntada por HL y lo carga en A, luego decrementa HL.
Es el complemento de LD (HL-), A. Útil para bucles de lectura rápida que recorren la memoria hacia atrás.
El decremento de HL permite iterar eficientemente sobre arrays o buffers en memoria.
Todos estos opcodes consumen 2 M-Cycles: uno para fetch del opcode y otro para la lectura de memoria. Los registros puntero (BC, DE, HL) no se modifican excepto en el caso de LD A, (HL-), donde HL se decrementa.
Fuente: Pan Docs - Instruction Set (LD A, (BC)), (LD A, (DE)), (LDD A, (HL))
Implementación
Se implementaron tres métodos en la clase CPU para manejar estos opcodes:
Componentes creados/modificados
src/cpu/core.py: Añadidos métodos_op_ld_a_bc_ptr(),_op_ld_a_de_ptr()y_op_ldd_a_hl_ptr()src/cpu/core.py: Registrados opcodes 0x0A, 0x1A y 0x3A en el dispatch tabletests/test_cpu_ld_indirect.py: Nuevo archivo con 5 tests para validar los opcodes
Decisiones de diseño
LD A, (BC) y LD A, (DE) son prácticamente idénticos en implementación, solo difieren en el registro usado como puntero. Se mantuvieron como métodos separados para claridad y consistencia con la estructura del código.
Para LD A, (HL-), el decremento de HL se implementa con wrap-around de 16 bits usando (hl_addr - 1) & 0xFFFF,
asegurando que si HL es 0x0000, al decrementar se convierte en 0xFFFF (comportamiento estándar de la Game Boy).
Todos los métodos incluyen logging a nivel DEBUG para facilitar la depuración, mostrando el valor leído, la dirección de memoria y el estado final de los registros afectados.
Archivos Afectados
src/cpu/core.py- Añadidos métodos_op_ld_a_bc_ptr(),_op_ld_a_de_ptr()y_op_ldd_a_hl_ptr(), registrados opcodes en dispatch tabletests/test_cpu_ld_indirect.py- Nuevo archivo con 5 tests:test_ld_a_bc_ptr,test_ld_a_de_ptr,test_ld_a_bc_ptr_wrap_around,test_ld_a_de_ptr_zero,test_ld_a_hl_ptr_decrement
Tests y Verificación
Se crearon 5 tests en tests/test_cpu_ld_indirect.py para validar el comportamiento de los opcodes:
- test_ld_a_bc_ptr: Verifica que LD A, (BC) lee correctamente de memoria, actualiza A y no modifica BC
- test_ld_a_de_ptr: Verifica que LD A, (DE) lee correctamente de memoria, actualiza A y no modifica DE
- test_ld_a_bc_ptr_wrap_around: Verifica que LD A, (BC) funciona correctamente con direcciones en el límite (0xFFFF)
- test_ld_a_de_ptr_zero: Verifica que LD A, (DE) funciona correctamente con dirección 0x0000
- test_ld_a_hl_ptr_decrement: Verifica que LD A, (HL-) lee correctamente, actualiza A y decrementa HL
Comando ejecutado: pytest -q tests/test_cpu_ld_indirect.py
Entorno: macOS, Python 3.10+
Resultado: ✅ 5 passed (todos los tests pasan correctamente)
Qué valida:
- Los opcodes leen correctamente de memoria usando los registros como punteros
- El registro A se actualiza con el valor leído
- Los registros puntero (BC, DE) no se modifican excepto HL en LD A, (HL-)
- El timing es correcto (2 M-Cycles para todos)
- El wrap-around funciona correctamente en casos límite
Código del test (ejemplo - test_ld_a_de_ptr):
def test_ld_a_de_ptr(self) -> None:
"""Test: Verificar LD A, (DE) - opcode 0x1A"""
mmu = MMU(None)
cpu = CPU(mmu)
cpu.registers.set_pc(0x0100)
cpu.registers.set_de(0xD000)
cpu.registers.set_a(0x00)
mmu.write_byte(0xD000, 0x55)
mmu.write_byte(0x0100, 0x1A) # LD A, (DE)
cycles = cpu.step()
assert cycles == 2
assert cpu.registers.get_a() == 0x55
assert cpu.registers.get_pc() == 0x0101
assert cpu.registers.get_de() == 0xD000
Validación con Tetris DX: El juego dejó de crashearse con errores de opcodes no implementados (0x1A y 0x3A), confirmando que estos opcodes son necesarios para la ejecución correcta del juego.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - LD A, (BC), LD A, (DE), LDD A, (HL)
- Pan Docs: CPU Registers and Flags - Registros BC, DE, HL como punteros
Integridad Educativa
Lo que Entiendo Ahora
- Punteros en LR35902: Los registros de 16 bits (BC, DE, HL) pueden usarse como punteros para acceder a memoria, proporcionando flexibilidad en las operaciones de transferencia de datos.
- Opcodes gemelos: Muchos opcodes tienen versiones de lectura y escritura (ej: LD A, (BC) y LD (BC), A), permitiendo transferencias bidireccionales.
- Opcodes con post-incremento/decremento: LD A, (HL-) es parte de una familia de opcodes que modifican el puntero después de la operación, útil para bucles eficientes.
- Timing consistente: Los opcodes de carga indirecta consumen 2 M-Cycles (fetch + read), independientemente del registro usado como puntero.
Lo que Falta Confirmar
- LD A, (BC+) y LD A, (DE+): No existen en el set de instrucciones de LR35902, solo existen versiones con HL (LD A, (HL+)). Esto es consistente con la arquitectura, donde HL es el registro más versátil.
- Comportamiento con direcciones inválidas: En hardware real, todas las direcciones de 16 bits son válidas (0x0000-0xFFFF), pero algunas regiones tienen comportamientos especiales (ej: memoria de video, I/O). Nuestro emulador maneja esto a través de la MMU.
Hipótesis y Suposiciones
Ninguna suposición crítica. La implementación sigue directamente la especificación de Pan Docs y los tests validan el comportamiento esperado. El wrap-around de 16 bits para el decremento de HL en LD A, (HL-) es estándar en arquitecturas de 16 bits y está documentado.
Próximos Pasos
- [ ] Investigar por qué BGP=0x00 (paleta completamente blanca) en Tetris DX
- [ ] Verificar si los tiles se cargan en VRAM después de la inicialización
- [ ] Implementar Window (WX/WY) para soportar ventanas superpuestas
- [ ] Implementar Sprites (OAM) para renderizar objetos móviles
- [ ] Continuar implementando opcodes faltantes según se encuentren durante la ejecución de juegos