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.
Cargas de 16 bits (BC, DE) y Comparaciones (CP)
Resumen
Implementación de cargas inmediatas de 16 bits para los registros BC y DE, almacenamiento indirecto usando BC y DE como punteros, y la instrucción crítica de comparación CP (Compare). Se añadió el helper _cp() que realiza una "resta fantasma" (actualiza flags sin modificar A) y se implementaron los opcodes LD BC, d16 (0x01), LD DE, d16 (0x11), LD (BC), A (0x02), LD (DE), A (0x12), CP d8 (0xFE) y CP (HL) (0xBE). Estas instrucciones son esenciales para que el emulador pueda avanzar más allá de la inicialización, permitiendo cargar constantes en registros pares y tomar decisiones condicionales. Suite completa de tests TDD (9 tests) validando todas las funcionalidades. También se corrigió un bug en la MMU donde el área de ROM (0x0000-0x7FFF) devolvía 0xFF cuando no había cartucho, impidiendo escribir/leer en memoria para tests.
Concepto de Hardware
Pares de Registros BC y DE: La CPU LR35902 tiene cuatro pares de registros de 16 bits: AF, BC, DE y HL. Ya teníamos implementado HL y SP. BC y DE son igualmente importantes:
- BC: Se usa frecuentemente como contador o puntero secundario en bucles.
- DE: Se usa frecuentemente como puntero de destino en operaciones de copia de datos (memcpy).
Almacenamiento Indirecto con BC y DE: Igual que con HL, podemos usar BC y DE como punteros de memoria. Las instrucciones LD (BC), A y LD (DE), A escriben el valor de A en la dirección apuntada por BC o DE respectivamente. Son muy comunes en bucles de limpieza de memoria o copia de datos.
La Instrucción CP (Compare) - Una Resta "Fantasma": CP es fundamentalmente una RESTA (SUB), pero con una diferencia crítica: descarta el resultado numérico y solo se queda con los Flags. El registro A NO se modifica.
CP se usa para comparaciones en código: "¿A == valor?", "¿A < valor?", etc.:
- Si
A == valor, la restaA - valores 0, así que se enciende el Flag Z (Zero). - Si
A < valor, la resta necesita "borrow", así que se enciende el Flag C (Carry/Borrow). - Si
A > valor, la resta es positiva, Z=0 y C=0.
Los flags se actualizan igual que en SUB:
- Z (Zero): 1 si A == valor (iguales)
- N (Subtract): Siempre 1 (es una resta)
- H (Half-Borrow): Si hubo borrow del bit 4 al 3 (nibble bajo)
- C (Borrow): Si hubo borrow del bit 7 (A < valor)
Sin CP, los juegos no pueden tomar decisiones condicionales como "¿He llegado al final del bucle?" o "¿Ha pulsado el usuario START?". Es una instrucción absolutamente crítica para la lógica de control.
Implementación
Se implementaron 6 nuevos opcodes siguiendo el patrón establecido de handlers dedicados en la tabla de despacho.
Helper _cp() para Comparaciones
Se creó el helper _cp(value) que reutiliza la lógica de _sub() pero con la diferencia crítica de que NO modifica el registro A. Calcula el resultado de A - value para determinar los flags, pero restaura el valor original de A al final (en realidad, nunca lo modifica, solo lo usa para calcular).
El helper actualiza los flags Z, N, H, C igual que _sub(), pero preserva A intacto. Esto es esencial para que CP funcione correctamente como una comparación "no destructiva".
Cargas de 16 bits: LD BC, d16 y LD DE, d16
Se implementaron _op_ld_bc_d16() (0x01) y _op_ld_de_d16() (0x11) siguiendo exactamente el mismo patrón que _op_ld_hl_d16() y _op_ld_sp_d16(). Leen 2 bytes en formato Little-Endian usando fetch_word() y cargan el valor en el par de registros correspondiente mediante registers.set_bc() o registers.set_de().
Ambas instrucciones consumen 3 M-Cycles (fetch opcode + fetch 2 bytes de valor).
Almacenamiento Indirecto: LD (BC), A y LD (DE), A
Se implementaron _op_ld_bc_ptr_a() (0x02) y _op_ld_de_ptr_a() (0x12) siguiendo el patrón de _op_ld_hl_ptr_a(). Obtienen la dirección apuntada por BC o DE, leen el valor de A, y escriben en memoria usando mmu.write_byte().
Ambas instrucciones consumen 2 M-Cycles (fetch opcode + write to memory).
Comparaciones: CP d8 y CP (HL)
Se implementaron _op_cp_d8() (0xFE) y _op_cp_hl_ptr() (0xBE). La primera lee un valor inmediato de 8 bits y lo compara con A. La segunda lee un valor de memoria apuntada por HL y lo compara con A. Ambas usan el helper _cp() para actualizar flags sin modificar A.
Ambas instrucciones consumen 2 M-Cycles (fetch opcode + fetch operand o read from memory).
Corrección de Bug en MMU
Se corrigió un bug crítico en MMU.read_byte() donde el área de ROM (0x0000-0x7FFF) devolvía siempre 0xFF cuando no había cartucho, incluso si se había escrito previamente en self._memory. Esto impedía que los tests funcionaran correctamente, ya que escribían opcodes en memoria pero luego se leían como 0xFF.
La solución fue modificar la lógica para que cuando no hay cartucho, se lea de self._memory en lugar de devolver 0xFF. Esto permite que los tests escriban y lean correctamente en el área de ROM, aunque en hardware real esta área sería de solo lectura (ROM del cartucho).
Componentes creados/modificados
src/cpu/core.py: Añadidos 6 nuevos handlers de opcodes y el helper _cp()src/memory/mmu.py: Corregido bug en read_byte() para área de ROM sin cartuchotests/test_cpu_load16_cp.py: Suite completa de 9 tests TDD
Decisiones de diseño
Se decidió reutilizar la lógica de _sub() para _cp() en lugar de duplicar código, pero asegurándose de que A no se modifique. Esto mantiene la consistencia en el cálculo de flags y reduce la posibilidad de errores.
Para la corrección del bug en MMU, se eligió permitir lectura/escritura en el área de ROM cuando no hay cartucho, ya que es necesario para los tests. En una implementación más completa, esto se manejaría mejor con regiones de memoria específicas, pero para esta etapa es suficiente.
Archivos Afectados
src/cpu/core.py- Añadidos 6 nuevos opcodes, helper _cp(), y entradas en tabla de despachosrc/memory/mmu.py- Corregido bug en read_byte() para área de ROM sin cartuchotests/test_cpu_load16_cp.py- Nueva suite de tests con 9 casos de prueba
Tests y Verificación
Se creó una suite completa de tests TDD con 9 casos de prueba:
- Tests de carga de 16 bits:
test_ld_bc_d16: Verifica que LD BC, d16 carga correctamente valores Little-Endiantest_ld_de_d16: Verifica que LD DE, d16 carga correctamente valores Little-Endian
- Tests de almacenamiento indirecto:
test_ld_bc_indirect_write: Verifica que LD (BC), A escribe correctamente en memoriatest_ld_de_indirect_write: Verifica que LD (DE), A escribe correctamente en memoria
- Tests de comparación:
test_cp_equality: Verifica que CP activa Z cuando A == valortest_cp_less: Verifica que CP activa C cuando A < valortest_cp_greater: Verifica que CP no activa C cuando A > valortest_cp_hl_ptr: Verifica que CP (HL) compara con valor en memoriatest_cp_half_carry: Verifica que CP actualiza H correctamente cuando hay half-borrow
Todos los tests pasan exitosamente (9/9), validando:
- Correcta carga de valores de 16 bits en registros BC y DE
- Correcto almacenamiento indirecto usando BC y DE como punteros
- Correcta actualización de flags en comparaciones (Z, N, H, C)
- Preservación del registro A en operaciones CP
- Consumo correcto de M-Cycles para cada instrucción
Fuentes Consultadas
- Pan Docs - CPU Instruction Set: https://gbdev.io/pandocs/CPU_Instruction_Set.html
- Pan Docs - CPU Flags: Referencia sobre actualización de flags en operaciones aritméticas
Nota: La implementación de CP sigue la especificación estándar de la arquitectura Z80/8080, de la cual LR35902 es derivada. El comportamiento de CP como "resta fantasma" es consistente con la documentación técnica.
Integridad Educativa
Lo que Entiendo Ahora
- CP es una resta fantasma: CP calcula A - valor para actualizar flags, pero descarta el resultado numérico. Solo los flags importan, A permanece intacto. Esto es fundamental para comparaciones condicionales.
- BC y DE como punteros: Igual que HL, BC y DE pueden usarse como punteros de memoria. LD (BC), A y LD (DE), A son muy comunes en bucles de inicialización y copia de datos.
- Importancia de CP: Sin CP, los juegos no pueden tomar decisiones condicionales. Es una instrucción crítica para cualquier lógica de control (if/else, loops, state machines).
- Área de ROM en tests: Para tests, necesitamos poder escribir en el área de ROM (0x0000-0x7FFF) aunque en hardware real sea de solo lectura. La corrección en MMU permite esto cuando no hay cartucho.
Lo que Falta Confirmar
- Comportamiento completo de CP: Todos los tests pasan, pero aún no hemos probado CP en situaciones más complejas (valores límite, wrap-around, etc.). Se validará cuando el emulador ejecute código real.
- Timing exacto: Los M-Cycles están correctos según documentación, pero el timing preciso de T-Cycles dentro de cada M-Cycle no está modelado. Esto se añadirá más adelante cuando sea necesario para precisión.
Hipótesis y Suposiciones
Asumimos que el comportamiento de CP es idéntico a SUB en cuanto a cálculo de flags, solo que no modifica A. Esto está respaldado por la documentación técnica y el comportamiento estándar de arquitecturas Z80/8080.
La corrección en MMU para permitir lectura/escritura en área de ROM cuando no hay cartucho es una simplificación para tests. En hardware real, esta área sería de solo lectura (ROM del cartucho), pero para nuestra implementación educativa es aceptable.
Próximos Pasos
- [ ] Continuar ejecutando el emulador con Tetris DX para identificar qué opcodes faltan
- [ ] Implementar más opcodes de carga (LD entre registros, LD con direccionamiento indirecto adicional)
- [ ] Implementar más operaciones aritméticas y lógicas (ADD, SUB, AND, OR, XOR con registros)
- [ ] Considerar implementar más variantes de CP (CP con otros registros, no solo d8 e (HL))
- [ ] Mejorar el manejo de regiones de memoria en MMU para ser más fiel al hardware real