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

Cargas Directas a Memoria (LD (nn), A y LD A, (nn))

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

Resumen

Se implementaron los opcodes críticos 0xEA (LD (nn), A) y 0xFA (LD A, (nn)) que permiten acceso directo a memoria usando direcciones absolutas de 16 bits especificadas directamente en el código. Estas instrucciones son esenciales para que los juegos puedan guardar y leer variables globales, estados del juego y configuraciones gráficas. El emulador se estaba estrellando en 0xEA cuando ejecutaba Tetris DX, impidiendo que se dibujaran los gráficos. Con esta implementación, el emulador puede avanzar más allá de ese punto y comenzar a renderizar la pantalla de título.

Concepto de Hardware

El direccionamiento directo es un modo de acceso a memoria donde la dirección de 16 bits viene escrita directamente en el código, justo después del opcode. A diferencia del direccionamiento indirecto (ej: LD (HL), A donde la dirección está en el registro HL), aquí la dirección es parte de la instrucción misma.

En la arquitectura LR35902, estas instrucciones son fundamentales para:

  • Acceso a variables globales: Los juegos guardan estados del juego, puntuaciones, vidas, etc. en posiciones fijas de memoria.
  • Acceso a registros de hardware: Muchos registros de I/O (como los del PPU, timers, etc.) están mapeados en direcciones específicas.
  • Inicialización de buffers: Se usan para inicializar áreas de memoria sin necesidad de usar registros intermedios.

LD (nn), A (0xEA): Lee los siguientes 2 bytes del código (dirección en Little-Endian), y escribe el valor del acumulador A en esa dirección. Consume 4 M-Cycles (fetch opcode + fetch 2 bytes + write).

LD A, (nn) (0xFA): Lee los siguientes 2 bytes del código (dirección en Little-Endian), lee el byte de esa dirección, y lo guarda en A. Consume 4 M-Cycles (fetch opcode + fetch 2 bytes + read).

Fuente: Pan Docs - Instruction Set (LD (nn), A y LD A, (nn))

Implementación

Se implementaron dos nuevos métodos en la clase CPU siguiendo el patrón establecido por otras operaciones de carga indirecta como LD (HL), A y LD (BC), A. La diferencia clave es que aquí la dirección se lee del código usando fetch_word() en lugar de obtenerla de un registro.

Componentes creados/modificados

  • src/cpu/core.py: Añadidos métodos _op_ld_nn_ptr_a() (0xEA) y _op_ld_a_nn_ptr() (0xFA), y registrados en la tabla de despacho.
  • tests/test_cpu_load_direct.py: Creado archivo nuevo con 4 tests unitarios que validan escritura, lectura, roundtrip y múltiples direcciones.

Decisiones de diseño

Se siguió el mismo patrón que otras operaciones de memoria indirecta:

  • Uso de fetch_word() para leer la dirección de 16 bits (ya maneja Little-Endian y avance de PC).
  • Logging con logger.debug() para trazar las operaciones durante depuración.
  • Retorno explícito de 4 M-Cycles según la especificación de Pan Docs.
  • Enmascarado implícito: fetch_word() ya maneja el wrap-around de 16 bits.

Los métodos son simples y directos, sin optimizaciones prematuras, priorizando claridad y corrección.

Archivos Afectados

  • src/cpu/core.py - Añadidos métodos _op_ld_nn_ptr_a() y _op_ld_a_nn_ptr(), y registrados en _opcode_table
  • tests/test_cpu_load_direct.py - Creado archivo nuevo con 4 tests unitarios

Tests y Verificación

Se ejecutaron tests unitarios exhaustivos que validan el comportamiento correcto de ambas instrucciones:

Ejecución de Tests

Comando ejecutado:

python3 -m pytest tests/test_cpu_load_direct.py -v

Entorno:

  • OS: macOS (darwin 21.6.0)
  • Python: 3.9.6
  • pytest: 8.4.2

Resultado:

============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-8.4.2, pluggy-1.6.0
collected 4 items

tests/test_cpu_load_direct.py::TestLoadDirect::test_ld_direct_write PASSED [ 25%]
tests/test_cpu_load_direct.py::TestLoadDirect::test_ld_direct_read PASSED [ 50%]
tests/test_cpu_load_direct.py::TestLoadDirect::test_ld_direct_write_read_roundtrip PASSED [ 75%]
tests/test_cpu_load_direct.py::TestLoadDirect::test_ld_direct_different_addresses PASSED [100%]

============================== 4 passed in 0.39s ===============================

