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

Pila Completa y Rotaciones del Acumulador

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

Resumen

Se completó el manejo del Stack (Pila) implementando PUSH/POP para todos los pares de registros (AF, DE, HL), y se añadieron las rotaciones rápidas del acumulador (RLCA, RRCA, RLA, RRA). La implementación de POP AF incluye la máscara crítica 0xF0 para los bits bajos del registro F, simulando el comportamiento del hardware real. Las rotaciones rápidas tienen un comportamiento especial con los flags: Z siempre es 0, incluso si el resultado es cero. Esta es una diferencia clave con las rotaciones del prefijo CB. Todos los tests pasan (17 tests en total).

Concepto de Hardware

Pila (Stack) Completa

La pila en la Game Boy es una región de memoria que crece hacia abajo (de direcciones altas a bajas). Permite guardar y restaurar el estado de los registros durante llamadas a subrutinas y manejo de interrupciones. Ya teníamos PUSH/POP BC implementados; ahora completamos con AF, DE y HL.

CRÍTICO - POP AF y la Máscara 0xF0:

Cuando hacemos POP AF, recuperamos el registro de Flags (F) de la pila. En el hardware real de la Game Boy, los 4 bits bajos del registro F (bits 0-3) SIEMPRE son cero. Esto es una característica física del hardware, no una convención de software.

Si no aplicamos la máscara 0xF0 al valor recuperado de la pila, los bits bajos pueden contener "basura" que afecta las comparaciones de flags. Juegos como Tetris fallan al comprobar flags si estos bits no están limpios, porque las instrucciones condicionales (JR NZ, RET Z, etc.) se comportan de forma aleatoria.

Rotaciones Rápidas del Acumulador

Las rotaciones rápidas (0x07, 0x0F, 0x17, 0x1F) son instrucciones optimizadas que rotan el registro A de diferentes formas. Son "rápidas" porque solo operan sobre A y consumen 1 ciclo, a diferencia de las rotaciones del prefijo CB que pueden operar sobre cualquier registro.

Tipos de rotación:

  • RLCA (0x07): Rotate Left Circular Accumulator. El bit 7 sale y entra por el bit 0. También se copia al flag C.
  • RRCA (0x0F): Rotate Right Circular Accumulator. El bit 0 sale y entra por el bit 7. También se copia al flag C.
  • RLA (0x17): Rotate Left Accumulator through Carry. El bit 7 va al flag C, y el *antiguo* flag C entra en el bit 0. Es una rotación de 9 bits (8 bits de A + 1 bit de C).
  • RRA (0x1F): Rotate Right Accumulator through Carry. El bit 0 va al flag C, y el *antiguo* flag C entra en el bit 7. Es una rotación de 9 bits.

CRÍTICO - Flags en Rotaciones Rápidas:

Estas instrucciones SIEMPRE ponen Z=0, N=0, H=0. Solo afectan a C. Esta es una diferencia clave con las rotaciones CB (0xCB), donde Z se calcula normalmente según el resultado. Si el resultado de una rotación rápida es 0, Z sigue siendo 0 (quirk del hardware).

Uso en Juegos:

Las rotaciones a través de carry (RLA, RRA) son fundamentales para generadores de números pseudo-aleatorios. Juegos como Tetris usan RLA intensivamente para generar secuencias aleatorias de piezas. Sin estas instrucciones, el juego se colgaría esperando un número aleatorio válido.

Implementación

Se implementaron 10 nuevos opcodes: 6 para la pila (PUSH/POP AF, DE, HL) y 4 para rotaciones rápidas (RLCA, RRCA, RLA, RRA).

Componentes creados/modificados

  • src/cpu/core.py: Añadidos handlers para PUSH/POP AF, DE, HL y rotaciones rápidas. POP AF aplica máscara 0xF0 usando set_af() que internamente llama a set_f() que ya aplica la máscara.
  • tests/test_cpu_stack.py: Añadidos 3 tests nuevos para PUSH/POP DE, HL y AF (incluyendo test crítico de máscara 0xF0).
  • tests/test_cpu_rotations.py: Nuevo archivo con 9 tests para todas las rotaciones, incluyendo tests de quirk de flags (Z siempre 0) y cadenas de RLA para simular generadores aleatorios.

Decisiones de diseño

Máscara 0xF0 en POP AF:

