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

ALU con Operandos Inmediatos (d8)

Fecha: 2025-12-17 Step ID: 0018 Estado: Verified

Resumen

Se completó el conjunto de operaciones ALU inmediatas (con operandos de 8 bits embebidos en el código), implementando ADC A, d8 (0xCE), SBC A, d8 (0xDE), AND d8 (0xE6), XOR d8 (0xEE) y OR d8 (0xF6). Estas instrucciones son críticas porque permiten operar con constantes directamente del código, sin necesidad de cargar valores en registros primero. La implementación reutiliza los helpers genéricos ya existentes (_adc, _sbc, _and, _xor, _or), siguiendo el principio DRY (Don't Repeat Yourself). Con esto, la CPU ahora tiene capacidad computacional completa para operaciones de 8 bits, lo que permite que juegos como Tetris DX avancen más allá de la inicialización.

Concepto de Hardware

El direccionamiento inmediato es un modo de direccionamiento donde el operando (el valor a operar) está embebido directamente en el código de la instrucción, justo después del opcode.

En la arquitectura LR35902, las instrucciones inmediatas de 8 bits siguen este formato:

  • Byte 1: Opcode (por ejemplo, 0xE6 para AND d8)
  • Byte 2: Operando inmediato (d8 = "data 8-bit")

Cuando la CPU ejecuta una instrucción inmediata:

  1. Lee el opcode desde la dirección apuntada por PC
  2. Incrementa PC
  3. Lee el operando inmediato desde la nueva dirección de PC
  4. Incrementa PC nuevamente
  5. Ejecuta la operación con el valor inmediato

La ventaja del direccionamiento inmediato es que permite operar con constantes sin necesidad de cargar valores en registros primero. Por ejemplo, para hacer `AND A, 0x0F`, no necesitas:

LD B, 0x0F    ; Cargar 0x0F en B
AND A, B      ; AND A con B

Simplemente puedes hacer:

AND 0x0F      ; AND A con 0x0F directamente

Esto ahorra bytes de código y ciclos de CPU, lo cual es crítico en sistemas con recursos limitados como la Game Boy.

Reutilización de lógica: La lógica interna de las operaciones (cálculo de flags Z, N, H, C) es idéntica entre las versiones de registro y las versiones inmediatas. La única diferencia es de dónde se obtiene el operando: de un registro o del código. Por eso, la implementación reutiliza los mismos helpers genéricos (_adc, _sbc, _and, _xor, _or) que ya existían para las versiones de registro.

Implementación

Se implementaron 5 nuevos opcodes inmediatos siguiendo el mismo patrón que los opcodes inmediatos ya existentes (ADD A, d8 y SUB d8). Cada método sigue esta estructura:

  1. Lee el operando inmediato usando self.fetch_byte()
  2. Llama al helper genérico correspondiente (por ejemplo, self._and(operand))
  3. Registra la operación en el log de depuración
  4. Retorna 2 M-Cycles (fetch opcode + fetch operand)

Componentes creados/modificados

  • src/cpu/core.py: Añadidos 5 nuevos métodos de handlers:
    • _op_adc_a_d8() - ADC A, d8 (0xCE)
    • _op_sbc_a_d8() - SBC A, d8 (0xDE)
    • _op_and_d8() - AND d8 (0xE6)
    • _op_xor_d8() - XOR d8 (0xEE)
    • _op_or_d8() - OR d8 (0xF6)
  • src/cpu/core.py: Actualizada la tabla de despacho (_opcode_table) para incluir los 5 nuevos opcodes.
  • tests/test_cpu_alu_immediate.py: Creado archivo nuevo con suite completa de tests (5 tests) validando todas las operaciones inmediatas.

Decisiones de diseño

Reutilización de helpers: Se decidió reutilizar los helpers genéricos ya existentes (_adc, _sbc, _and, _xor, _or) en lugar de duplicar la lógica. Esto sigue el principio DRY y garantiza que el comportamiento de flags sea idéntico entre versiones de registro e inmediatas.

Consistencia con opcodes existentes: Los nuevos métodos siguen exactamente el mismo patrón que _op_add_a_d8 y _op_sub_d8, manteniendo consistencia en el código y facilitando el mantenimiento futuro.

Documentación exhaustiva: Cada método incluye docstrings detallados explicando qué hace la instrucción, cuándo es útil, qué flags actualiza y cuántos ciclos consume. Esto es crítico para un proyecto educativo donde la comprensión es tan importante como la funcionalidad.

Archivos Afectados

  • src/cpu/core.py - Añadidos 5 nuevos métodos de handlers y actualizada la tabla de despacho
  • tests/test_cpu_alu_immediate.py - Creado archivo nuevo con suite completa de tests (5 tests)

Tests y Verificación

Descripción de cómo se validó la implementación:

  • Tests unitarios: pytest con 5 tests pasando:
    • test_and_immediate: Verifica AND d8 con máscara de bits (0xFF AND 0x0F = 0x0F) y el quirk del hardware donde H siempre es 1.
    • test_xor_immediate: Verifica XOR d8 que resulta en cero (0xFF XOR 0xFF = 0x00, Z=1).
    • test_adc_immediate: Verifica ADC A, d8 con carry activo (0x00 + 0x00 + 1 = 0x01).
    • test_or_immediate: Verifica OR d8 básico (0x00 OR 0x55 = 0x55).
    • test_sbc_immediate: Verifica SBC A, d8 con borrow activo (0x00 - 0x00 - 1 = 0xFF).
  • ROM real (Tetris DX): Se ejecutó python3 main.py tetris_dx.gbc --debug:
    • La CPU ejecutó correctamente el bucle de inicialización alrededor de 0x1383-0x1390 que usa combinaciones de DEC, LD y OR entre registros.
    • El opcode 0xE6 (AND d8) se ejecuta ahora sin problemas en 0x12CA, enmascarando el valor leído de memoria con una constante inmediata.
    • El emulador avanza hasta PC=0x12CF tras ~70.082 M-Cycles y se detiene en opcode 0x0E (LD C, d8) no implementado, lo que confirma que el siguiente cuello de botella ya no es la ALU inmediata sino una carga inmediata de 8 bits en C.
  • Logs: Los métodos incluyen logging de depuración que muestra el operando, el resultado y los flags actualizados. El modo --debug de Viboy registra PC, opcode, registros y ciclos, permitiendo seguir el flujo exacto que lleva hasta 0x12CF.
  • Documentación: Implementación basada en Pan Docs - Instruction Set.

Fuentes Consultadas

Nota: La implementación sigue el mismo patrón que los opcodes inmediatos ya existentes (ADD A, d8, SUB d8, CP d8), garantizando consistencia en el código.

Integridad Educativa

Lo que Entiendo Ahora

  • Direccionamiento inmediato: Entiendo que es un modo de direccionamiento donde el operando está embebido en el código, justo después del opcode. Esto permite operar con constantes sin necesidad de cargar valores en registros primero.
  • Reutilización de lógica: Entiendo que la lógica interna de las operaciones (cálculo de flags) es idéntica entre versiones de registro e inmediatas. La única diferencia es de dónde se obtiene el operando.
  • Timing: Entiendo que todas las instrucciones inmediatas de 8 bits consumen 2 M-Cycles: uno para fetch del opcode y otro para fetch del operando.
  • Completitud del set ALU: Con estos 5 opcodes, ahora tenemos el conjunto completo de operaciones ALU inmediatas de 8 bits, lo que da a la CPU capacidad computacional completa para operaciones de 8 bits.

Lo que Falta Confirmar

  • Timing exacto: Aunque asumo que todas las instrucciones inmediatas de 8 bits consumen 2 M-Cycles, no he verificado esto exhaustivamente con documentación técnica detallada. Debería confirmar esto con Pan Docs o tests de timing si es necesario en el futuro.
  • Comportamiento en casos edge: Los tests cubren casos básicos, pero no he probado exhaustivamente todos los casos edge (overflow, underflow, etc.). Los helpers genéricos ya están testeados, así que debería ser correcto, pero es algo a tener en cuenta.

Hipótesis y Suposiciones

Suposición principal: Asumo que el timing (2 M-Cycles) es correcto para todas las instrucciones inmediatas de 8 bits, basándome en que ADD A, d8 y SUB d8 (que ya estaban implementados) también usan 2 M-Cycles. Esta suposición parece razonable, pero no está explícitamente verificada con documentación técnica detallada.

Suposición de completitud: Asumo que con estos 5 opcodes, ahora tenemos el conjunto completo de operaciones ALU inmediatas de 8 bits. Sin embargo, no he verificado exhaustivamente si hay otras operaciones inmediatas que falten. Esta suposición se basa en el conocimiento general de la arquitectura LR35902.

Próximos Pasos

  • [x] Probar Tetris DX para ver si ahora avanza más allá del opcode 0xE6
  • [x] Si Tetris avanza, identificar el siguiente opcode no implementado que cause fallo (0x0E - LD C, d8 en PC=0x12CF).
  • [ ] Implementar el opcode 0x0E (LD C, d8) reutilizando el patrón de cargas inmediatas de 8 bits.
  • [ ] Si Tetris intenta acceder a registros de hardware (0xFF40, 0xFF44, etc.), implementar el subsistema de PPU (Pixel Processing Unit) básico.
  • [ ] Si Tetris intenta escribir en VRAM (0x8000-0x9FFF), implementar el mapeo de VRAM en la MMU.
  • [ ] Continuar implementando opcodes faltantes según las necesidades del juego.