Qué valida:

  • test_ld_direct_write: Verifica que LD (nn), A escribe correctamente el valor de A en la dirección especificada, consume 4 M-Cycles, y avanza PC correctamente (3 bytes).
  • test_ld_direct_read: Verifica que LD A, (nn) lee correctamente de la dirección especificada, carga el valor en A, consume 4 M-Cycles, y avanza PC correctamente.
  • test_ld_direct_write_read_roundtrip: Valida que se puede escribir y leer de vuelta el mismo valor, demostrando que ambas instrucciones funcionan correctamente en conjunto.
  • test_ld_direct_different_addresses: Verifica que las instrucciones funcionan con diferentes direcciones de memoria, asegurando que no hay efectos secundarios entre direcciones.

Código del test (fragmento esencial):

def test_ld_direct_write(self):
    """Verificar escritura directa a memoria (LD (nn), A - 0xEA)."""
    mmu = MMU()
    cpu = CPU(mmu)
    cpu.registers.set_pc(0x0100)
    cpu.registers.set_a(0x55)
    
    # Escribir opcode + dirección 0xC000 (Little-Endian: 0x00 0xC0)
    mmu.write_byte(0x0100, 0xEA)  # Opcode
    mmu.write_byte(0x0101, 0x00)  # Byte bajo
    mmu.write_byte(0x0102, 0xC0)  # Byte alto
    
    cycles = cpu.step()
    
    assert mmu.read_byte(0xC000) == 0x55
    assert cycles == 4
    assert cpu.registers.get_pc() == 0x0103

Por qué estos tests demuestran el comportamiento del hardware:

  • Los tests verifican que la dirección se lee correctamente en formato Little-Endian (0x00 0xC0 = 0xC000).
  • Validan que el acceso a memoria es correcto (lectura/escritura en la dirección especificada).
  • Confirman el timing correcto (4 M-Cycles según Pan Docs).
  • Demuestran que el PC avanza correctamente (3 bytes: opcode + 2 bytes de dirección).

Impacto en la ejecución de ROMs

Antes de esta implementación, el emulador se estrellaba cuando encontraba el opcode 0xEA durante la ejecución de Tetris DX. Esto impedía que el juego avanzara lo suficiente para inicializar el PPU y dibujar la pantalla de título. Con estos opcodes implementados, el emulador puede ejecutar más instrucciones y potencialmente llegar a renderizar gráficos.

Fuentes Consultadas

Nota: La implementación sigue exactamente la especificación de Pan Docs para timing (4 M-Cycles) y formato de direcciones (Little-Endian).

Integridad Educativa

Lo que Entiendo Ahora

  • Direccionamiento directo vs indirecto: El direccionamiento directo permite especificar la dirección en el código, mientras que el indirecto la obtiene de un registro. Ambos tienen casos de uso diferentes: directo para variables globales y registros de hardware, indirecto para bucles y punteros dinámicos.
  • Little-Endian en direcciones: Las direcciones de 16 bits se almacenan en memoria con el byte bajo primero (0x00) y el byte alto después (0xC0), formando 0xC000. Esto es consistente con toda la arquitectura LR35902.
  • Timing de instrucciones: Las instrucciones que leen direcciones del código consumen más ciclos porque deben hacer múltiples accesos a memoria: fetch del opcode, fetch de los 2 bytes de dirección, y luego el acceso final (lectura o escritura).

Lo que Falta Confirmar

  • Comportamiento con direcciones de I/O: Aunque los tests validan direcciones de RAM, falta verificar el comportamiento cuando se accede a direcciones mapeadas de I/O (0xFF00-0xFFFF). Esto se validará cuando se pruebe con ROMs reales.
  • Impacto en el renderizado: Aunque se espera que estos opcodes desbloqueen los gráficos, falta ejecutar Tetris DX nuevamente para confirmar que ahora se puede ver la pantalla de título.

Hipótesis y Suposiciones

Se asume que el comportamiento de estas instrucciones con direcciones de I/O es el mismo que con RAM, delegando al MMU el manejo correcto del mapeo. Esto se validará cuando se ejecute el emulador con ROMs reales.

Próximos Pasos

  • [ ] Ejecutar Tetris DX nuevamente para verificar que el emulador ya no se estrelle en 0xEA
  • [ ] Verificar si ahora se pueden ver gráficos en la pantalla (pantalla de título de Tetris)
  • [ ] Si los gráficos aparecen, proceder con la implementación del Joypad (Paso 27 original)
  • [ ] Si aún faltan opcodes, identificarlos mediante logs y continuar implementándolos