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

Aritmética de Pila Avanzada (SP+r8)

Fecha: 2025-12-18 Step ID: 0068 Estado: Verified

Resumen

Se implementaron dos opcodes críticos de aritmética de pila con offset: ADD SP, r8 (0xE8) y LD HL, SP+r8 (0xF8). Estos opcodes permiten calcular direcciones de pila con un offset con signo de 8 bits, una operación común en código de juegos para acceder a variables locales o estructuras de datos en la pila. El emulador había avanzado más de 1 millón de ciclos ejecutando Pokémon y chocó con el opcode 0xF8 no implementado en PC=0x1D5C, lo que indica un progreso significativo. Ambos opcodes tienen flags especiales (H y C) que se calculan basándose en el byte bajo de SP, no en los 12 bits bajos como en ADD HL, rr.

Concepto de Hardware

La CPU LR35902 proporciona dos instrucciones para realizar aritmética de pila con offset:

  • ADD SP, r8 (0xE8): Suma un entero con signo de 8 bits al Stack Pointer (SP). El offset se lee como un byte con signo usando representación Two's Complement (0x00-0x7F son positivos, 0x80-0xFF son negativos). El resultado se almacena en SP y consume 4 M-Cycles.
  • LD HL, SP+r8 (0xF8): Calcula SP + offset (mismo formato de offset) y almacena el resultado en HL. SP NO se modifica. Consume 3 M-Cycles.

Flags especiales: Ambas instrucciones tienen un comportamiento único con los flags:

  • Z (Zero): Siempre 0 (no se toca).
  • N (Subtract): Siempre 0 (es una suma).
  • H (Half-Carry): Se activa si hay carry del bit 3 al 4 (nibble bajo). Se calcula como: ((sp & 0xF) + (offset & 0xF)) > 0xF.
  • C (Carry): Se activa si hay carry del bit 7 al 8 (byte bajo). Se calcula como: ((sp & 0xFF) + (offset & 0xFF)) > 0xFF.

Diferencia crítica con ADD HL, rr: En ADD HL, rr, los flags H y C se calculan en los 12 bits bajos (bits 0-11) y 16 bits respectivamente. En ADD SP, r8 y LD HL, SP+r8, los flags se calculan solo en el byte bajo (bits 0-7) de SP, porque estamos sumando un valor de 8 bits a un valor de 16 bits.

Uso en juegos: Estas instrucciones son fundamentales para acceder a variables locales en la pila. Por ejemplo, si una función tiene variables locales en la pila, puede usar LD HL, SP-4 para obtener un puntero a esas variables sin modificar SP.

Implementación

Se implementó un helper genérico _add_sp_offset() que calcula SP + offset y devuelve el resultado junto con los flags H y C. Este helper se reutiliza en ambos opcodes para mantener consistencia y evitar duplicación de código.

Helper: _add_sp_offset()

El helper recibe un offset con signo (rango [-128, 127]) y devuelve una tupla (result, h_flag, c_flag):

  • Convierte el offset a su representación unsigned para cálculos de flags.
  • Calcula el resultado con wrap-around de 16 bits: (sp + offset) & 0xFFFF.
  • Calcula H flag: ((sp_low & 0xF) + (offset_low & 0xF)) > 0xF.
  • Calcula C flag: ((sp_low + offset_low) & 0x100) != 0.

Opcode 0xE8: ADD SP, r8

Implementado en _op_add_sp_r8():

  • Lee el offset usando _read_signed_byte() (ya existente).
  • Llama a _add_sp_offset() para calcular resultado y flags.
  • Actualiza SP con el resultado.
  • Actualiza flags: Z=0, N=0, H y C según cálculo.
  • Retorna 4 M-Cycles.

Opcode 0xF8: LD HL, SP+r8

Implementado en _op_ld_hl_sp_r8():

  • Lee el offset usando _read_signed_byte().
  • Llama a _add_sp_offset() para calcular resultado y flags.
  • Actualiza HL con el resultado (SP NO se modifica).
  • Actualiza flags: Z=0, N=0, H y C según cálculo.
  • Retorna 3 M-Cycles.

Integración en tabla de despacho

Ambos opcodes se añadieron a la tabla de despacho _opcode_table en __init__():

  • 0xE8: self._op_add_sp_r8
  • 0xF8: self._op_ld_hl_sp_r8

Archivos Afectados

  • src/cpu/core.py - Añadido helper _add_sp_offset() y handlers _op_add_sp_r8() y _op_ld_hl_sp_r8(). Integrados en tabla de despacho.
  • tests/test_cpu_sp_arithmetic.py - Nuevo archivo con 9 tests unitarios que cubren ambos opcodes: offsets positivos/negativos, flags H y C, wrap-around, y verificación de que SP no cambia en LD HL, SP+r8.

Tests y Verificación

Se crearon 9 tests unitarios exhaustivos que cubren todos los casos relevantes:

  • ADD SP, r8 (5 tests): Offset positivo, offset negativo, half-carry, carry, wrap-around.
  • LD HL, SP+r8 (4 tests): Offset positivo, offset negativo, flags H y C, verificación de que SP no cambia.

Ejecución de Tests

Comando ejecutado:

python -m pytest tests/test_cpu_sp_arithmetic.py -v

Entorno:

  • OS: Windows 10
  • Python: 3.13.5

Resultado:

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

