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.
Memoria Indirecta e Incremento/Decremento
Resumen
Implementación de direccionamiento indirecto usando HL como puntero de memoria, operaciones LDI/LDD
(incremento/decremento automático del puntero) y operaciones unarias de incremento/decremento (INC/DEC)
con manejo correcto de flags. Se implementaron helpers críticos _inc_n y _dec_n
que actualizan flags Z, N, H pero NO tocan el flag C (Carry), una peculiaridad importante del hardware
LR35902 que muchos emuladores fallan al implementar. Estos opcodes son esenciales para bucles de limpieza
de memoria que los juegos ejecutan al inicio (memset). Suite completa de tests TDD validando memoria
indirecta, wrap-around de punteros y comportamiento correcto de flags en INC/DEC.
Concepto de Hardware
Direccionamiento Indirecto (HL): Cuando una instrucción usa (HL), no se
usa el valor del registro HL directamente, sino que HL se utiliza como una dirección de memoria
para leer o escribir. Es análogo a un puntero en C: *ptr. Por ejemplo, si HL contiene
0xC000 y ejecutamos LD (HL), A, escribimos el valor de A en la dirección
de memoria 0xC000, no en el registro HL mismo.
LDI / LDD (Load with Increment/Decrement): Estas instrucciones son "navajas suizas"
que combinan una operación de memoria con el incremento o decremento automático del puntero. Por ejemplo,
LD (HL+), A (LDI) escribe A en la dirección apuntada por HL e incrementa HL en un solo
ciclo. Esto es ideal para bucles rápidos de copia o inicialización de memoria (memset),
ya que evita tener que incrementar el puntero manualmente en una instrucción separada.
Flags en INC/DEC (CRÍTICO): Las operaciones INC y DEC de
8 bits afectan a los flags Z (Zero), N (Subtract) y H (Half-Carry), pero NO afectan al flag C
(Carry). Esta es una peculiaridad importante del hardware LR35902 que muchos emuladores fallan
al implementar, rompiendo la lógica condicional que depende de mantener el flag C intacto durante
operaciones de incremento/decremento.
- Flag H en INC: Se activa si hay desbordamiento del bit 3 al 4 (nibble bajo).
Ejemplo:
0x0F + 1 = 0x10activa H porque hubo carry del bit 3 al 4. - Flag H en DEC: Se activa si hay préstamo del bit 4 al 3 (nibble bajo).
Ejemplo:
0x10 - 1 = 0x0Factiva H porque hubo borrow del bit 4 al 3. - Flag C: NO SE TOCA, incluso si hay overflow (INC 0xFF -> 0x00) o underflow (DEC 0x00 -> 0xFF). Esta preservación del flag C es crítica para código que usa INC/DEC en bucles con condiciones basadas en C.
¿Por qué esto desbloquea Tetris? Tetris (y la mayoría de juegos de Game Boy) usan bucles de limpieza de memoria al inicio que hacen algo como:
LD HL, 0xDFFF ; Final de la RAM
XOR A ; A = 0
loop:
LD (HL-), A ; Escribe 0 en RAM y baja el puntero
BIT 7, H ; Comprueba si ha llegado al final...
JR NZ, loop ; Repite
Sin LD (HL-), A (LDD) implementado, el emulador no puede ejecutar este bucle de
limpieza de memoria.
Implementación
Se implementaron helpers genéricos _inc_n y _dec_n que encapsulan la
lógica de incremento/decremento con manejo correcto de flags. Estos helpers se reutilizan en
todos los opcodes INC/DEC de registros individuales (B, C, D, E, H, L, A).
Componentes creados/modificados
- Helpers ALU:
_inc_n(val)y_dec_n(val)- Incrementan/decrementan un valor de 8 bits, actualizan flags Z, N, H, pero NO tocan C. - Opcodes de memoria indirecta:
0x77:LD (HL), A- Escribe A en dirección apuntada por HL0x22:LD (HL+), A(LDI) - Escribe A en (HL) e incrementa HL0x32:LD (HL-), A(LDD) - Escribe A en (HL) y decrementa HL0x2A:LD A, (HL+)(LDI) - Lee de (HL) a A e incrementa HL
- Opcodes de incremento/decremento:
0x04:INC B0x05:DEC B0x0C:INC C0x0D:DEC C0x3C:INC A0x3D:DEC A
- Tests:
test_cpu_memory_ops.py- Suite completa de tests para memoria indirecta y flags de INC/DEC
Decisiones de diseño
Helpers genéricos vs. código duplicado: Se crearon helpers _inc_n y
_dec_n para evitar duplicar la lógica de actualización de flags en cada opcode INC/DEC.
Esto facilita el mantenimiento y asegura consistencia. Todos los opcodes INC/DEC de 8 bits usan estos
helpers, lo que garantiza que el comportamiento de flags sea idéntico en todos los casos.
Preservación explícita del flag C: Se documentó explícitamente en código y tests que INC/DEC NO tocan el flag C. Esto es crítico porque muchos desarrolladores (y emuladores) asumen incorrectamente que cualquier operación aritmética debe actualizar todos los flags. Los tests verifican explícitamente que C se preserva incluso en casos de overflow/underflow.
Wrap-around de punteros: Las operaciones LDI/LDD aplican wrap-around explícito
usando & 0xFFFF para asegurar que HL siempre esté en el rango válido de 16 bits.
Esto permite que los bucles funcionen correctamente incluso si alcanzan los límites del espacio
de direcciones.
Archivos Afectados
src/cpu/core.py- Añadidos helpers_inc_ny_dec_n, y handlers para opcodes de memoria indirecta e INC/DECtests/test_cpu_memory_ops.py- Nueva suite de tests para memoria indirecta y comportamiento de flags en INC/DEC. Corregidos para usar direcciones fuera del área de ROM (0x8000+) para permitir escritura de código de test.
Tests y Verificación
Se creó una suite completa de tests en test_cpu_memory_ops.py que valida:
- Memoria indirecta básica:
LD (HL), Aescribe correctamente en la dirección apuntada por HL sin modificar HL - LDI (incremento):
LD (HL+), AyLD A, (HL+)escriben/leen e incrementan HL correctamente, incluyendo wrap-around (0xFFFF -> 0x0000) - LDD (decremento):
LD (HL-), Aescribe y decrementa HL correctamente, incluyendo wrap-around (0x0000 -> 0xFFFF) - INC con flags: Casos normales, Half-Carry (0x0F -> 0x10), y overflow (0xFF -> 0x00) verificando que C NO cambia
- DEC con flags: Casos normales, Half-Borrow (0x10 -> 0x0F), y underflow verificando que C NO cambia
- Preservación de C: Tests explícitos verificando que C se preserva incluso cuando está activo antes de INC/DEC
- Variantes: Tests para INC/DEC de B, C, A verificando comportamiento consistente
Tests unitarios: 14 tests en pytest cubriendo todos los casos críticos mencionados anteriormente. Los tests siguen el patrón TDD establecido en el proyecto. Todos los tests pasan correctamente después de corregir un problema inicial con el uso de direcciones de memoria.
Fix aplicado: Inicialmente, los tests intentaban escribir código en 0x0100
(área ROM 0x0000-0x7FFF), pero la MMU lee desde el cartucho en esa área, no desde la memoria interna.
Esto causaba que los tests leyeran 0xFF en lugar de los opcodes escritos. Se corrigió
cambiando todos los tests para usar direcciones fuera del área de ROM (0x8000+), donde
la escritura funciona correctamente. Este fix documenta un aspecto importante del mapeo de memoria:
las áreas de ROM son de solo lectura desde la perspectiva del programa, mientras que las áreas de
RAM/VRAM permiten lectura y escritura.
Fuentes Consultadas
- Pan Docs: CPU Instruction Set - Comportamiento de flags en INC/DEC, direccionamiento indirecto
- Pan Docs: CPU Registers and Flags - Descripción detallada de flags y comportamiento de operaciones aritméticas
- Implementación basada en documentación técnica estándar del hardware LR35902. No se consultó código de otros emuladores para mantener integridad clean-room.
Integridad Educativa
Lo que Entiendo Ahora
- Direccionamiento indirecto: Entiendo que
(HL)significa "el valor en la dirección de memoria apuntada por HL", no "el valor de HL". Es como usar un puntero en C. - Flags en INC/DEC: Entiendo que INC/DEC de 8 bits NO tocan el flag C, incluso con overflow/underflow. Esta preservación es crítica y muchos emuladores la fallan. El Half-Carry (H) se calcula diferente en INC (carry del bit 3 al 4) vs DEC (borrow del bit 4 al 3).
- LDI/LDD: Entiendo que estas instrucciones son optimizaciones para bucles, combinando operación de memoria con actualización de puntero en un solo ciclo. LDI incrementa, LDD decrementa.
- Wrap-around: Entiendo que los punteros de 16 bits hacen wrap-around usando
& 0xFFFF. Esto es importante para bucles que alcanzan los límites del espacio de direcciones.
Lo que Falta Confirmar
- INC/DEC de HL (16 bits): Falta implementar INC HL y DEC HL que son diferentes (no afectan flags). Estos son útiles para bucles que necesitan incrementar/decrementar punteros de 16 bits.
- INC/DEC de (HL): Falta implementar INC (HL) y DEC (HL) que incrementan/decrementan el valor en memoria apuntado por HL. Estos también afectan flags Z, N, H pero no C.
- Validación con ROMs de test: Sería ideal validar con ROMs de test redistribuibles que prueben bucles de limpieza de memoria para confirmar que el comportamiento es correcto.
- Timing exacto: Por ahora usamos M-Cycles aproximados. El timing exacto de LDI/LDD podría diferir ligeramente en el hardware real, pero para la mayoría de casos debería ser correcto.
- Mapeo de memoria en tests: Confirmado que el área ROM (0x0000-0x7FFF) es de solo lectura desde el cartucho, mientras que las áreas de RAM/VRAM (0x8000+) permiten escritura. Los tests deben usar direcciones fuera de ROM para poder escribir código de prueba.
Hipótesis y Suposiciones
La implementación de flags en INC/DEC se basa en documentación técnica estándar (Pan Docs) que indica explícitamente que C no se toca. Los tests verifican este comportamiento, pero no he podido validarlo directamente con hardware real. La preservación de C es una característica documentada y ampliamente conocida del hardware LR35902, pero es fácil de pasar por alto si no se lee cuidadosamente la documentación.
El comportamiento de Half-Carry en DEC (activado cuando el nibble bajo es 0, indicando borrow) se implementó basándose en la lógica de cómo funciona el borrow en restas binarias. Si el nibble bajo es 0 y decrementamos, necesitamos pedir prestado del nibble alto, activando H. Esta lógica es consistente con cómo funciona el Half-Carry en restas normales.
Lección aprendida sobre mapeo de memoria: Durante el desarrollo de los tests, descubrimos
que la MMU tiene un comportamiento diferente para lectura y escritura en el área ROM (0x0000-0x7FFF).
La lectura siempre se hace desde el cartucho (si existe), mientras que la escritura se hace en la memoria
interna, pero no es visible en lecturas posteriores. Esto es consistente con cómo funciona el hardware real:
la ROM del cartucho es de solo lectura desde la perspectiva del programa. Los tests fueron corregidos para
usar direcciones fuera del área ROM (0x8000+) donde la escritura funciona correctamente.
Este descubrimiento refuerza la importancia de entender el mapeo de memoria completo del sistema.
Próximos Pasos
- [ ] Implementar
BIT 7, H(instrucción BIT) necesaria para el bucle de limpieza de Tetris - [ ] Implementar más opcodes INC/DEC (D, E, H, L, (HL)) para completar el conjunto
- [ ] Implementar INC HL / DEC HL (16 bits, no afectan flags) para bucles de punteros
- [ ] Ejecutar trace de Tetris para verificar que el bucle de limpieza funciona correctamente
- [ ] Validar con ROMs de test redistribuibles si están disponibles