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