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

La Prueba Final: Completar la ALU (SUB, SBC) para el Checksum

Fecha: 2025-12-20 Step ID: 0188 Estado: ✅ VERIFIED

Resumen

El emulador ha superado todos los deadlocks de sincronización, pero la pantalla sigue en blanco porque la VRAM permanece vacía. El diagnóstico indica que la CPU está fallando la verificación del checksum del header del cartucho porque le faltan instrucciones de resta (SUB, SBC). Como resultado, el software de arranque entra en un bucle infinito deliberado, impidiendo que el juego se inicie.

Este Step completa la ALU de la CPU implementando y corrigiendo las instrucciones SUB A, r y SBC A, r (Subtract with Carry), permitiendo que la CPU calcule correctamente el checksum del cartucho y supere la secuencia de arranque. Con la ALU completa, el emulador finalmente podrá pasar la verificación de integridad del cartucho y ceder el control al juego principal.

🔍 Diagnóstico Definitivo: La Paradoja de la Precisión - El Checksum Fallido. El código de arranque realiza una verificación de integridad del cartucho antes de ceder el control al juego. Esta verificación calcula un checksum usando la fórmula x = 0; for (i = 0x0134; i <= 0x014C; i++) { x = x - rom[i] - 1; }, que depende fundamentalmente de las instrucciones SUB y SBC. Si estas instrucciones no están implementadas o tienen bugs, el checksum será incorrecto y el sistema se bloqueará deliberadamente.

Concepto de Hardware: El Cartridge Header Checksum y la Aritmética

El header de la ROM, en la dirección 0x014D, contiene un checksum de 8 bits. El software de arranque calcula su propio checksum para validar la integridad de la ROM. La fórmula es:

x = 0;
for (i = 0x0134; i <= 0x014C; i++) {
    x = x - rom[i] - 1;
}

Esta operación repetida de resta y decremento depende fundamentalmente de las instrucciones SUB (resta) y SBC (resta con acarreo/préstamo). Si alguna de estas instrucciones falla o no está implementada, el checksum será incorrecto y el sistema se bloqueará.

¿Por qué es crítico? El código de arranque (ya sea el BIOS o el propio juego) realiza esta verificación como medida de seguridad. Si el checksum calculado no coincide con el almacenado en 0x014D, el sistema entra deliberadamente en un bucle infinito para congelar el sistema. No copia los gráficos. No inicia el juego. Simplemente se detiene de forma segura.

Fuente: Pan Docs - Cartridge Header, Checksum Calculation.

Implementación

1. Corrección de la Implementación de SBC

Aunque las funciones alu_sub y alu_sbc ya estaban implementadas, se detectó un bug sutil en el cálculo del flag C (Carry/Borrow) en alu_sbc. La implementación original usaba a_old < (value + carry), que puede causar overflow si value + carry > 255.

Se corrigió para usar el resultado de 16 bits de forma segura: result > 0xFF indica que hubo underflow (el resultado es negativo en aritmética con signo de 16 bits), lo cual es la condición correcta para activar el flag C en una resta.

void CPU::alu_sbc(uint8_t value) {
    // SBC: Subtract with Carry - A = A - value - C
    uint8_t a_old = regs_->a;
    uint8_t carry = regs_->get_flag_c() ? 1 : 0;
    uint16_t result = static_cast<uint16_t>(a_old) - 
                      static_cast<uint16_t>(value) - 
                      static_cast<uint16_t>(carry);
    
    regs_->a = static_cast<uint8_t>(result);
    
    // Calcular flags
    regs_->set_flag_z(regs_->a == 0);
    regs_->set_flag_n(true);
    
    // H: half-borrow (bit 4 -> 3) incluyendo carry
    uint8_t a_low = a_old & 0x0F;
    uint8_t value_low = value & 0x0F;
    bool half_borrow = (a_low < (value_low + carry));
    regs_->set_flag_h(half_borrow);
    
    // C: borrow completo (underflow)
    // Usar el resultado de 16 bits para detectar underflow de forma segura
    regs_->set_flag_c(result > 0xFF);
}

2. Verificación de Opcodes SUB y SBC

Se verificó que todos los opcodes de SUB (0x90-0x97) y SBC (0x98-0x9F) están correctamente implementados en el switch de la CPU:

  • 0x90-0x97: SUB A, r (donde r puede ser B, C, D, E, H, L, (HL), A)
  • 0x98-0x9F: SBC A, r (donde r puede ser B, C, D, E, H, L, (HL), A)

Todos los opcodes están correctamente mapeados y llaman a las funciones helper correspondientes.

