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

Opcodes LD Indirect (0x0A, 0x1A, 0x3A)

Fecha: 2025-12-17 Step ID: 0034 Estado: Verified

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 table
  • tests/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 table
  • tests/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

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