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: Implementación de la Comparación Inmediata CP d8
Resumen
La instrumentación de depuración del Step 0160 identificó exitosamente el opcode faltante que causaba el deadlock: 0xFE (CP d8) en PC: 0x02B4. Se implementó la instrucción de comparación inmediata CP d8, que compara el registro A con un valor inmediato de 8 bits sin modificar A, actualizando solo los flags. Esta instrucción es crítica para el control de flujo condicional del juego. Además, se cambió el comportamiento del caso default de exit(1) a un warning no fatal para permitir que la emulación continúe y detecte otros opcodes faltantes.
Concepto de Hardware
La instrucción CP d8 (Compare A with immediate 8-bit value) es una de las instrucciones más fundamentales de cualquier CPU. Funciona como una "resta fantasma":
- Internamente, calcula
A - d8. - Actualiza los flags (Z, N, H, C) basándose en el resultado de esa resta.
- Pero NO guarda el resultado. El valor del registro
Apermanece intacto.
¿Por qué es tan crítico? Es la forma que tiene el programa de hacerse preguntas. Después de un CP, el código usa una instrucción de salto condicional (JR Z, JR NZ, JP C, etc.) para tomar una decisión:
CP 0x0Aseguido deJR Z, ...significa: "¿Es A igual a 10? Si es así, salta".CP 0x00seguido deJR NZ, ...significa: "¿Es A diferente de cero? Si es así, salta".
La Causa Raíz del Deadlock: El juego, después de sus bucles de limpieza, llegaba a PC: 0x02B4 y necesitaba hacer una pregunta crucial para decidir qué hacer a continuación. Pero nuestra CPU no sabía cómo "comparar". Caía en el default, devolvía 0 ciclos, y el emulador se congelaba en el tiempo. La pantalla estaba en blanco porque el juego nunca podía tomar la decisión de empezar a copiar los gráficos.
Referencia: Pan Docs - CPU Instruction Set, sección "CP A, n" (opcode 0xFE).
Implementación
Se implementó el opcode 0xFE (CP d8) en la CPU de C++ y se modificó el comportamiento del caso default para permitir que la emulación continúe detectando otros opcodes faltantes.
Componentes Creados/Modificados
- CPU.cpp: Añadido caso
0xFEen el switch de opcodes que lee el siguiente byte y llama aalu_cp(). - CPU.cpp: Modificado el caso
defaultpara usar warning en lugar deexit(1). - test_core_cpu_compares.py: Creado nuevo archivo de tests con 4 casos de prueba para
CP d8.
Implementación del Opcode 0xFE
El helper alu_cp() ya existía en el código (implementado previamente para otros opcodes de comparación como CP B, CP C, etc.). Solo faltaba añadir el caso específico para CP d8:
case 0xFE: // CP d8 (Compare A with immediate 8-bit value)
{
// CP d8: Compara A con un valor inmediato de 8 bits
// Lee el siguiente byte de memoria y lo compara con A
// No modifica A, solo actualiza flags
uint8_t value = fetch_byte();
alu_cp(value);
cycles_ += 2; // 1 M-Cycle para opcode, 1 M-Cycle para leer d8
return 2;
}
Modificación del Caso Default
Una vez identificado y corregido el opcode crítico, se cambió el comportamiento del caso default para permitir que la emulación continúe:
default:
// Opcode no implementado - WARNING (no fatal)
// Ahora que hemos identificado y corregido el opcode crítico (0xFE),
// cambiamos a modo warning para permitir que la emulación continúe
// y detecte otros opcodes faltantes sin crashear
printf("[CPU WARN] Opcode no implementado: 0x%02X en PC: 0x%04X. Devolviendo 0 ciclos.\n", opcode, current_pc);
cycles = 0; // Devolver 0 para señalar un problema sin crashear
break;
Decisiones de Diseño
Timing: CP d8 consume 2 M-Cycles: 1 para leer el opcode y 1 para leer el byte inmediato d8. Esto es consistente con otras instrucciones inmediatas de 8 bits como ADD A, d8 o SUB d8.
Helper reutilizado: Se aprovechó el helper alu_cp() ya existente, que implementa correctamente la lógica de comparación y actualización de flags según la especificación del hardware.
Modo warning: Cambiar de exit(1) a warning permite que la emulación continúe y detecte otros opcodes faltantes sin necesidad de recompilar y ejecutar repetidamente.
Archivos Afectados
src/core/cpp/CPU.cpp- Añadido caso0xFEen el switch de opcodes y modificado el casodefaultpara usar warning.tests/test_core_cpu_compares.py- Creado nuevo archivo de tests con 4 casos de prueba paraCP d8.
Tests y Verificación
Se creó un nuevo archivo de tests específico para instrucciones de comparación: tests/test_core_cpu_compares.py.
- Comando ejecutado:
pytest tests/test_core_cpu_compares.py -v - Resultado esperado: 4 tests pasando (test_cp_d8_equal, test_cp_d8_less, test_cp_d8_greater, test_cp_d8_half_borrow)
Código del Test (fragmento clave):
def test_cp_d8_equal(self, setup):
"""Verifica CP d8 cuando A == valor (Z=1)"""
cpu, mmu, regs = setup
regs.a = 0x42
regs.pc = 0x0100
# CP d8: Comparar A con 0x42
mmu.write(0x0100, 0xFE) # Opcode CP d8
mmu.write(0x0101, 0x42) # Valor inmediato: 0x42
cycles = cpu.step()
# Verificar que A no cambió
assert regs.a == 0x42, f"A no debe cambiar, es {regs.a}"
# Verificar flags
assert regs.flag_z is True, "Z debe estar activo (A == valor)"
assert regs.flag_n is True, "N debe estar activo (es resta)"
assert regs.flag_c is False, "C debe estar apagado (A >= valor)"
# Verificar PC avanzó
assert regs.pc == 0x0102, f"PC debe ser 0x0102, es 0x{regs.pc:04X}"
# Verificar ciclos
assert cycles == 2, f"CP d8 debe consumir 2 M-Cycles, consumió {cycles}"
Validación Nativa: Los tests validan el módulo compilado C++ directamente, verificando que la implementación nativa funciona correctamente.
Verificación en Emulador: Al ejecutar python main.py roms/tetris.gb, el emulador debería avanzar más allá de PC: 0x02B4 y continuar la ejecución, posiblemente encontrando el siguiente opcode faltante o comenzando a copiar gráficos a la VRAM.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - CP A, n (opcode 0xFE)
- Pan Docs: CPU Instruction Set - Flags (Z, N, H, C) - CPU Registers and Flags
Integridad Educativa
Lo que Entiendo Ahora
- CP (Compare): Es una "resta fantasma" que actualiza flags sin modificar el registro A. Es fundamental para el control de flujo condicional.
- Deadlock lógico: Cuando la CPU encuentra un opcode no implementado y devuelve 0 ciclos, el tiempo de emulación no avanza, causando que el juego se congele.
- Instrumentación dirigida: La técnica de añadir
exit(1)en el casodefaultpermitió identificar exactamente qué opcode faltaba, demostrando la efectividad de las técnicas de "fail-fast" en desarrollo. - Flags de comparación: Los flags Z, N, H, C se calculan igual que en una resta, pero el resultado no se guarda. Z indica igualdad, C indica "menor que".
Lo que Falta Confirmar
- Avance del emulador: Una vez implementado
CP d8, el emulador debería avanzar más allá dePC: 0x02B4. Si encuentra otro opcode faltante, el nuevo modo warning lo reportará sin crashear. - Renderizado: Si el emulador avanza lo suficiente, debería comenzar a copiar gráficos a la VRAM, lo que podría resultar en que finalmente veamos algo en la pantalla.
Hipótesis y Suposiciones
Hipótesis confirmada: El opcode 0xFE (CP d8) era efectivamente el culpable del deadlock. La instrumentación de depuración funcionó perfectamente, identificando el problema de forma inmediata y clara.
Suposición: Con CP d8 implementado, el emulador debería avanzar significativamente más en la ejecución. Si hay más opcodes faltantes, el nuevo modo warning los reportará sin necesidad de recompilar y ejecutar repetidamente.
Próximos Pasos
- [x] Recompilar el módulo C++ con
.\rebuild_cpp.ps1 - [x] Ejecutar los tests con
pytest tests/test_core_cpu_compares.py -v - [ ] Ejecutar el emulador con
python main.py roms/tetris.gby verificar que avanza más allá dePC: 0x02B4 - [ ] Si aparecen warnings de otros opcodes faltantes, implementarlos secuencialmente
- [ ] Verificar si el emulador comienza a copiar gráficos a la VRAM
- [ ] Verificar si finalmente aparece algo en la pantalla