⚠️ Clean-Room / Educational

This project is educational and Open Source. No code is copied from other emulators. Implementation based solely on technical documentation (Pan Docs) and tests allowed.

← Return to Log

Step 0417: Fix CPU Unit Tests (Run from WRAM)

Executive Summary

Complete refactoring of the CPU unit test harness to run test programs fromWRAM (0xC000)instead of trying to write inROM (0x0000-0x7FFF). The original problem was thatPyMMU.write(0x0000)does not write memory (ROM is read-only), causing the CPU to read0x00(NOP) and the tests will not validate the actual instructions.

Result:6/6 tests passing(100% success rate)
Impact:Tests now execute actual instructions instead of NOPs, correctly validating the behavior of the CPU.

Hardware Concept: Game Boy Memory Map

The Game Boy has a well-defined memory map where each region has specific characteristics read/write:

Range Name Reading Writing Use
0x0000-0x7FFF ROM Game code (Read Only Memory)
0x8000-0x9FFF VRAM ✅* Tiles and background maps (*except Mode 3)
0xA000-0xBFFF External RAM Cartridge RAM (if any)
0xC000-0xDFFF WRAM Work RAM (internal work RAM)
0xE000-0xFDFF Echo RAM WRAM mirror (prohibited to use)
0xFE00-0xFE9F OAM ✅* Sprite Attribute Table (*except Mode 2/3)
0xFF00-0xFF7F I/O Registers Controls, video, audio, timer
0xFF80-0xFFFE HRAM High RAM
0xFFFF IE Register Interrupt Enable
Why WRAM for tests:
  • ROM (0x0000-0x7FFF)is read-only. ROM writes control the MBC (Memory Bank Controller), they do not write data.
  • WRAM (0xC000-0xDFFF)It is fully writeable and readable RAM, ideal for load test programs.
  • Real games use WRAM for temporary code, call stacks, and data buffers.
  • Running tests from WRAM is more realistic than modifying the MMU to allow writes to ROM.

Reference:Pan Docs - Memory Map (https://gbdev.io/pandocs/Memory_Map.html)

Implementation

1. Program Loading Helper

The file was createdtests/helpers_cpu.pywith a helper that:

  • Write a program (list of bytes) to WRAM
  • Configure the PC to run from that address
  • Verify that the writing was successful (read-back check)
# tests/helpers_cpu.py

# Constant: base address to execute test programs
TEST_EXEC_BASE = 0xC000 # WRAM

def load_program(mmu, regs, program_bytes: List[int], start_addr: int = TEST_EXEC_BASE) -> None:
    """
    Load a test program into memory and configure the PC.
    
    Args:
        mmu: PyMMU instance where to write the program
        regs: PyRegisters instance where to configure the PC
        program_bytes: List of bytes (opcodes and immediates) of the program
        start_addr: Start address (default TEST_EXEC_BASE = 0xC000)
    """
    # Write each byte of the program
    for i, byte_val in enumerate(program_bytes):
        addr = start_addr + i
        mmu.write(addr, byte_val)
    
    # Configure PC at program start
    regs.pc = start_addr
    
    # Verification: read back to confirm write
    for i, byte_val in enumerate(program_bytes):
        addr = start_addr + i
        read_back = mmu.read(addr)
        if read_back != byte_val:
            raise AssertionError(
                f"load_program: Verification failed at 0x{addr:04X}: "
                f"expected 0x{byte_val:02X}, read 0x{read_back:02X}"
            )

2. Test Refactoring

All tests intests/test_core_cpu.pywere refactored to use the new helper:

# BEFORE (wrote in ROM, didn't work)
mmu.write(0x0000, 0x3E) # LD A, d8
mmu.write(0x0001, 0x42)
regs.pc = 0x0000

# AFTER (run from WRAM, works)
load_program(mmu, regs, [0x3E, 0x42]) # LD A, 0x42
# PC is automatically set to TEST_EXEC_BASE (0xC000)

3. Test Opcode Correction

The testtest_unknown_opcode_returns_zeroused0xFF(RST 38h), whichyes it is implemented. It was changed to0xD3, an illegal opcode on Game Boy which is not defined in the instruction set.

# test_unknown_opcode_returns_zero (fixed)
load_program(mmu, regs, [0xD3]) # 0xD3 is illegal opcode
cycles = cpu.step()
assert cycles == 0, "Unknown opcode must return 0"

Tests and Verification

✅ All Tests Passing

Command:

pytest -v tests/test_core_cpu.py

Result:

tests/test_core_cpu.py::TestCoreCPU::test_cpu_initialization PASSED [ 16%]
tests/test_core_cpu.py::TestCoreCPU::test_nop_instruction PASSED [ 33%]
tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction PASSED [ 50%]
tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_multiple_executions PASSED [ 66%]
tests/test_core_cpu.py::TestCoreCPU::test_unknown_opcode_returns_zero PASSED [ 83%]
tests/test_core_cpu.py::TestCoreCPU::test_cpu_with_shared_mmu_and_registers PASSED [100%]

============================== 6 passed in 0.36s

Key Test Validation

Command:

pytest -v tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction --maxfail=1 -x

Result:

tests/test_core_cpu.py::TestCoreCPU::test_ld_a_d8_instruction PASSED [100%]

============================== 1 passed in 0.47s

Validated Test Code

def test_ld_a_d8_instruction(self):
    """Test: The LD A, d8 (0x3E) instruction works correctly."""
    mmu = PyMMU()
    regs = PyRegisters()
    cpu = PyCPU(mmu, regs)
    
    # Load test program in WRAM
    #0x3E = LD A, d8
    #0x42 = immediate value (d8)
    load_program(mmu, regs, [0x3E, 0x42])
    regs.a = 0x00 # Initialize A to 0
    
    # Run a loop
    cycles = cpu.step()
    
    # Check results
    assert cycles == 2, "LD A, d8 must consume 2 M-Cycles"
    assert regs.a == 0x42, "Register A must contain 0x42"
    assert regs.pc == TEST_EXEC_BASE + 2, "PC must be incremented by 2 bytes"
    assert cpu.get_cycles() == 2, "Cycle counter must be 2"

✅ C++ Compiled Module Validation

The tests validate the compiled C++/Cython module (viboy_core.so), not pure Python code.

Affected Files

  • Created:
    • tests/helpers_cpu.py- Program loading helper for tests
  • Modified:
    • tests/test_core_cpu.py- 6 refactored tests to run from WRAM

Conclusions

  • Robust tests:They now execute actual instructions instead of NOPs
  • Realism:Running from WRAM is closer to how real games work
  • Maintainability:Reusable helper for future CPU tests
  • Integrity:The MMU was not modified to allow writes to ROM (it would contaminate real emulation)
  • Discovery:Unknown opcode test revealed that 0xFF (RST 38h) is implemented
Next Steps:
  • Use this pattern for new CPU instruction tests
  • Expand test coverage to more opcodes
  • Consider edge case tests (flags, overflows, etc.)

Technical Environment

  • Compiler:GCC 13.2.0 (C++17)
  • Cython:3.0.11
  • Python:3.12.3
  • pytest:9.0.2
  • YOU:Ubuntu 24.04 LTS (Linux 6.14.0-37-generic)
  • Build: python3 setup.py build_ext --inplace