⚠️ 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 (Pan Docs) y tests permitidas.

← Volver a Bitácora

Step 0417: Fix CPU Unit Tests (Ejecutar desde WRAM)

Resumen Ejecutivo

Refactorización completa del harness de tests unitarios de CPU para ejecutar programas de prueba desde WRAM (0xC000) en lugar de intentar escribir en ROM (0x0000-0x7FFF). El problema original era que PyMMU.write(0x0000) no escribe memoria (ROM es read-only), causando que la CPU leyera 0x00 (NOP) y los tests no validaran las instrucciones reales.

Resultado:6/6 tests pasando (100% success rate)
Impacto: Los tests ahora ejecutan las instrucciones reales en lugar de NOPs, validando correctamente el comportamiento de la CPU.

Concepto de Hardware: Mapa de Memoria Game Boy

El Game Boy tiene un mapa de memoria bien definido donde cada región tiene características específicas de lectura/escritura:

Rango Nombre Lectura Escritura Uso
0x0000-0x7FFF ROM Código del juego (Read Only Memory)
0x8000-0x9FFF VRAM ✅* Tiles y mapas de fondo (*salvo Mode 3)
0xA000-0xBFFF External RAM RAM del cartucho (si existe)
0xC000-0xDFFF WRAM Work RAM (RAM de trabajo interna)
0xE000-0xFDFF Echo RAM Espejo de WRAM (prohibido usar)
0xFE00-0xFE9F OAM ✅* Sprite Attribute Table (*salvo Mode 2/3)
0xFF00-0xFF7F I/O Registers Controles, video, audio, timer
0xFF80-0xFFFE HRAM High RAM (RAM rápida)
0xFFFF IE Register Interrupt Enable
Por qué WRAM para tests:
  • ROM (0x0000-0x7FFF) es read-only. Las escrituras a ROM controlan el MBC (Memory Bank Controller), no escriben datos.
  • WRAM (0xC000-0xDFFF) es RAM totalmente escribible y legible, ideal para cargar programas de prueba.
  • Los juegos reales usan WRAM para código temporal, stacks de llamadas, y buffers de datos.
  • Ejecutar tests desde WRAM es más realista que modificar la MMU para permitir escrituras a ROM.