tests/test_cpu_sp_arithmetic.py::TestAddSpR8::test_add_sp_positive PASSED
tests/test_cpu_sp_arithmetic.py::TestAddSpR8::test_add_sp_negative PASSED
tests/test_cpu_sp_arithmetic.py::TestAddSpR8::test_add_sp_with_half_carry PASSED
tests/test_cpu_sp_arithmetic.py::TestAddSpR8::test_add_sp_with_carry PASSED
tests/test_cpu_sp_arithmetic.py::TestAddSpR8::test_add_sp_wraparound PASSED
tests/test_cpu_sp_arithmetic.py::TestLdHlSpR8::test_ld_hl_sp_r8_positive PASSED
tests/test_cpu_sp_arithmetic.py::TestLdHlSpR8::test_ld_hl_sp_r8_negative PASSED
tests/test_cpu_sp_arithmetic.py::TestLdHlSpR8::test_ld_hl_sp_r8_with_flags PASSED
tests/test_cpu_sp_arithmetic.py::TestLdHlSpR8::test_ld_hl_sp_r8_sp_unchanged PASSED

============================== 9 passed in 0.15s ==============================

Qué valida:

  • Correctitud aritmética: Los tests verifican que SP + offset se calcula correctamente, incluyendo casos con wrap-around (0xFFFF + 1 = 0x0000).
  • Flags H y C: Los tests verifican que los flags se calculan correctamente basándose en el byte bajo de SP, no en los 12 bits bajos como en ADD HL, rr.
  • Preservación de SP: Los tests verifican que en LD HL, SP+r8, el Stack Pointer no se modifica, solo se usa para calcular HL.
  • Flags Z y N: Los tests verifican que Z=0 y N=0 siempre, independientemente del resultado.

Código del test (ejemplo):

def test_add_sp_positive(self):
    """Test: Verificar que ADD SP, r8 suma un offset positivo correctamente."""
    mmu = MMU()
    cpu = CPU(mmu)
    
    cpu.registers.set_pc(0x0100)
    cpu.registers.set_sp(0x1000)
    
    # Escribir opcode y offset
    mmu.write_byte(0x0100, 0xE8)  # ADD SP, r8
    mmu.write_byte(0x0101, 0x05)  # +5
    
    cycles = cpu.step()
    
    # Verificar resultado
    assert cpu.registers.get_sp() == 0x1005, "SP debe ser 0x1005"
    
    # Verificar flags
    assert not cpu.registers.get_flag_z(), "Z debe ser 0"
    assert not cpu.registers.get_flag_n(), "N debe ser 0"
    assert not cpu.registers.get_flag_h(), "H debe ser 0 (no hay half-carry)"
    assert not cpu.registers.get_flag_c(), "C debe ser 0 (no hay carry)"
    
    # Verificar ciclos
    assert cycles == 4, "ADD SP, r8 debe consumir 4 M-Cycles"

Por qué estos tests demuestran el hardware: Los tests verifican que el cálculo de flags H y C se basa en el byte bajo de SP (bits 0-7), no en los 12 bits bajos como en ADD HL, rr. Esto es una característica específica del hardware LR35902 que diferencia estas instrucciones de otras operaciones aritméticas de 16 bits. Los tests también verifican que el offset se interpreta correctamente como un entero con signo (Two's Complement), permitiendo offsets negativos que son comunes en código de juegos para acceder a variables locales en la pila.

Fuentes Consultadas

Nota: La implementación se basó en la documentación de Pan Docs sobre el comportamiento de flags en estas instrucciones específicas, que difiere del comportamiento estándar de ADD HL, rr.

Integridad Educativa

Lo que Entiendo Ahora

  • Flags especiales en SP+r8: Los flags H y C en ADD SP, r8 y LD HL, SP+r8 se calculan basándose en el byte bajo de SP (bits 0-7), no en los 12 bits bajos como en ADD HL, rr. Esto es porque estamos sumando un valor de 8 bits a un valor de 16 bits, y el hardware solo verifica el desbordamiento del byte bajo.
  • Uso en código de juegos: Estas instrucciones son fundamentales para acceder a variables locales en la pila. Por ejemplo, LD HL, SP-4 obtiene un puntero a variables locales sin modificar SP, permitiendo acceso eficiente a estructuras de datos en la pila.
  • Diferencia con ADD HL, rr: Aunque ambas son operaciones aritméticas de 16 bits, el cálculo de flags es diferente. En ADD HL, rr, H se calcula en los 12 bits bajos (bits 0-11), mientras que en SP+r8, H se calcula solo en el nibble bajo (bits 0-3).

Lo que Falta Confirmar

  • Comportamiento en casos límite: Los tests cubren casos normales y wrap-around, pero no se han probado casos extremos como SP=0x0000 con offset negativo grande o SP=0xFFFF con offset positivo grande. Sin embargo, los tests de wrap-around cubren estos casos implícitamente.
  • Validación con ROMs reales: La implementación se validó con tests unitarios, pero aún no se ha probado ejecutando una ROM real que use estos opcodes. El hecho de que el emulador haya llegado a ejecutar 0xF8 en Pokémon indica que el juego los necesita, pero la validación final será cuando el juego avance más allá de ese punto.

Hipótesis y Suposiciones

Suposición sobre el cálculo de flags: La implementación asume que el cálculo de flags H y C se basa en el byte bajo de SP (bits 0-7), no en los 12 bits bajos. Esta suposición está respaldada por la documentación de Pan Docs, pero no se ha verificado con hardware real. Los tests unitarios verifican que el cálculo es correcto según esta suposición.

Próximos Pasos

  • [ ] Ejecutar Pokémon (pkmn.gb) y verificar que avanza más allá de PC=0x1D5C (donde chocó con 0xF8).
  • [ ] Verificar que el juego muestra la intro (estrella fugaz, copyright) después de implementar estos opcodes.
  • [ ] Si aparecen más opcodes no implementados, implementarlos siguiendo el mismo patrón.
  • [ ] Continuar con la implementación de subsistemas faltantes (APU, si es necesario para el juego).