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.
Implementación de Saltos y Control de Flujo
Resumen
Implementación de instrucciones de salto (jumps) que permiten romper la ejecución lineal de la CPU, habilitando bucles y decisiones. Se implementaron saltos absolutos (JP nn), saltos relativos (JR e) y saltos condicionales (JR NZ, e). Concepto crítico: conversión de enteros sin signo a con signo (Two's Complement) para offsets relativos negativos. Implementación de timing condicional (diferentes ciclos según si se toma o no el salto). Suite completa de tests TDD (11 tests) validando saltos positivos, negativos y condicionales.
Concepto de Hardware
Saltos Absolutos (JP nn)
La instrucción JP nn (Jump to absolute address) carga una dirección de 16 bits directamente en el Program Counter (PC). El valor se lee en formato Little-Endian: el byte menos significativo (LSB) está en la dirección más baja, y el byte más significativo (MSB) está en la siguiente dirección.
Ejemplo: Si en memoria tenemos 0xC3 0x00 0xC0, la CPU lee 0x00C0 (Little-Endian) y
establece PC = 0xC000.
Saltos Relativos (JR e)
La instrucción JR e (Jump Relative) suma un offset de 8 bits (con signo) al PC actual. El offset se suma después de leer toda la instrucción (opcode + offset).
Si PC está en 0x0100 y ejecutamos JR +5:
- PC después de leer opcode: 0x0101
- PC después de leer offset: 0x0102
- PC final: 0x0102 + 5 = 0x0107
Enteros con Signo (Two's Complement) - Concepto Crítico
El concepto más importante de esta implementación es la representación de números negativos en complemento a 2 (Two's Complement) en 8 bits.
En Python, los enteros tienen precisión infinita. El valor 0xFF siempre es 255.
Sin embargo, en una CPU de 8 bits, el mismo byte puede representar dos valores diferentes según
el contexto:
- Sin signo (unsigned): 0x00-0xFF = 0-255
- Con signo (signed): 0x00-0x7F = 0-127, 0x80-0xFF = -128 a -1
Fórmula de conversión: Para convertir un byte sin signo (0-255) a entero con signo (-128 a +127) en Python:
signed_value = unsigned_value if unsigned_value < 128 else unsigned_value - 256
Ejemplos:
0x00= 0 (sin signo) = 0 (con signo)0x7F= 127 (sin signo) = 127 (con signo)0x80= 128 (sin signo) = -128 (con signo)0xFE= 254 (sin signo) = -2 (con signo)0xFF= 255 (sin signo) = -1 (con signo)
¿Por qué es crítico? Si no implementamos correctamente esta conversión, un salto
relativo negativo como JR -2 (codificado como 0x18 0xFE) saltaría hacia
adelante (a 0x0200) en lugar de retroceder (a 0x0100), rompiendo bucles infinitos y causando
comportamientos erróneos en el emulador.
Saltos Condicionales (JR NZ, e)
La instrucción JR NZ, e (Jump Relative if Not Zero) ejecuta un salto relativo solo si el flag Z (Zero) está desactivado (Z == 0). Si Z == 1, la CPU continúa con la siguiente instrucción sin saltar.
Timing Condicional: Esta instrucción tiene un comportamiento especial en cuanto a ciclos de máquina:
- Si se toma el salto (Z == 0): 3 M-Cycles (12 T-Cycles)
- Si NO se toma (Z == 1): 2 M-Cycles (8 T-Cycles)
Esto refleja el comportamiento real del hardware: cuando no se toma el salto, la CPU no necesita calcular la nueva dirección ni actualizar el PC, ahorrando un ciclo de máquina.
Fuente: Pan Docs - CPU Instruction Set (JP, JR instructions)
Implementación
Se añadieron tres helpers y tres opcodes nuevos a la CPU. La implementación utiliza la tabla de despacho existente, manteniendo la escalabilidad del código.
Helpers implementados
-
fetch_word(): Lee una palabra de 16 bits (Little-Endian) y avanza PC en 2 bytes. Usado por JP nn para leer direcciones absolutas. -
_read_signed_byte(): Lee un byte y lo convierte a entero con signo usando Two's Complement. Usado por instrucciones JR para leer offsets relativos.
Opcodes implementados
- 0xC3 - JP nn: Salto absoluto incondicional. Lee dirección de 16 bits y la carga en PC. Consume 4 M-Cycles.
- 0x18 - JR e: Salto relativo incondicional. Lee offset de 8 bits (signed) y lo suma al PC actual. Consume 3 M-Cycles.
- 0x20 - JR NZ, e: Salto relativo condicional. Salta solo si Z flag está desactivado. Consume 3 M-Cycles si salta, 2 M-Cycles si no salta.
Decisiones de diseño
-
Conversión signed: Se implementó la fórmula explícita
val if val < 128 else val - 256para mayor claridad y documentación del concepto Two's Complement, aunque Python tiene operaciones bitwise que podrían usarse. - Timing condicional: Se retorna el número correcto de M-Cycles según si se toma o no el salto, permitiendo una emulación precisa del comportamiento del hardware.
- Logging: Se añadieron logs de debug que muestran si un salto condicional se tomó o no, facilitando la depuración.
Archivos Afectados
src/cpu/core.py- Añadidos helpers fetch_word() y _read_signed_byte(), implementados opcodes JP nn, JR e y JR NZ,etests/test_cpu_jumps.py- Nuevo archivo con 11 tests exhaustivos para saltos absolutos, relativos y condicionalesdocs/bitacora/index.html- Actualizado con nueva entrada 0005docs/bitacora/entries/2025-12-16__0005__saltos-control-flujo.html- Nueva entrada de bitácoraINFORME_COMPLETO.md- Actualizado con registro de este paso
Tests y Verificación
Se creó una suite completa de 11 tests unitarios en tests/test_cpu_jumps.py que
validan todos los aspectos de las instrucciones de salto:
- Tests de JP nn (2 tests): Verificación de salto absoluto a diferentes direcciones, incluyendo wrap-around en 0xFFFF.
-
Tests de JR e (5 tests): Validación de saltos relativos positivos (+5, +127),
negativos (-2, -128), y offset cero. Test crítico:
test_jr_relative_negativeque verifica que 0xFE se interpreta como -2, no como 254. -
Tests de JR NZ, e (4 tests): Validación de saltos condicionales con diferentes
estados del flag Z. Tests críticos:
test_jr_nz_taken(3 ciclos) ytest_jr_nz_not_taken(2 ciclos) que verifican el timing condicional.
Validación manual: Se ejecutaron tests manuales en Python que verificaron:
- ✅ Conversión correcta de 0xFE a -2 (signed)
- ✅ JR -2 retrocede correctamente (PC: 0x0100 → 0x0100)
- ✅ JR NZ con Z=1 no salta y consume 2 ciclos
- ✅ JR NZ con Z=0 salta y consume 3 ciclos
Todos los tests pasan correctamente, validando que la implementación maneja correctamente Two's Complement y timing condicional.
Fuentes Consultadas
- Pan Docs - CPU Instruction Set: https://gbdev.io/pandocs/CPU_Instruction_Set.html
- Pan Docs - CPU Registers and Flags: https://gbdev.io/pandocs/CPU_Registers_and_Flags.html
- Documentación técnica sobre Two's Complement: Implementación basada en conocimiento estándar de arquitectura de computadores
Integridad Educativa
Lo que Entiendo Ahora
- Two's Complement en 8 bits: Entiendo cómo un mismo byte puede representar valores diferentes según el contexto (unsigned vs signed), y la importancia crítica de convertir correctamente en instrucciones de salto relativo. Sin esta conversión, los bucles infinitos no funcionarían correctamente.
- Timing condicional: Comprendo que las instrucciones condicionales pueden tener diferentes tiempos de ejecución según si se cumple o no la condición, reflejando el comportamiento real del hardware.
- Offset relativo: Entiendo que el offset en JR se suma al PC después de leer toda la instrucción, no al inicio. Esto es importante para calcular correctamente la dirección de destino.
Lo que Falta Confirmar
- Otras condiciones de salto: Solo se implementó JR NZ. Faltan JR Z, JR NC, JR C (condiciones basadas en flags C y Z). La lógica será similar, pero cada una tiene su opcode específico.
- JP condicionales: Existen versiones condicionales de JP (JP NZ, JP Z, etc.) que aún no están implementadas.
- CALL y RET: Para ejecutar subrutinas (funciones), se necesitan CALL (llamada) y RET (retorno), que requieren una pila (stack) funcional. Esto será el siguiente paso.
Hipótesis y Suposiciones
La implementación del timing condicional (3 ciclos si salta, 2 si no) está basada en la documentación de Pan Docs. No se ha verificado con hardware real, pero es la especificación estándar aceptada por la comunidad de emulación.
Próximos Pasos
- [ ] Implementar la Pila (Stack) para soportar CALL y RET
- [ ] Implementar CALL nn (llamada a subrutina absoluta)
- [ ] Implementar RET (retorno de subrutina)
- [ ] Implementar más condiciones de salto (JR Z, JR C, JR NC)
- [ ] Implementar JP condicionales (JP NZ, JP Z, etc.)
- [ ] Añadir más tests para verificar comportamiento con múltiples saltos anidados