This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation and permitted tests.
Indirect Memory and Increment/Decrement
Summary
Implementation of indirect addressing using HL as memory pointer, LDI/LDD operations
(automatic pointer increment/decrement) and unary increment/decrement operations (INC/DEC)
with correct handling of flags. Critical helpers implemented_inc_nand_dec_nthat update flags Z, N, H but do NOT touch the C (Carry) flag, an important hardware quirk
LR35902 which many emulators fail to implement. These opcodes are essential for cleaning loops
of memory that games run at startup (memset). Complete suite of TDD tests validating memory
indirection, wrap-around of pointers and correct behavior of flags in INC/DEC.
Hardware Concept
Indirect Addressing (HL):When an instruction uses(H.L.), I don't know
uses the value of the HL register directly, but HL is used as amemory addressto read or write. It is analogous to a pointer in C:*ptr. For example, if HL contains0xC000and we executeRHP (HL), A, we write the value of A in the address
memory0xC000, not in the HL record itself.
LDI / LDD (Load with Increment/Decrement):These instructions are "Swiss army knives"
that combine a memory operation with automatic increment or decrement of the pointer. For example,RHP (HL+), A(LDI) writes A to the address pointed to by HL and increments HL by a single
cycle. This is ideal for quick memory copy or initialization loops (memset),
since it avoids having to increment the pointer manually in a separate statement.
Flags in INC/DEC (CRITICAL):The operationsINCandDECof
8 bits affect the Z (Zero), N (Subtract) and H (Half-Carry) flags, butThey do NOT affect the C flag
(Carry). This is an important quirk of the LR35902 hardware that many emulators fail at
when implementing, breaking the conditional logic that depends on keeping the C flag intact for
increment/decrement operations.
- Flag H in INC:It is activated if there is an overflow from bit 3 to 4 (low nibble).
Example:
0x0F + 1 = 0x10activates H because there was carry from bit 3 to 4. - Flag H in DEC:It is activated if there is a loan from bit 4 to 3 (low nibble).
Example:
0x10 - 1 = 0x0Factivates H because there was borrowing from bit 4 to 3. - Flag C: DO NOT TOUCH, even if there is overflow (INC 0xFF -> 0x00) or underflow (DEC 0x00 -> 0xFF). This preservation of the C flag is critical for code that uses INC/DEC in loops with C based conditions.
Why does this unlock Tetris?Tetris (and most Game Boy games) use memory cleanup loops at startup that do something like:
LD HL, 0xDFFF ; End of RAM
XOR A ; A = 0
loop:
LD (HL-), A ; Write 0 to RAM and move the pointer down
BIT 7, H ; Check if you have reached the end...
JR NZ, loop ; Repeat
WithoutRHP (HL-), A(LDD) implemented, the emulator cannot execute this loop
memory cleaning.
Implementation
Generic helpers implemented_inc_nand_dec_nthat encapsulate the
increment/decrement logic with correct flag management. These helpers are reused in
all INC/DEC opcodes of individual registers (B, C, D, E, H, L, A).
Components created/modified
- ALU Helpers:
_inc_n(val)and_dec_n(val)- Increase/decrease an 8-bit value, they update flags Z, N, H, but DO NOT touch C. - Indirect memory opcodes:
0x77:RHP (HL), A- Write A in the address pointed by HL0x22:RHP (HL+), A(LDI) - Write A in (HL) and increment HL0x32:RHP (HL-), A(LDD) - Write A in (HL) and decrement HL0x2A:LD A, (HL+)(LDI) - Read from (HL) to A and increment HL
- Increment/decrement opcodes:
0x04:INC B0x05:DEC B0x0C:INC C0x0D:DEC C0x3C:INC A0x3D:DEC A
- Tests:
test_cpu_memory_ops.py- Complete suite of memory tests hint and INC/DEC flags
Design decisions
Generic Helpers vs. duplicate code:Helpers were created_inc_nand_dec_nto avoid duplicating the flag update logic in each INC/DEC opcode.
This facilitates maintenance and ensures consistency. All 8-bit INC/DEC opcodes use these
helpers, which guarantees that the behavior of flags is identical in all cases.
Explicit preservation of the C flag:It was explicitly documented in code and tests that INC/DEC DO NOT touch the C flag. This is critical because many developers (and emulators) assume incorrectly that any arithmetic operation must update all flags. The tests They explicitly verify that C is preserved even in overflow/underflow cases.
Wrap-around of pointers:LDI/LDD operations apply explicit wrap-around
using& 0xFFFFto ensure that HL is always in the valid 16-bit range.
This allows loops to work correctly even if they reach space limits
of addresses.
Affected Files
src/cpu/core.py- Added helpers_inc_nand_dec_n, and handlers for indirect memory and INC/DEC opcodestests/test_cpu_memory_ops.py- New suite of tests for indirect memory and behavior of flags in INC/DEC. Fixed to use addresses outside ROM area (0x8000+) to allow writing test code.
Tests and Verification
A complete test suite was created intest_cpu_memory_ops.pywhich validates:
- Basic indirect memory:
RHP (HL), Awrite correctly in the address pointed to by HL without modifying HL - LDI (increment):
RHP (HL+), AandLD A, (HL+)write/read and increment HL correctly, including wrap-around (0xFFFF -> 0x0000) - LDD (decrement):
RHP (HL-), Awrite and decrement HL correctly, including wrap-around (0x0000 -> 0xFFFF) - INC with flags:Normal cases, Half-Carry (0x0F -> 0x10), and overflow (0xFF -> 0x00) verifying that C does NOT change
- DEC with flags:Normal cases, Half-Borrow (0x10 -> 0x0F), and underflow checking that C does NOT change
- Preservation of C:Explicit tests verifying that C is preserved even when is active before INC/DEC
- Variants:Tests for INC/DEC of B, C, A verifying consistent behavior
Unit tests:14 tests in pytest covering all the critical cases mentioned previously. The tests follow the TDD pattern established in the project.All tests pass correctlyafter fixing an initial problem with memory address usage.
Fix applied:Initially, the tests attempted to write code in0x0100(ROM area 0x0000-0x7FFF), but the MMU reads from the cartridge in that area, not from internal memory.
This caused the tests to read0xFFinstead of written opcodes. It was corrected
changing all tests to use addresses outside the ROM area (0x8000+), where
writing works correctly. This fix documents an important aspect of memory mapping:
ROM areas are read-only from the program's perspective, while
RAM/VRAM allow reading and writing.
Sources consulted
- Bread Docs:CPU Instruction Set- Behavior of flags in INC/DEC, indirect addressing
- Bread Docs:CPU Registers and Flags- Detailed description of flags and behavior of arithmetic operations
- Implementation based on standard technical documentation of the LR35902 hardware. No code was consulted from other emulators to maintain clean-room integrity.
Educational Integrity
What I Understand Now
- Indirect addressing:I understand that
(H.L.)means "the value at the memory address pointed to by HL", not "the value of HL". It's like using a pointer in C. - Flags in INC/DEC:I understand that 8-bit INC/DEC does NOT touch the C flag, even with overflow/underflow. This preservation is critical and many emulators fail it. The Half-Carry (H) is calculated differently in INC (carry from bit 3 to 4) vs DEC (borrow from bit 4 to 3).
- LDI/LDD:I understand that these instructions are optimizations for loops, combining memory operation with pointer update in a single cycle. LDI increases, LDD decreases.
- Wrap-around:I understand that 16-bit pointers do wrap-around using
& 0xFFFF. This is important for loops that reach the limits of the space. addresses.
What remains to be confirmed
- INC/DEC of HL (16 bits):INC HL and DEC HL need to be implemented, which are different (they do not affect flags). These are useful for loops that need to increment/decrement pointers 16 bit.
- INC/DEC of (HL):INC (HL) and DEC (HL) that increase/decrement need to be implemented the value in memory pointed to by HL. These also affect flags Z, N, H but not C.
- Validation with test ROMs:It would be ideal to validate with redistributable test ROMs Try memory cleanup loops to confirm that the behavior is correct.
- Exact timing:For now we use approximate M-Cycles. The exact timing of LDI/LDD It might differ slightly on the actual hardware, but for most cases it should be correct.
- Memory mapping in tests:Confirmed that the ROM area (0x0000-0x7FFF) is only reading from the cartridge, while the RAM/VRAM areas (0x8000+) allow writing. Tests must use addresses outside of ROM in order to write test code.
Hypotheses and Assumptions
The implementation of flags in INC/DEC is based on standard technical documentation (Pan Docs) that indicates explicitly that C is not touched. The tests verify this behavior, but I have not been able to validate it directly with real hardware. The preservation of C is a widely documented feature. known from the LR35902 hardware, but it is easy to miss if you do not carefully read the documentation.
The behavior of Half-Carry in DEC (activated when the low nibble is 0, indicating borrow) is implemented based on the logic of how borrowing works in binary subtraction. If the nibble low is 0 and we decrement, we need to borrow from the high nibble, activating H. This logic is consistent with how Half-Carry works in normal subtraction.
Lesson learned on memory mapping:During the development of the tests, we discovered
that the MMU has a different behavior for reading and writing in the ROM area (0x0000-0x7FFF).
Reading is always done from the cartridge (if it exists), while writing is done in memory
internal, but is not visible in subsequent readings. This is consistent with how real hardware works:
the cartridge ROM is read-only from the program perspective. The tests were corrected to
use addresses outside the ROM area (0x8000+) where the writing works correctly.
This discovery reinforces the importance of understanding the complete memory mapping of the system.
Next Steps
- [ ] Implement
BIT 7, H(BIT instruction) needed for Tetris cleanup loop - [ ] Implement more INC/DEC opcodes (D, E, H, L, (HL)) to complete the set
- [ ] Implement INC HL / DEC HL (16 bits, do not affect flags) for pointer loops
- [ ] Run Tetris trace to verify that the cleanup loop is working correctly
- [ ] Validate with redistributable test ROMs if available