3. Tests Específicos para SUB y SBC

Se añadieron tres tests nuevos en tests/test_core_cpu_alu.py para validar específicamente las instrucciones SUB y SBC con registros:

  • test_sub_a_b: Verifica que SUB B calcula correctamente la resta y activa el flag Z cuando el resultado es 0.
  • test_sbc_a_b_with_borrow: Verifica que SBC A, B funciona correctamente cuando el flag C (borrow) está activado.
  • test_sbc_a_b_with_full_borrow: Verifica que SBC A, B detecta correctamente el borrow completo (underflow) y activa el flag C.

Archivos Afectados

  • src/core/cpp/CPU.cpp - Corrección del cálculo del flag C en alu_sbc
  • tests/test_core_cpu_alu.py - Añadidos 3 tests nuevos para SUB y SBC con registros

Tests y Verificación

Se ejecutaron los tests de la ALU para validar que todas las instrucciones de resta funcionan correctamente:

$ python -m pytest tests/test_core_cpu_alu.py -v

============================= test session starts =============================
platform win32 -- Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
collected 10 items

tests/test_core_cpu_alu.py::TestCoreCPUALU::test_add_immediate_basic PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_sub_immediate_zero_flag PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_add_half_carry PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_xor_a_optimization PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_inc_a PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_dec_a PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_add_full_carry PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_sub_a_b PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_sbc_a_b_with_borrow PASSED
tests/test_core_cpu_alu.py::TestCoreCPUALU::test_sbc_a_b_with_full_borrow PASSED

============================= 10 passed in 0.07s =============================

Validación de módulo compilado C++: Todos los tests pasan, confirmando que las instrucciones SUB y SBC están correctamente implementadas y calculan los flags de forma precisa.

Test específico de SUB:

def test_sub_a_b(self, cpu, mmu):
    """Verifica SUB B."""
    cpu.registers.a = 0x3E
    cpu.registers.b = 0x3E
    mmu.write(0x0100, 0x90) # SUB B
    cpu.registers.pc = 0x0100
    
    cpu.step()
    
    assert cpu.registers.a == 0x00
    assert cpu.registers.flag_z is True
    assert cpu.registers.flag_n is True

Fuentes Consultadas

Integridad Educativa

Lo que Entiendo Ahora

  • El Checksum del Cartucho: Es una medida de seguridad crítica que valida la integridad de la ROM antes de iniciar el juego. Si el checksum calculado no coincide con el almacenado, el sistema se bloquea deliberadamente.
  • La Importancia de SUB y SBC: Estas instrucciones no son solo operaciones aritméticas básicas; son fundamentales para el cálculo del checksum. Sin ellas, el emulador no puede pasar la verificación de integridad.
  • El Flag C en Restas: En restas, el flag C indica "borrow" (préstamo). Se activa cuando hay underflow (el resultado es negativo en aritmética con signo). La forma segura de calcularlo es usando el resultado de 16 bits: result > 0xFF.
  • La Secuencia de Arranque: El código de arranque realiza múltiples verificaciones antes de ceder el control al juego. El checksum es la última barrera de software antes de que el juego pueda iniciar.

Lo que Falta Confirmar

  • Verificación con ROM Real: Aunque los tests unitarios pasan, falta verificar que el emulador puede calcular correctamente el checksum de una ROM real y pasar la verificación de arranque.
  • Comportamiento del BIOS: Si el emulador usa un BIOS real, el checksum se calcula durante la ejecución del BIOS. Si no usa BIOS, el juego mismo puede calcular su propio checksum.

Hipótesis y Suposiciones

Hipótesis Principal: Con la ALU completa (SUB y SBC correctamente implementadas), el emulador debería poder calcular el checksum del cartucho correctamente y pasar la verificación de arranque. Esto debería permitir que el juego finalmente copie los gráficos a la VRAM y active el renderizado del fondo.

Suposición: Asumimos que el código de arranque usa la fórmula estándar de checksum documentada en Pan Docs. Si un juego usa una fórmula diferente, podría requerir investigación adicional.

Próximos Pasos

  • [ ] Ejecutar el emulador con una ROM real (ej: tetris.gb) y verificar que puede calcular el checksum correctamente
  • [ ] Verificar que el juego pasa la verificación de arranque y copia los gráficos a la VRAM
  • [ ] Si la pantalla sigue en blanco, investigar otras posibles causas (ej: instrucciones faltantes, bugs en otras partes de la CPU)
  • [ ] Documentar el resultado final: ¿Se ve el logo de Nintendo o hay más barreras que superar?