This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Implementation of Jumps and Flow Control
Summary
Implementation of jump instructions that allow breaking the linear execution of the CPU, enabling loops and decisions. Absolute jumps (JP nn), relative jumps (JR e) were implemented and conditional jumps (JR NZ, e). Critical Concept: Converting Unsigned to Signed Integers (Two's Complement) for negative relative offsets. Implementation of conditional timing (different cycles depending on whether or not the jump is taken). Complete TDD test suite (11 tests) validating jumps positive, negative and conditional.
Hardware Concept
Absolute Jumps (JP nn)
The instructionJP nn(Jump to absolute address) loads a 16-bit address directly into the Program Counter (PC). The value is read in Little-Endian format: the least byte significant byte (LSB) is at the lowest address, and the most significant byte (MSB) is at the following address.
Example: If in memory we have0xC3 0x00 0xC0, the CPU reads 0x00C0 (Little-Endian) and
set PC = 0xC000.
Relative Jumps (JR e)
The instructionJR and(Jump Relative) adds an 8-bit (signed) offset to the PC current. The offset is addedafterto read the entire instruction (opcode + offset).
If PC is at 0x0100 and we executeJR +5:
- PC after reading opcode: 0x0101
- PC after reading offset: 0x0102
- End PC: 0x0102 + 5 = 0x0107
Signed Integers (Two's Complement) - Critical Concept
The most important concept of this implementation is the representation ofnegative numbers in 2's complement (Two's Complement)in 8 bits.
In Python, integers have infinite precision. The value0xFFIt is always 255.
However, on an 8-bit CPU, the same byte can represent two different values depending on
the context:
- Unsigned: 0x00-0xFF = 0-255
- Signed: 0x00-0x7F = 0-127, 0x80-0xFF = -128 to -1
Conversion formula: To convert an unsigned byte (0-255) to a signed integer (-128 to +127) in Python:
signed_value = unsigned_value if unsigned_value < 128 else unsigned_value - 256
Examples:
0x00= 0 (unsigned) = 0 (signed)0x7F= 127 (unsigned) = 127 (signed)0x80= 128 (unsigned) = -128 (signed)0xFE= 254 (unsigned) = -2 (signed)0xFF= 255 (unsigned) = -1 (signed)
Why is it critical?If we do not implement this conversion correctly, a jump
negative relative likeJR-2(encoded as0x18 0xFE) would jump towards
forward (to 0x0200) instead of backward (to 0x0100), breaking infinite loops and causing
wrong behaviors in the emulator.
Conditional Jumps (JR NZ, e)
The instructionJR NZ, e(Jump Relative if Not Zero) executes a relative jump only if the Z (Zero) flag is disabled (Z == 0). If Z == 1, the CPU continues with the next instruction without skipping.
Conditional Timing: This instruction has special behavior regarding machine cycles:
- If you take the jump(Z == 0): 3 M-Cycles (12 T-Cycles)
- If NOT taken(Z == 1): 2 M-Cycles (8 T-Cycles)
This reflects actual hardware behavior: when the jump is not taken, the CPU does not need calculate the new address or update the PC, saving a machine cycle.
Fountain:Pan Docs - CPU Instruction Set (JP, JR instructions)
Implementation
Three helpers and three new opcodes were added to the CPU. The implementation uses the table existing dispatch, maintaining the scalability of the code.
Helpers implemented
-
fetch_word(): Read a 16-bit word (Little-Endian) and move forward PC in 2 bytes. Used by JP nn to read absolute addresses. -
_read_signed_byte(): Reads a byte and converts it to a signed integer using Two's Complement. Used by JR instructions to read relative offsets.
Opcodes implemented
- 0xC3 - JP nn: Unconditional absolute jump. Reads 16-bit address and load on PC. Consumes 4 M-Cycles.
- 0x18 - JR e: Unconditional relative jump. Reads 8-bit offset (signed) and adds to the current PC. Consumes 3 M-Cycles.
- 0x20 - JR NZ, e: Conditional relative jump. Jumps only if Z flag is disabled. Consumes 3 M-Cycles if jumping, 2 M-Cycles if not jumping.
Design decisions
-
signed conversion: Explicit formula implemented
val if val < 128 else val - 256For clarity and documentation of the Two's Complement concept, although Python has bitwise operations that could be used. - Conditional timing: The correct number of M-Cycles is returned depending on whether you take or not the jump, allowing precise emulation of hardware behavior.
- Logging: Added debug logs that show whether a conditional jump is took or not, making debugging easier.
Affected Files
src/cpu/core.py- Added fetch_word() and _read_signed_byte() helpers, implemented JP nn, JR e and JR NZ,e opcodestests/test_cpu_jumps.py- New file with 11 exhaustive tests for absolute, relative and conditional jumpsdocs/bitacora/index.html- Updated with new entry 0005docs/bitacora/entries/2025-12-16__0005__saltos-control-flujo.html- New log entryCOMPLETE_REPORT.md- Updated with record of this step
Tests and Verification
A complete suite of 11 unit tests was created intests/test_cpu_jumps.pythat
They validate all aspects of the jump instructions:
- JP tests nn (2 tests): Absolute jump verification to different addresses, including wrap-around at 0xFFFF.
-
JR e tests (5 tests): Validation of positive relative jumps (+5, +127),
negative (-2, -128), and zero offset. Critical test:
test_jr_relative_negativethat verify that 0xFE is interpreted as -2, not 254. -
JR NZ tests, e (4 tests): Validation of conditional jumps with different
Z flag states. Critical tests:
test_jr_nz_taken(3 cycles) andtest_jr_nz_not_taken(2 cycles) that verify the conditional timing.
Manual validation:Manual tests were executed in Python that verified:
- ✅ Correct conversion from 0xFE to -2 (signed)
- ✅ JR -2 rolls back correctly (PC: 0x0100 → 0x0100)
- ✅ JR NZ with Z=1 does not jump and consumes 2 cycles
- ✅ JR NZ with Z=0 jumps and consumes 3 cycles
All tests pass correctly, validating that the implementation correctly handles Two's Complement and conditional timing.
Sources consulted
- 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
- Technical documentation on Two's Complement: Implementation based on standard knowledge of computer architecture
Educational Integrity
What I Understand Now
- Two's Complement in 8 bits: I understand how the same byte can represent different values depending on the context (unsigned vs signed), and the critical importance of correctly convert to relative jump instructions. Without this conversion, the loops infinites would not work correctly.
- Conditional timing: I understand that conditional statements can have different execution times depending on whether the condition is met or not, reflecting the real hardware behavior.
- Relative offset: I understand that the offset in JR is added to the PCafterRead the entire instruction, not at the beginning. This is important to correctly calculate the destination address.
What remains to be confirmed
- Other jump conditions: Only JR NZ implemented. JR Z, JR NC, are missing JR C (conditions based on flags C and Z). The logic will be similar, but each one has its own specific opcode.
- Conditional JPs: There are conditional versions of JP (JP NZ, JP Z, etc.) that are not yet implemented.
- CALL and RET: To execute subroutines (functions), CALL is needed (call) and RET (return), which require a functional stack. This will be next step.
Hypotheses and Assumptions
The implementation of conditional timing (3 cycles if it jumps, 2 if it doesn't) is based on the Pan Docs documentation. It has not been verified with real hardware, but it is the specification standard accepted by the emulation community.
Next Steps
- [ ] Implement the Stack to support CALL and RET
- [ ] Implement CALL nn (absolute subroutine call)
- [ ] Implement RET (subroutine return)
- [ ] Implement more jump conditions (JR Z, JR C, JR NC)
- [ ] Implement conditional JPs (JP NZ, JP Z, etc.)
- [ ] Add more tests to verify behavior with multiple nested jumps