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

CPU Nativa: Implementación de I/O Básico (LDH)

Fecha: 2025-12-19 Step ID: 0134 Estado: Verified

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