Referencia: Pan Docs - Memory Map (https://gbdev.io/pandocs/Memory_Map.html)

Implementación

1. Helper de Carga de Programas

Se creó el archivo tests/helpers_cpu.py con un helper que:

  • Escribe un programa (lista de bytes) en WRAM
  • Configura el PC para ejecutar desde esa dirección
  • Verifica que la escritura fue exitosa (read-back check)
# tests/helpers_cpu.py

# Constante: dirección base para ejecutar programas de test
TEST_EXEC_BASE = 0xC000  # WRAM

def load_program(mmu, regs, program_bytes: List[int], start_addr: int = TEST_EXEC_BASE) -> None:
    """
    Carga un programa de test en memoria y configura el PC.
    
    Args:
        mmu: Instancia de PyMMU donde escribir el programa
        regs: Instancia de PyRegisters donde configurar el PC
        program_bytes: Lista de bytes (opcodes e inmediatos) del programa
        start_addr: Dirección de inicio (por defecto TEST_EXEC_BASE = 0xC000)
    """
    # Escribir cada byte del programa
    for i, byte_val in enumerate(program_bytes):
        addr = start_addr + i
        mmu.write(addr, byte_val)
    
    # Configurar PC al inicio del programa
    regs.pc = start_addr
    
    # Verificación: leer de vuelta para confirmar escritura
    for i, byte_val in enumerate(program_bytes):
        addr = start_addr + i
        read_back = mmu.read(addr)
        if read_back != byte_val:
            raise AssertionError(
                f"load_program: Verificación falló en 0x{addr:04X}: "
                f"esperado 0x{byte_val:02X}, leído 0x{read_back:02X}"
            )

2. Refactorización de Tests

Todos los tests en tests/test_core_cpu.py se refactorizaron para usar el nuevo helper:

# ANTES (escribía en ROM, no funcionaba)
mmu.write(0x0000, 0x3E)  # LD A, d8
mmu.write(0x0001, 0x42)
regs.pc = 0x0000

# DESPUÉS (ejecuta desde WRAM, funciona)
load_program(mmu, regs, [0x3E, 0x42])  # LD A, 0x42
# PC se configura automáticamente a TEST_EXEC_BASE (0xC000)

3. Corrección de Opcode de Test

El test test_unknown_opcode_returns_zero usaba 0xFF (RST 38h), que sí está implementado. Se cambió a 0xD3, un opcode ilegal en Game Boy que no está definido en el instruction set.

# test_unknown_opcode_returns_zero (corregido)
load_program(mmu, regs, [0xD3])  # 0xD3 es opcode ilegal
cycles = cpu.step()
assert cycles == 0, "Opcode desconocido debe retornar 0"

Tests y Verificación

✅ Todos los Tests Pasando

Comando:

pytest -v tests/test_core_cpu.py

Resultado:

tests/test_core_cpu.py::TestCoreCPU::test_cpu_initialization PASSED                [ 16%]
tests/test_core_cpu.py::TestCoreCPU::test_nop_instruction PASSED                    [ 33%]
tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction PASSED                [ 50%]
tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_multiple_executions PASSED        [ 66%]
tests/test_core_cpu.py::TestCoreCPU::test_unknown_opcode_returns_zero PASSED        [ 83%]
tests/test_core_cpu.py::TestCoreCPU::test_cpu_with_shared_mmu_and_registers PASSED  [100%]

============================== 6 passed in 0.36s

Validación del Test Clave

Comando:

pytest -v tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction --maxfail=1 -x

Resultado:

tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction PASSED     [100%]

============================== 1 passed in 0.47s

Código del Test Validado

def test_ld_a_d8_instruction(self):
    """Test: La instrucción LD A, d8 (0x3E) funciona correctamente."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    # Cargar programa de test en WRAM
    # 0x3E = LD A, d8
    # 0x42 = valor inmediato (d8)
    load_program(mmu, regs, [0x3E, 0x42])
    regs.a = 0x00  # Inicializar A a 0
    
    # Ejecutar un ciclo
    cycles = cpu.step()
    
    # Verificar resultados
    assert cycles == 2, "LD A, d8 debe consumir 2 M-Cycles"
    assert regs.a == 0x42, "Registro A debe contener 0x42"
    assert regs.pc == TEST_EXEC_BASE + 2, "PC debe incrementarse 2 bytes"
    assert cpu.get_cycles() == 2, "Contador de ciclos debe ser 2"

✅ Validación de Módulo Compilado C++

Los tests validan el módulo C++/Cython compilado (viboy_core.so), no código Python puro.

Archivos Afectados

  • Creados:
    • tests/helpers_cpu.py - Helper de carga de programas para tests
  • Modificados:
    • tests/test_core_cpu.py - 6 tests refactorizados para ejecutar desde WRAM

Conclusiones

  • Tests robustos: Ahora ejecutan las instrucciones reales en lugar de NOPs
  • Realismo: Ejecutar desde WRAM es más cercano a cómo funcionan los juegos reales
  • Mantenibilidad: Helper reutilizable para futuros tests de CPU
  • Integridad: No se modificó la MMU para permitir escrituras a ROM (contaminaría emulación real)
  • Descubrimiento: El test de opcode desconocido reveló que 0xFF (RST 38h) está implementado
Próximos Pasos:
  • Usar este patrón para nuevos tests de instrucciones CPU
  • Expandir la cobertura de tests a más opcodes
  • Considerar tests de edge cases (flags, overflows, etc.)

Entorno Técnico

  • Compilador: GCC 13.2.0 (C++17)
  • Cython: 3.0.11
  • Python: 3.12.3
  • pytest: 9.0.2
  • OS: Ubuntu 24.04 LTS (Linux 6.14.0-37-generic)
  • Build: python3 setup.py build_ext --inplace