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.
CPU Nativa: Implementación de I/O Básico (LDH)
Resumen
Se implementaron las instrucciones de I/O de memoria alta LDH (n), A (0xE0) y LDH A, (n) (0xF0) en la CPU nativa (C++). Estas instrucciones son críticas para la comunicación entre la CPU y los registros de hardware (PPU, Timer, etc.). El diagnóstico reveló que el opcode 0xE0 era el siguiente eslabón perdido que causaba el Segmentation Fault cuando el emulador intentaba ejecutar ROMs reales.
Concepto de Hardware
La Game Boy tiene un espacio de memoria mapeado de 64KB (0x0000-0xFFFF). El rango 0xFF00-0xFFFF está reservado para los registros de hardware (I/O). Estos registros controlan componentes críticos como:
- 0xFF40 (LCDC): Control de la PPU (LCD habilitado, sprites, background, etc.)
- 0xFF41 (STAT): Estado de la PPU (modo actual, interrupciones, etc.)
- 0xFF47 (BGP): Paleta de colores del background
- 0xFF00 (JOYP): Estado del joypad
- 0xFF04-0xFF07: Registros del Timer
La instrucción LDH (Load High) es una optimización del hardware que permite
acceder a estos registros de forma más eficiente que una instrucción LD estándar. LDH calcula
la dirección como 0xFF00 + n, donde n es un byte inmediato (0-255).
Esto permite acceder a cualquier registro en el rango 0xFF00-0xFFFF con solo 3 M-Cycles,
en lugar de los 4 M-Cycles que requeriría una instrucción LD indirecta.
¿Por qué es crítico? Cuando un juego inicia, lo primero que hace es configurar estos registros de hardware. Sin LDH, la CPU no puede escribir en LCDC, BGP, o cualquier otro registro de I/O, lo que impide que la PPU se inicialice correctamente y causa que el emulador crashee al intentar ejecutar instrucciones inválidas.
Fuente: Pan Docs - CPU Instruction Set, sección "LDH (n), A" y "LDH A, (n)"
Implementación
Se añadieron dos casos al switch principal de CPU::step() para manejar los opcodes
0xE0 y 0xF0. La implementación es directa: lee el offset inmediato, calcula la dirección
0xFF00 + offset, y realiza la operación de lectura o escritura correspondiente.
Componentes creados/modificados
- src/core/cpp/CPU.cpp: Añadidos casos 0xE0 y 0xF0 en el switch principal
- tests/test_core_cpu_io.py: Suite completa de tests para LDH (nuevo archivo)
Código Implementado
Implementación de LDH (n), A (0xE0):
case 0xE0: // LDH (n), A
{
uint8_t offset = fetch_byte();
uint16_t addr = 0xFF00 + static_cast<uint16_t>(offset);
mmu_->write(addr, regs_->a);
cycles_ += 3;
return 3;
}
Implementación de LDH A, (n) (0xF0):
case 0xF0: // LDH A, (n)
{
uint8_t offset = fetch_byte();
uint16_t addr = 0xFF00 + static_cast<uint16_t>(offset);
regs_->a = mmu_->read(addr);
cycles_ += 3;
return 3;
}
Decisiones de diseño
Timing: Ambas instrucciones consumen 3 M-Cycles según Pan Docs. Esto es consistente con el hecho de que requieren leer un byte inmediato (1 M-Cycle) y realizar una operación de memoria (2 M-Cycles adicionales).
Cast explícito: Se usa static_cast<uint16_t>(offset) para evitar
warnings del compilador y hacer explícita la promoción de tipos. El offset es un uint8_t (0-255),
pero la suma con 0xFF00 requiere un uint16_t.
No hay validación de rango: No se valida que la dirección resultante esté en el rango 0xFF00-0xFFFF porque matemáticamente siempre lo estará (0xFF00 + 0x00 = 0xFF00, 0xFF00 + 0xFF = 0xFFFF). La MMU es responsable de manejar accesos inválidos de forma segura.
Archivos Afectados
src/core/cpp/CPU.cpp- Añadidos casos 0xE0 y 0xF0 en el switch principal (líneas ~906-930)tests/test_core_cpu_io.py- Suite completa de tests para LDH (nuevo archivo, 5 tests)
Tests y Verificación
Se creó una suite completa de tests unitarios en test_core_cpu_io.py que valida:
- test_ldh_write: Verifica que LDH (n), A escribe correctamente en 0xFF00 + n
- test_ldh_read: Verifica que LDH A, (n) lee correctamente de 0xFF00 + n
- test_ldh_write_lcdc: Caso específico para escribir en LCDC (0xFF40)
- test_ldh_read_stat: Caso específico para leer de STAT (0xFF41)
- test_ldh_offset_wraparound: Verifica que offsets grandes (0xFF) funcionan correctamente
Comando ejecutado:
pytest tests/test_core_cpu_io.py -v
Resultado esperado: 5 tests pasando
Código del Test (ejemplo):
def test_ldh_write(self):
"""Test: LDH (n), A (0xE0) escribe A en 0xFF00 + n."""
mmu = PyMMU()
regs = PyRegisters()
cpu = PyCPU(mmu, regs)
regs.pc = 0x8000
regs.a = 0xAB
mmu.write(0x8000, 0xE0) # Opcode LDH (n), A
mmu.write(0x8001, 0x40) # offset 'n' (para 0xFF40 - LCDC)
cycles = cpu.step()
assert mmu.read(0xFF40) == 0xAB
assert regs.pc == 0x8002
assert cycles == 3
Validación Nativa: Todos los tests validan el módulo compilado C++ a través del wrapper Cython. No hay código Python intermedio; la CPU nativa ejecuta directamente las instrucciones LDH.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set, sección "LDH (n), A" y "LDH A, (n)" - Timing: 3 M-Cycles
- Pan Docs: Memory Map, sección "I/O Registers" (0xFF00-0xFFFF)
Integridad Educativa
Lo que Entiendo Ahora
- LDH es una optimización del hardware: Permite acceder a registros de I/O con menos ciclos que una instrucción LD indirecta estándar. Esto es crítico porque los juegos acceden constantemente a estos registros durante la ejecución.
- El rango 0xFF00-0xFFFF es mapeado a hardware: Cada dirección en este rango corresponde a un registro específico del hardware. La CPU no puede simplemente "escribir en memoria" aquí; cada escritura tiene efectos secundarios en el hardware emulado.
- La inicialización del juego depende de LDH: Sin esta instrucción, los juegos no pueden configurar la PPU, el Timer, o cualquier otro componente de hardware, lo que causa que el emulador crashee inmediatamente.
Lo que Falta Confirmar
- Comportamiento de registros de solo lectura: Algunos registros de I/O son de solo lectura (ej: STAT tiene bits de solo lectura). La MMU debería manejar esto, pero necesitamos verificar que los juegos no intenten escribir en estos registros de forma incorrecta.
- Efectos secundarios de escritura: Algunos registros tienen efectos secundarios cuando se escriben (ej: escribir en DIV resetea el contador). Esto se implementará cuando migremos el Timer a C++.
Hipótesis y Suposiciones
Suposición: Asumimos que la MMU maneja correctamente los accesos a registros de I/O. En la implementación actual, la MMU simplemente lee/escribe en memoria, pero en el futuro necesitaremos implementar mapeo específico para cada registro de hardware (ej: cuando se escribe en DIV, resetear el contador).
Próximos Pasos
- [ ] Ejecutar el emulador con una ROM real (ej: Tetris) y verificar que avanza más allá del opcode 0xE0
- [ ] Identificar el siguiente opcode no implementado que cause el siguiente crash
- [ ] Implementar más instrucciones de I/O si es necesario (ej: LD (C), A y LD A, (C))
- [ ] Verificar que la PPU puede recibir configuraciones correctamente a través de LDH