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.
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.
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 |
- 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
- 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