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.
I/O Dinámico y Mapeo de Registros
Resumen
¡ISA (Instruction Set Architecture) de la CPU completada al 100%! Se implementaron los dos últimos opcodes faltantes de la CPU LR35902: LD (C), A (0xE2) y LD A, (C) (0xF2). Estas instrucciones permiten acceso dinámico a los registros de hardware usando el registro C como offset, lo que es especialmente útil para bucles de inicialización. Además, se mejoró significativamente la visibilidad del sistema añadiendo constantes para todos los registros de hardware (LCDC, STAT, BGP, etc.) y mejorando el logging de la MMU para mostrar nombres de registros en lugar de direcciones hexadecimales. Con esto, el emulador puede ejecutar código completo de juegos reales y los logs ahora muestran información legible como "IO WRITE: LCDC = 0x91" en lugar de "Escribiendo en 0xFF40".
Concepto de Hardware
LD (C), A y LD A, (C) - Acceso I/O Dinámico
La Game Boy controla sus periféricos (pantalla, audio, timers, interrupciones) mediante Memory Mapped I/O. Esto significa que escribir en ciertas direcciones de memoria (rango 0xFF00-0xFF7F) no escribe en RAM, sino que controla hardware real.
Ya teníamos implementadas las instrucciones LDH (n), A (0xE0) y LDH A, (n) (0xF0), que
leen un byte inmediato n y acceden a 0xFF00 + n. Sin embargo, estas instrucciones requieren
que el offset esté embebido en el código, lo que las hace estáticas.
LD (C), A (0xE2) y LD A, (C) (0xF2) son variantes optimizadas que usan el registro
C como offset dinámico. Esto permite:
- Bucles de inicialización: Un juego puede inicializar múltiples registros de hardware en un bucle, incrementando C en cada iteración.
- Ahorro de espacio: 1 byte menos que LDH (no necesita leer byte inmediato).
- Flexibilidad: El offset puede calcularse en tiempo de ejecución.
Ejemplo práctico: Tetris DX usa LD (C), A para escribir en LCDC (0xFF40), STAT (0xFF41),
BGP (0xFF47), etc. en un bucle, incrementando C desde 0x40 hasta 0x4F.
Registros de Hardware (Memory Mapped I/O)
La Game Boy tiene decenas de registros de hardware mapeados en el rango 0xFF00-0xFF7F. Los más importantes son:
- LCDC (0xFF40): LCD Control - Enciende/Apaga la pantalla, configuración de fondo/sprites.
- STAT (0xFF41): LCD Status - Estado actual del LCD (modo, flags de interrupción).
- SCY/SCX (0xFF42/43): Scroll Y/X - Posición del fondo.
- LY (0xFF44): Línea actual que se está dibujando (0-153, solo lectura).
- BGP (0xFF47): Background Palette Data - Paleta de colores para el fondo.
- IF (0xFF0F): Interrupt Flag - Flags de interrupciones pendientes.
- IE (0xFFFF): Interrupt Enable - Máscara de interrupciones habilitadas.
Fuente: Pan Docs - Memory Map / I/O Ports
Implementación
Se implementaron los dos opcodes faltantes en src/cpu/core.py y se mejoró significativamente el sistema
de logging en src/memory/mmu.py para hacer visible qué registros de hardware se están accediendo.
Componentes creados/modificados
_op_ld_c_a()ensrc/cpu/core.py:- Implementa LD (C), A (0xE2).
- Calcula dirección I/O:
0xFF00 + C. - Escribe el valor de A en esa dirección.
- Consume 2 M-Cycles (1 menos que LDH porque no lee byte inmediato).
_op_ld_a_c()ensrc/cpu/core.py:- Implementa LD A, (C) (0xF2).
- Calcula dirección I/O:
0xFF00 + C. - Lee el valor de esa dirección y lo carga en A.
- Consume 2 M-Cycles.
- Constantes de registros de hardware en
src/memory/mmu.py:- Añadidas constantes para todos los registros principales:
IO_LCDC,IO_STAT,IO_BGP,IO_IF,IO_IE, etc. - Diccionario
IO_REGISTER_NAMESque mapea direcciones a nombres legibles.
- Añadidas constantes para todos los registros principales:
- Logging mejorado en
MMU.write_byte():- Detecta escrituras en el rango I/O (0xFF00-0xFF7F).
- Registra log informativo con nombre del registro:
"IO WRITE: LCDC = 0x91". - Si el registro no está en el diccionario, muestra formato genérico:
"IO WRITE: IO_0xFF50 = 0x42".
Opcodes añadidos a la tabla de despacho
0xE2:_op_ld_c_a(LD (C), A)0xF2:_op_ld_a_c(LD A, (C))
Decisiones de diseño
- Timing: LD (C), A y LD A, (C) consumen 2 M-Cycles (vs 3 de LDH) porque no necesitan leer un byte inmediato. Esto coincide con la documentación de Pan Docs.
- Logging: Se usa
logger.info()para escrituras I/O (nodebug) porque es información valiosa para entender qué está haciendo el juego. El log es "lazy" (solo se formatea si el nivel de logging está activado). - Constantes: Se definieron constantes para los registros más comunes, pero el sistema es extensible.
Si se necesita añadir más registros en el futuro, solo hay que añadirlos al diccionario
IO_REGISTER_NAMES.
Archivos Afectados
src/cpu/core.py- Añadidos métodos_op_ld_c_a()y_op_ld_a_c(). Añadidos opcodes 0xE2 y 0xF2 a la tabla de despacho.src/memory/mmu.py- Añadidas constantes de registros de hardware (IO_LCDC, IO_STAT, etc.) y diccionarioIO_REGISTER_NAMES. Mejorado métodowrite_byte()para logging informativo de escrituras I/O.tests/test_cpu_io_c.py- Archivo nuevo con 6 tests unitarios:- 3 tests para LD (C), A (LCDC, STAT, BGP, wrap-around)
- 2 tests para LD A, (C) (STAT, LCDC)
- 1 test para wrap-around de dirección I/O
Tests y Verificación
Se ejecutó la suite completa de tests TDD para validar los dos opcodes implementados.
Ejecución de Tests
Comando ejecutado:
python3 -m pytest tests/test_cpu_io_c.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 6 items
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_c_a_write PASSED [ 16%]
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_c_a_write_stat PASSED [ 33%]
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_c_a_write_bgp PASSED [ 50%]
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_a_c_read PASSED [ 66%]
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_a_c_read_lcdc PASSED [ 83%]
tests/test_cpu_io_c.py::TestIOAccessViaC::test_ld_c_a_wrap_around PASSED [100%]
============================== 6 passed in 0.19s ==============================
Qué valida:
- LD (C), A: Verifica que la escritura en
0xFF00 + Cfunciona correctamente para diferentes valores de C (LCDC=0x40, STAT=0x41, BGP=0x47). Valida que C y A no se modifican después de la escritura. Confirma que consume 2 M-Cycles (correcto según documentación). - LD A, (C): Verifica que la lectura de
0xFF00 + Ccarga correctamente el valor en A. Valida que C no se modifica. Confirma timing de 2 M-Cycles. - Wrap-around: Verifica que con C=0xFF, la dirección calculada es 0xFFFF (IE), demostrando que el cálculo de dirección funciona correctamente incluso en el límite.
Código del Test (Fragmento Esencial)
Ejemplo de test para LD (C), A:
def test_ld_c_a_write(self):
"""Test: LD (C), A escribe correctamente en 0xFF00 + C."""
mmu = MMU()
cpu = CPU(mmu)
# Configurar estado inicial
cpu.registers.set_c(0x40) # LCDC
cpu.registers.set_a(0x91)
cpu.registers.set_pc(0x8000)
# Escribir opcode en memoria
mmu.write_byte(0x8000, 0xE2) # LD (C), A
# Ejecutar instrucción
cycles = cpu.step()
# Verificar que se escribió correctamente en 0xFF40 (LCDC)
assert mmu.read_byte(IO_LCDC) == 0x91, "LCDC debe ser 0x91"
assert cpu.registers.get_c() == 0x40, "C no debe cambiar"
assert cpu.registers.get_a() == 0x91, "A no debe cambiar"
assert cycles == 2, "Debe consumir 2 M-Cycles"
Ruta completa: tests/test_cpu_io_c.py
Validación con ROM Real (Tetris DX)
ROM: Tetris DX (ROM aportada por el usuario, no distribuida)
Modo de ejecución: Headless, con logging activado a nivel INFO para ver escrituras I/O.
Criterio de éxito: El emulador debe ejecutar el opcode 0xE2 sin errores de "Opcode no implementado" y mostrar logs informativos de escrituras I/O con nombres de registros legibles.
Observación: Al ejecutar Tetris DX, el emulador ahora:
- Ejecuta correctamente el opcode 0xE2 (LD (C), A) que estaba causando el error.
- Muestra logs informativos como:
IO WRITE: LCDC = 0x91 (addr: 0xFF40) IO WRITE: STAT = 0x85 (addr: 0xFF41) IO WRITE: BGP = 0xE4 (addr: 0xFF47) - El juego avanza más allá de la inicialización y entra en un bucle esperando que el registro LY (0xFF44) cambie, lo cual es el comportamiento esperado ya que aún no tenemos implementada la PPU (Unidad de Procesamiento de Gráficos).
Resultado: Verified - Los opcodes funcionan correctamente y el sistema de logging muestra información valiosa para depuración.
Notas legales: La ROM de Tetris DX es propiedad del usuario y no se distribuye ni se incluye en el repositorio. Se usa únicamente para pruebas locales de validación del emulador.
Fuentes Consultadas
- Pan Docs - CPU Instruction Set: https://gbdev.io/pandocs/CPU_Instruction_Set.html
- LD (C), A (opcode 0xE2)
- LD A, (C) (opcode 0xF2)
- Pan Docs - Memory Map: https://gbdev.io/pandocs/Memory_Map.html
- I/O Ports (0xFF00-0xFF7F)
- Registros de hardware (LCDC, STAT, BGP, etc.)
Integridad Educativa
Lo que Entiendo Ahora
- Memory Mapped I/O: La Game Boy usa direcciones de memoria para controlar hardware. Escribir en 0xFF40 no escribe en RAM, sino que configura el LCD. Esto es más eficiente que tener instrucciones especiales para cada periférico.
- LD (C), A vs LDH (n), A: La diferencia clave es que LD (C), A usa un registro (C) como offset, lo que permite bucles dinámicos. LDH (n), A usa un byte inmediato, lo que es estático pero más directo. LD (C), A es 1 ciclo más rápido porque no necesita leer el byte inmediato.
- Registros de hardware: Cada registro tiene un propósito específico. LCDC controla si la pantalla está encendida, STAT indica el modo actual del LCD (H-Blank, V-Blank, OAM, etc.), BGP define los colores del fondo. Entender estos registros es crucial para implementar la PPU más adelante.
- Logging informativo: Mostrar nombres de registros en lugar de direcciones hexadecimales hace que los logs sean mucho más legibles y útiles para depuración. Esto es especialmente importante cuando se trabaja con hardware complejo como la Game Boy.
Lo que Falta Confirmar
- Comportamiento de registros de solo lectura: Algunos registros como LY (0xFF44) son de solo lectura. La MMU actualmente permite escribir en ellos, pero el hardware real ignora las escrituras. Esto debería implementarse cuando se añada la PPU.
- Registros con comportamiento especial: Algunos registros tienen comportamientos especiales al escribir. Por ejemplo, escribir en DMA (0xFF46) inicia una transferencia. DIV (0xFF04) se resetea al escribir cualquier valor. Estos comportamientos se implementarán cuando se añadan los subsistemas correspondientes (DMA, Timer).
- Rango completo de registros: Se definieron constantes para los registros más comunes, pero hay muchos más en el rango 0xFF00-0xFF7F. A medida que se implementen más subsistemas (APU, Timer, etc.), se añadirán más constantes y se mejorará el logging.
Hipótesis y Suposiciones
Timing de 2 M-Cycles: Asumimos que LD (C), A y LD A, (C) consumen 2 M-Cycles basándonos en la documentación de Pan Docs. Esto es consistente con el hecho de que no necesitan leer un byte inmediato (a diferencia de LDH que consume 3 M-Cycles). Sin embargo, no hemos validado esto con hardware real, solo con documentación.
Comportamiento de wrap-around: Asumimos que si C=0xFF, la dirección calculada es 0xFFFF (IE), lo cual es correcto matemáticamente. El test de wrap-around valida esto, pero no hemos verificado si el hardware real tiene algún comportamiento especial en este caso.
Próximos Pasos
- [x] Implementar LD (C), A (0xE2) y LD A, (C) (0xF2)
- [x] Añadir constantes de registros de hardware en MMU
- [x] Mejorar logging de escrituras I/O
- [x] Crear tests TDD para los nuevos opcodes
- [ ] Implementar PPU (Unidad de Procesamiento de Gráficos): El siguiente gran módulo. La PPU es responsable de renderizar la pantalla, actualizar el registro LY, generar interrupciones V-Blank y H-Blank, y gestionar sprites y fondos. Sin la PPU, los juegos se quedan en bucles infinitos esperando que LY cambie.
- [ ] Implementar Timer (DIV, TIMA, TMA, TAC): Subsistema que genera interrupciones de timer y mantiene el contador de división.
- [ ] Implementar Interrupciones: Sistema completo de manejo de interrupciones (V-Blank, H-Blank, Timer, Joypad).
- [ ] Implementar Joypad: Lectura de botones y direcciones del controlador.
Nota: Con la implementación de estos dos opcodes, la CPU LR35902 está teóricamente completa al 100%. Todos los opcodes del set de instrucciones están implementados. El siguiente paso lógico es implementar la PPU para que los juegos puedan renderizar gráficos y avanzar más allá de los bucles de espera.