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.
Completar INC/DEC de 8 bits (Todas las Variantes)
Resumen
Se completaron todas las variantes de INC/DEC de 8 bits que faltaban en la CPU. En el paso 9 se habían implementado INC/DEC para B, C y A, pero se dejaron fuera D, E, H, L y la versión en memoria (HL). El emulador crasheaba en el opcode 0x1D (DEC E) cuando ejecutaba Tetris DX, lo que confirmaba que faltaban estas instrucciones críticas para el manejo de contadores de bucles. Con esta implementación, la aritmética unaria de 8 bits está completa y el emulador puede avanzar más allá de la inicialización en juegos reales.
Concepto de Hardware
Las instrucciones INC (Increment) y DEC (Decrement) son operaciones aritméticas unarias que incrementan o decrementan un valor en 1. En la Game Boy, estas instrucciones están disponibles para todos los registros de 8 bits (A, B, C, D, E, H, L) y para memoria indirecta (HL).
Patrón de la Tabla de Opcodes:
- Columna x4: INC (B, D, H, (HL))
- Columna x5: DEC (B, D, H, (HL))
- Columna xC: INC (C, E, L, A)
- Columna xD: DEC (C, E, L, A)
Comportamiento de Flags:
- Z (Zero): Se activa si el resultado es 0
- N (Subtract): Siempre 1 en DEC, siempre 0 en INC
- H (Half-Carry/Half-Borrow): Se activa cuando hay carry/borrow del bit 3 al 4 (nibble bajo)
- C (Carry): NO SE TOCA - Esta es una peculiaridad crítica del hardware LR35902
INC/DEC (HL) - Operaciones Read-Modify-Write:
Las instrucciones INC (HL) y DEC (HL) son especiales porque operan sobre memoria indirecta. Estas instrucciones realizan una operación de Read-Modify-Write:
- Leen el valor de memoria en la dirección apuntada por HL
- Modifican el valor (incrementan o decrementan)
- Escriben el nuevo valor de vuelta en memoria
Por esta razón, estas instrucciones consumen 3 M-Cycles (12 T-Cycles) en lugar de 1: uno para leer, uno para escribir, y uno para la operación interna.
Fuente: Pan Docs - CPU Instruction Set (INC/DEC instructions, Flags behavior)
Implementación
Se implementaron los siguientes opcodes faltantes, reutilizando los métodos helper _inc_n y
_dec_n que ya existían desde el paso 9:
Opcodes Implementados
- 0x14: INC D - Incrementa el registro D
- 0x15: DEC D - Decrementa el registro D
- 0x1C: INC E - Incrementa el registro E
- 0x1D: DEC E - Decrementa el registro E (¡El culpable del crash!)
- 0x24: INC H - Incrementa el registro H
- 0x25: DEC H - Decrementa el registro H
- 0x2C: INC L - Incrementa el registro L
- 0x2D: DEC L - Decrementa el registro L
- 0x34: INC (HL) - Incrementa el valor en memoria apuntada por HL
- 0x35: DEC (HL) - Decrementa el valor en memoria apuntada por HL
Componentes Creados/Modificados
src/cpu/core.py:- Añadidos 10 nuevos métodos:
_op_inc_d,_op_dec_d,_op_inc_e,_op_dec_e,_op_inc_h,_op_dec_h,_op_inc_l,_op_dec_l,_op_inc_hl_ptr,_op_dec_hl_ptr - Actualizada la tabla de opcodes para registrar los nuevos handlers
- Los métodos para registros individuales siguen el mismo patrón que B, C y A (1 M-Cycle)
- Los métodos para (HL) implementan Read-Modify-Write (3 M-Cycles)
- Añadidos 10 nuevos métodos:
tests/test_cpu_inc_dec_full.py:- Nuevo archivo de tests con 10 tests completos
- Tests para todos los registros (D, E, H, L)
- Tests para memoria indirecta (HL)
- Tests para preservación del flag C
- Tests para wrap-around y activación de flags
Decisiones de Diseño
Reutilización de Helpers: Se decidió reutilizar los métodos _inc_n y _dec_n
existentes en lugar de duplicar lógica. Esto asegura consistencia en el comportamiento de flags y facilita el mantenimiento.
Read-Modify-Write para (HL): Las instrucciones INC (HL) y DEC (HL) implementan explícitamente
la secuencia Read-Modify-Write usando mmu.read_byte() y mmu.write_byte(). Esto es
importante porque en hardware real, estas operaciones requieren múltiples accesos a memoria.
Logging Consistente: Todos los métodos incluyen logging DEBUG con el mismo formato que las instrucciones existentes, facilitando la depuración.
Archivos Afectados
src/cpu/core.py- Añadidos 10 nuevos métodos de opcodes y actualizada la tabla de opcodestests/test_cpu_inc_dec_full.py- Nuevo archivo con suite completa de tests (10 tests)
Tests y Verificación
Se creó una suite completa de tests TDD con 10 tests que validan todas las variantes implementadas:
Tests Unitarios (pytest)
Comando ejecutado: python3 -m pytest tests/test_cpu_inc_dec_full.py -v
Entorno: macOS, Python 3.9.6
Resultado: 10/10 tests PASSED en 0.59 segundos
Qué valida:
- test_inc_dec_e: Verifica que DEC E funciona correctamente y afecta flags Z, N, H (pero NO C). Este test es crítico porque DEC E (0x1D) es el opcode que causaba el crash en Tetris cuando no estaba implementado. Incluye casos de wrap-around (0x00 -> 0xFF) y activación de flags.
- test_inc_dec_d, test_inc_dec_h, test_inc_dec_l: Verifican que INC/DEC funcionan correctamente para todos los registros restantes.
- test_inc_hl_memory: Verifica que INC (HL) realiza correctamente la operación Read-Modify-Write. Pone 0x0F en (HL), ejecuta INC (HL), y verifica que la memoria tiene 0x10 y Flag H=1 (half-carry).
- test_dec_hl_memory: Similar a test_inc_hl_memory pero para DEC (HL).
- test_inc_hl_memory_zero_flag, test_dec_hl_memory_zero_flag: Verifican que las operaciones en memoria activan correctamente el flag Z cuando el resultado es 0.
- test_inc_preserves_carry, test_dec_preserves_carry: Verifican que el flag C NO se modifica durante operaciones INC/DEC, que es una peculiaridad crítica del hardware LR35902.
Código del Test Crítico (test_inc_dec_e)
def test_inc_dec_e(self, cpu: CPU) -> None:
"""
Verifica que DEC E funciona correctamente y afecta flags Z, N, H.
Este test es crítico porque DEC E (0x1D) es el opcode que causaba
el crash en Tetris cuando no estaba implementado.
"""
# Test 1: DEC E desde valor no cero
cpu.registers.set_e(0x05)
cpu.registers.set_f(0x00) # Limpiar todos los flags
cycles = cpu._op_dec_e()
assert cycles == 1
assert cpu.registers.get_e() == 0x04
assert not cpu.registers.get_flag_z() # No es cero
assert cpu.registers.get_flag_n() # Es una resta
assert not cpu.registers.get_flag_h() # No hay half-borrow
assert not cpu.registers.get_flag_c() # C no se toca
# Test 2: DEC E desde 0x01 (debe dar 0x00 y activar Z)
cpu.registers.set_e(0x01)
cpu.registers.set_f(0x00)
cycles = cpu._op_dec_e()
assert cycles == 1
assert cpu.registers.get_e() == 0x00
assert cpu.registers.get_flag_z() # Es cero
assert cpu.registers.get_flag_n() # Es una resta
assert not cpu.registers.get_flag_h() # No hay half-borrow
assert not cpu.registers.get_flag_c() # C no se toca
# Test 3: DEC E desde 0x00 (wrap-around a 0xFF)
cpu.registers.set_e(0x00)
cpu.registers.set_f(0x00)
cycles = cpu._op_dec_e()
assert cycles == 1
assert cpu.registers.get_e() == 0xFF
assert not cpu.registers.get_flag_z() # No es cero
assert cpu.registers.get_flag_n() # Es una resta
assert cpu.registers.get_flag_h() # Hay half-borrow (0x0 -> 0xF)
assert not cpu.registers.get_flag_c() # C no se toca
# Test 4: INC E desde 0x0F (debe activar H)
cpu.registers.set_e(0x0F)
cpu.registers.set_f(0x00)
cycles = cpu._op_inc_e()
assert cycles == 1
assert cpu.registers.get_e() == 0x10
assert not cpu.registers.get_flag_z() # No es cero
assert not cpu.registers.get_flag_n() # Es una suma
assert cpu.registers.get_flag_h() # Hay half-carry (0xF -> 0x10)
assert not cpu.registers.get_flag_c() # C no se toca
Por qué este test demuestra el comportamiento del hardware:
- Verifica que DEC E decrementa correctamente el valor del registro
- Confirma que el flag Z se activa cuando el resultado es 0
- Confirma que el flag N se activa siempre en DEC (es una resta)
- Confirma que el flag H se activa cuando hay half-borrow (0x0 -> 0xF)
- Crítico: Confirma que el flag C NO se modifica, que es una peculiaridad del hardware LR35902
- Verifica el wrap-around correcto (0x00 -> 0xFF)
Tests de Memoria Indirecta (test_inc_hl_memory)
def test_inc_hl_memory(self, cpu: CPU) -> None:
"""
Verifica INC (HL) con operación Read-Modify-Write.
Pone 0x0F en (HL), ejecuta INC (HL), y verifica que:
- La memoria tiene 0x10
- Flag H=1 (half-carry)
"""
# Configurar HL para apuntar a una dirección de RAM
hl_addr = 0xC000 # Dirección en RAM de la Game Boy
cpu.registers.set_hl(hl_addr)
# Escribir 0x0F en memoria
cpu.mmu.write_byte(hl_addr, 0x0F)
# Limpiar flags
cpu.registers.set_f(0x00)
# Ejecutar INC (HL)
cycles = cpu._op_inc_hl_ptr()
# Verificar ciclos (3 M-Cycles para Read-Modify-Write)
assert cycles == 3
# Verificar que la memoria se actualizó correctamente
assert cpu.mmu.read_byte(hl_addr) == 0x10
# Verificar flags
assert not cpu.registers.get_flag_z() # 0x10 no es cero
assert not cpu.registers.get_flag_n() # Es una suma
assert cpu.registers.get_flag_h() # Hay half-carry (0xF -> 0x10)
assert not cpu.registers.get_flag_c() # C no se toca
Por qué este test demuestra el comportamiento del hardware:
- Verifica que INC (HL) realiza correctamente la operación Read-Modify-Write (lee, modifica, escribe)
- Confirma que consume 3 M-Cycles (12 T-Cycles) en lugar de 1, como requiere el hardware
- Verifica que la memoria se actualiza correctamente
- Confirma que los flags se actualizan correctamente durante la operación en memoria
Validación con ROM Real (Tetris DX)
ROM: Tetris DX (ROM aportada por el usuario, no distribuida)
Modo de ejecución: Headless, límite de 10,000 instrucciones, logging INFO activado
Criterio de éxito: El emulador debe ejecutar sin crashear en el opcode 0x1D (DEC E) que anteriormente causaba el error. El registro E debe cambiar correctamente durante la ejecución, confirmando que DEC E funciona.
Observación:
- ✅ El emulador ejecutó 10,000 instrucciones sin errores
- ✅ No hubo crash en 0x1D (DEC E) - el problema se resolvió completamente
- ✅ El registro E cambió correctamente durante la ejecución (0x00 → 0xC9 → 0xBB → ... → 0x43), confirmando que DEC E funciona
- ✅ El PC está en un bucle entre 0x1383-0x1389, lo cual es normal para un juego esperando V-Blank
- ✅ LY (Línea Y) incrementó correctamente hasta 125 líneas, confirmando que el timing de la PPU funciona
- ✅ El registro A también cambió correctamente, confirmando que otras operaciones funcionan
Logs relevantes (muestra cada 100 instrucciones):
Instrucción 100: PC=0x1384, A=0xCF, E=0xC9, LY=1
Instrucción 200: PC=0x1386, A=0xBF, E=0xBB, LY=2
Instrucción 300: PC=0x1388, A=0x06, E=0xAC, LY=3
Instrucción 400: PC=0x1383, A=0x9E, E=0x9E, LY=5
...
Instrucción 1300: PC=0x1387, A=0x1E, E=0x1D, LY=16
...
Instrucción 10000: PC=0x1386, A=0x43, E=0x43, LY=125
✅ Ejecutadas 10000 instrucciones sin errores
PC final = 0x1386
A = 0x43
E = 0x43
LY = 125
Resultado: verified - El crash en 0x1D (DEC E) se ha resuelto completamente. El emulador ahora puede ejecutar Tetris DX más allá de la inicialización, llegando a un bucle de espera de V-Blank que es comportamiento normal del juego.
Notas legales: La ROM de Tetris DX es aportada por el usuario para pruebas locales. No se distribuye, no se incluye en el repositorio, y no se enlazan descargas.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - Sección INC/DEC instructions
- Pan Docs: CPU Registers and Flags - Comportamiento de flags en INC/DEC
Integridad Educativa
Lo que Entiendo Ahora
- Patrón de Opcodes: Las instrucciones INC/DEC siguen un patrón claro en la tabla de opcodes: columnas x4/x5 para B/D/H/(HL) y columnas xC/xD para C/E/L/A. Este patrón facilita la memorización y la implementación sistemática.
- Preservación del Flag C: Una peculiaridad crítica del hardware LR35902 es que INC/DEC de 8 bits NO modifican el flag C (Carry). Esto es diferente de muchas otras arquitecturas y es importante para la lógica condicional de los juegos.
- Read-Modify-Write: Las operaciones en memoria indirecta (HL) requieren múltiples accesos a memoria, lo que se refleja en el consumo de 3 M-Cycles en lugar de 1.
- Half-Carry/Half-Borrow: El flag H se activa cuando hay carry/borrow del bit 3 al 4 (nibble bajo), lo que es útil para operaciones BCD (Binary Coded Decimal) aunque no se use mucho en la Game Boy.
Lo que Falta Confirmar
- Timing Exacto: Por ahora, asumimos que INC/DEC (HL) consume exactamente 3 M-Cycles según la documentación. Si en el futuro hay problemas de timing con juegos reales, podríamos necesitar verificar el timing exacto con tests de hardware real.
Hipótesis y Suposiciones
Suposición 1: Asumimos que el comportamiento de flags en INC/DEC es idéntico para todos los registros y para memoria indirecta. Esto está respaldado por la documentación, pero no lo hemos verificado con hardware real.
Suposición 2: RESUELTA - Se probó con Tetris DX después de esta implementación y se confirmó que el crash en 0x1D se ha resuelto completamente. El emulador ejecutó 10,000 instrucciones sin errores, llegando a un bucle de espera de V-Blank que es comportamiento normal del juego.
Próximos Pasos
- [x] Ejecutar Tetris DX para verificar que el crash en 0x1D (DEC E) se ha resuelto - COMPLETADO
- [ ] Continuar con la implementación de instrucciones faltantes según los logs de ejecución
- [ ] Analizar el bucle en 0x1383-0x1389 para entender qué está esperando el juego
- [ ] Si ya se implementó el renderizado de background, verificar que Tetris DX muestra la pantalla de título