Se aprovechó que set_af() ya llama internamente a set_f(), que aplica la máscara 0xF0 automáticamente. Esto garantiza que los bits bajos de F siempre estén limpios, sin necesidad de código adicional en el handler.

Rotaciones: Implementación explícita de flags:

Aunque las rotaciones rápidas siempre ponen Z=0, N=0, H=0, se implementó de forma explícita en cada handler para claridad y para evitar errores si en el futuro se modifica el comportamiento de helpers genéricos. Esto también hace el código más autodocumentado.

Reutilización de helpers de pila:

Todos los PUSH/POP reutilizan los helpers _push_word() y _pop_word() que ya existían, garantizando consistencia en el orden de bytes (Little-Endian) y el manejo del Stack Pointer.

Archivos Afectados

  • src/cpu/core.py - Añadidos 10 nuevos handlers de opcodes (PUSH/POP AF, DE, HL y rotaciones rápidas)
  • tests/test_cpu_stack.py - Añadidos 3 tests nuevos (PUSH/POP DE, HL, AF con máscara)
  • tests/test_cpu_rotations.py - Nuevo archivo con 9 tests para rotaciones rápidas

Tests y Verificación

Se crearon 12 tests nuevos (3 para pila + 9 para rotaciones) y todos pasan correctamente:

  • Tests unitarios: pytest con 17 tests pasando (5 tests de pila existentes + 3 nuevos + 9 de rotaciones)
  • Test crítico POP AF: Verifica que al recuperar 0xFFFF de la pila, F se convierte en 0xF0 (bits bajos limpiados)
  • Tests de rotaciones: Validan rotaciones circulares, rotaciones a través de carry, quirk de flags (Z siempre 0), y cadenas de RLA para generadores aleatorios
  • Documentación: Pan Docs - CPU Instruction Set (PUSH/POP, rotaciones rápidas, flags behavior)

Fuentes Consultadas

Nota: Implementación basada en documentación técnica de Pan Docs sobre el comportamiento del hardware LR35902.

Integridad Educativa

Lo que Entiendo Ahora

  • Máscara 0xF0 en F: Los 4 bits bajos del registro F siempre son cero en hardware real. Esto no es una convención de software, sino una limitación física del hardware. Si no aplicamos la máscara en POP AF, los flags pueden tener valores inválidos que rompen la lógica condicional.
  • Rotaciones rápidas vs CB: Las rotaciones rápidas (0x07, 0x0F, 0x17, 0x1F) tienen un comportamiento especial con los flags: Z siempre es 0, incluso si el resultado es cero. Las rotaciones CB calculan Z normalmente según el resultado. Esta diferencia es crítica para la precisión del emulador.
  • Rotaciones a través de carry: RLA y RRA son rotaciones de 9 bits (8 bits de A + 1 bit de C). El carry antiguo entra en el registro, y el bit que sale va al carry. Esto permite crear generadores de números pseudo-aleatorios eficientes.
  • Uso en juegos: Las rotaciones a través de carry son fundamentales para generadores aleatorios. Sin ellas, juegos como Tetris no pueden generar piezas aleatorias y se cuelgan.

Lo que Falta Confirmar

  • Timing exacto: Por ahora asumimos que todas las rotaciones rápidas consumen 1 M-Cycle. Pendiente verificar si hay diferencias sutiles en timing entre rotaciones circulares y a través de carry.
  • Comportamiento en edge cases: Las rotaciones están testeadas con valores comunes, pero podrían necesitar más tests con valores extremos (0x00, 0xFF, etc.) para asegurar que el wrap-around funciona correctamente en todos los casos.

Hipótesis y Suposiciones

Asumimos que el comportamiento de flags en rotaciones rápidas (Z siempre 0) es consistente en todo el hardware Game Boy. Esta suposición está respaldada por Pan Docs, pero no hemos verificado con hardware real o múltiples emuladores (por la regla clean-room, no podemos consultar código de otros emuladores).

Próximos Pasos

  • [ ] Implementar más opcodes del prefijo CB (rotaciones, shifts, BIT, SET, RES)
  • [ ] Implementar CALL condicionales (CALL NZ, CALL Z, CALL NC, CALL C)
  • [ ] Implementar JP condicionales (JP NZ, JP Z, JP NC, JP C)
  • [ ] Verificar que el emulador puede ejecutar más código de Tetris DX (objetivo: superar 100.000 ciclos)
  • [ ] Implementar sistema de interrupciones completo (IF, IE, manejo de interrupciones)