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

Transferencias de 8 bits (LD r, r') y HALT

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

Resumen

Se implementó el bloque completo de transferencias de 8 bits (LD r, r') del rango 0x40-0x7F, cubriendo 63 opcodes nuevos que permiten mover datos entre registros y memoria. Se implementó también la instrucción HALT (0x76) que pone la CPU en modo de bajo consumo. Este bloque es crítico porque permite que el emulador ejecute código real de juegos que necesita transferir datos entre registros para su funcionamiento normal.

Concepto de Hardware

Bloque de Transferencias 0x40-0x7F

En la arquitectura LR35902 (y Z80), el bloque central de opcodes 0x40-0x7F está dedicado casi exclusivamente a transferencias de datos entre registros. Es una matriz de 8x8 donde cada opcode codifica un origen y un destino usando 3 bits para cada uno.

Estructura del opcode:

  • Bits 0-2: Código del registro origen (000=B, 001=C, 010=D, 011=E, 100=H, 101=L, 110=(HL), 111=A)
  • Bits 3-5: Código del registro destino (mismo mapeo)

Esta estructura permite 64 combinaciones posibles (8x8), pero el opcode 0x76 es especial: en lugar de ser LD (HL), (HL) (que no tiene sentido), es la instrucción HALT.

HALT (0x76) - Modo de Bajo Consumo

HALT pone la CPU en un estado de bajo consumo donde deja de ejecutar instrucciones. El Program Counter (PC) no avanza y la CPU simplemente espera. La CPU se despierta automáticamente cuando ocurre una interrupción (si IME está activado) o puede ser despertada manualmente.

Mientras está en HALT, la CPU consume 1 ciclo por tick (espera activa), pero no ejecuta ninguna instrucción. Esto es útil para juegos que esperan eventos como V-Blank (interrupción de refresco de pantalla) para sincronizar la lógica del juego.

Timing de las Transferencias

Las transferencias tienen diferentes tiempos de ejecución según si involucran memoria:

  • LD r, r: 1 M-Cycle (transferencia entre registros, sin acceso a memoria)
  • LD r, (HL) o LD (HL), r: 2 M-Cycles (acceso a memoria indirecta)

Esta diferencia refleja el costo real del hardware: acceder a memoria es más lento que acceder a registros internos de la CPU.

Implementación

Se implementó el bloque completo de transferencias usando un sistema de inicialización lazy que crea los handlers dinámicamente cuando se accede a ellos por primera vez. Esto permite usar los métodos helper que se definen después del constructor.

Componentes creados/modificados

  • CPU.__init__(): Añadido flag halted y llamada a _init_ld_handlers()
  • CPU.step(): Modificado para manejar estado HALT (verificar interrupciones, consumir ciclos)
  • CPU._get_register_value(): Helper para obtener valor de registro según código (0-7)
  • CPU._set_register_value(): Helper para establecer valor en registro según código (0-7)
  • CPU._op_ld_r_r(): Handler genérico para todas las transferencias LD r, r'
  • CPU._op_halt(): Handler para HALT (0x76)
  • CPU._init_ld_handler_lazy(): Inicialización lazy de handlers de transferencias
  • CPU._execute_opcode(): Modificado para inicializar handlers lazy cuando se accede a ellos

Decisiones de diseño

Inicialización Lazy: Los handlers de transferencias se crean de forma lazy (cuando se accede por primera vez) porque los métodos helper (_get_register_value, _set_register_value, _op_ld_r_r) se definen después del constructor. Esto evita problemas de orden de definición y mantiene el código organizado.

Helper Genérico: Se creó _op_ld_r_r() como handler genérico que acepta códigos de registro (0-7) en lugar de crear 63 funciones individuales. Esto reduce duplicación de código y facilita el mantenimiento.

Manejo de HALT: El estado HALT se verifica al inicio de step(). Si la CPU está en HALT y hay interrupciones pendientes (IME activado), se despierta automáticamente. Si no, consume 1 ciclo y retorna sin ejecutar ninguna instrucción.

Archivos Afectados

  • src/cpu/core.py - Añadido flag halted, modificado step(), implementados helpers y handlers de transferencias y HALT
  • tests/test_cpu_load8.py - Suite completa de tests TDD (8 tests) validando transferencias y HALT

Tests y Verificación

Se creó una suite completa de tests TDD con 8 tests que validan todas las funcionalidades:

  • test_ld_r_r: Verifica transferencia entre registros (LD A, D - 0x7A) con timing correcto (1 M-Cycle)
  • test_ld_r_hl: Verifica lectura desde memoria indirecta (LD B, (HL) - 0x46) con timing correcto (2 M-Cycles)
  • test_ld_hl_r: Verifica escritura a memoria indirecta (LD (HL), C - 0x71) con timing correcto (2 M-Cycles)
  • test_ld_all_registers: Verifica múltiples combinaciones de transferencias entre registros básicos
  • test_halt_sets_flag: Verifica que HALT activa el flag halted correctamente
  • test_halt_pc_does_not_advance: Verifica que en HALT el PC no avanza y se consume 1 ciclo por tick
  • test_halt_wake_on_interrupt: Verifica que HALT se despierta cuando IME está activado
  • test_ld_hl_hl_is_halt: Verifica que 0x76 es HALT, no LD (HL), (HL)

Resultado: Todos los tests pasan ✅ (8/8)

Prueba con ROM Real (Tetris DX)

Se ejecutó el emulador con Tetris DX para verificar que las transferencias permiten que el juego avance más allá de la inicialización:

  • ROM: tetris_dx.gbc (524,288 bytes, 512 KB ROM, 8 KB RAM)
  • Ciclos ejecutados: 30 M-Cycles
  • PC final: 0x1389
  • Resultado: El juego avanzó exitosamente desde la inicialización (PC=0x0100) hasta PC=0x1389, ejecutando código real del juego.
  • Error encontrado: Opcode 0xB3 (OR E / OR A, E) no implementado

Análisis del error: El opcode 0xB3 es una operación lógica OR entre el acumulador A y el registro E. Este opcode pertenece al rango 0xB0-0xB7 que contiene las operaciones OR con diferentes registros:

  • 0xB0: OR B
  • 0xB1: OR C
  • 0xB2: OR D
  • 0xB3: OR E ← Error aquí
  • 0xB4: OR H
  • 0xB5: OR L
  • 0xB6: OR (HL)
  • 0xB7: OR A

Conclusión: La implementación de transferencias funciona correctamente y permite que el juego ejecute código real. El siguiente paso lógico es implementar las operaciones lógicas (OR, AND, XOR) en el rango 0xA0-0xBF para continuar avanzando.

Fuentes Consultadas

  • Pan Docs: CPU Instruction Set - LD r, r' encoding (bloque 0x40-0x7F)
  • Pan Docs: CPU Instruction Set - HALT instruction (0x76)
  • Pan Docs: CPU Timing - M-Cycles para transferencias con/sin memoria

Implementación basada en documentación técnica de Pan Docs sobre la arquitectura LR35902.

Integridad Educativa

Lo que Entiendo Ahora

  • Bloque de Transferencias: El rango 0x40-0x7F es una matriz de 8x8 que codifica todas las combinaciones posibles de transferencias entre registros. La estructura es elegante y permite cubrir 63 opcodes con una implementación genérica.
  • HALT como Excepción: El opcode 0x76 es especial porque rompe el patrón de la matriz. En lugar de ser LD (HL), (HL) (que no tiene sentido), es HALT. Esto es una peculiaridad del diseño del hardware.
  • Timing Diferencial: Las transferencias que involucran memoria (usando (HL)) consumen 2 M-Cycles, mientras que las que solo involucran registros consumen 1 M-Cycle. Esto refleja el costo real del hardware.
  • Estado HALT: HALT es un estado especial de la CPU que permite esperar eventos (como interrupciones) sin consumir recursos innecesarios. Es fundamental para la sincronización en juegos.

Lo que Falta Confirmar

  • Despertar de HALT: La implementación actual simplifica el despertar de HALT asumiendo que si IME está activado, hay interrupciones pendientes. Cuando se implemente el manejo completo de interrupciones, se deberá verificar los registros IF (Interrupt Flag) e IE (Interrupt Enable) para determinar si realmente hay interrupciones pendientes.
  • Comportamiento de HALT con IME desactivado: En hardware real, cuando IME está desactivado y se ejecuta HALT, la CPU puede tener comportamientos especiales. Esto necesita verificación con documentación más detallada o tests con hardware real.

Hipótesis y Suposiciones

La implementación del despertar de HALT asume que si IME está activado, hay interrupciones pendientes. Esto es una simplificación que funcionará para la mayoría de casos, pero cuando se implemente el manejo completo de interrupciones, se deberá verificar explícitamente los registros IF e IE.

Próximos Pasos

  • [ ] Implementar más opcodes del set de instrucciones (operaciones lógicas, rotaciones, etc.)
  • [ ] Implementar manejo completo de interrupciones (IF, IE, vectores de interrupción)
  • [ ] Mejorar el despertar de HALT para verificar explícitamente interrupciones pendientes
  • [x] Probar el emulador con Tetris DX para verificar que puede avanzar más allá de la inicialización ✅
  • [ ] Implementar operaciones lógicas (OR, AND, XOR) en el rango 0xA0-0xBF