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 the Basic MMU
Summary
The class was implementedMMU(Memory Management Unit) that manages the Game Boy's 16-bit address space (0x0000 to 0xFFFF).
Implemented methods for reading and writing bytes (8 bits) and words (16 bits) with correct Little-Endian support.
Created a complete unit test suite with 13 tests, all passing correctly.
This is the necessary basis for the CPU to read instructions and data from memory.
Hardware Concept
The Game Boy has a 16-bit address space, allowing 65536 bytes (0x0000 to 0xFFFF) to be addressed. This space is not simply a linear data store; it's amemory mapwhere different address ranges They activate different hardware components.
Game Boy Memory Map
The address space is divided into specific regions:
- 0x0000 - 0x3FFF:ROM Bank 0 (Cartridge, non-changeable, 16KB)
- 0x4000 - 0x7FFF:ROM Bank N (Cartridge, switchable, 16KB)
- 0x8000 - 0x9FFF:VRAM (Video RAM, 8KB) - graphics memory
- 0xA000 - 0xBFFF:External RAM (Cartridge, switchable, 8KB)
- 0xC000 - 0xCFFF:WRAM Bank 0 (Working RAM, 4KB)
- 0xD000 - 0xDFFF:WRAM Bank 1-7 (Working RAM, switchable, 4KB)
- 0xE000 - 0xFDFF:Echo RAM (0xC000-0xDDFF mirror, do not use)
- 0xFE00 - 0xFE9F:OAM (Object Attribute Memory, 160 bytes) - sprites
- 0xFEA0 - 0xFEFF:Not usable (prohibited)
- 0xFF00 - 0xFF7F:I/O Ports - buttons, display, sound
- 0xFF80 - 0xFFFE:HRAM (High RAM, 127 bytes) - fast RAM
- 0xFFFF:IE (Interrupt Enable Register) - interrupt register
Endianness: Little-Endian (CRITICAL)
The Game Boy usesLittle-Endianfor 16-bit values. This means that:
- The byteleast significant (LSB)is stored at the lowest address
- The bytemost significant (MSB)is stored at the highest address (addr+1)
Practical example:If we want to store the value 0x1234 at address 0x1000:
- At 0x1000, 0x34 is written (LSB, bits 0-7)
- 0x1001 is written 0x12 (MSB, bits 8-15)
- When reading:
read_word(0x1000)= (0x12<< 8) | 0x34 = 0x1234
Why it is critical:If implemented incorrectly (Big-Endian), all 16-bit operations will fail, including addresses, counters and numerical values. It is one of the most common errors in novice emulators.
Linear Memory vs. Mapping by Regions
In this first iteration, we use abytearraylinear of 65536 bytes to simulate all memory.
This is enough to start running simple opcodes, but later we will need:
- Separate memory regions (ROM, VRAM, WRAM, I/O, etc.)
- Implement specific mapping for each region
- Manage "Bank Switching" for cartridge ROM and RAM
- Implement write protection on read-only regions
Implementation
The class was implementedMMUin Python with strict typing and complete educational documentation.
All methods ensure that addresses and values are in the valid range using bitwise masks.
Components created/modified
- MMU class:Manages the entire address space with a 65536-byte bytearray
- read_byte(addr):Reads one byte (8 bits) from the specified address
- write_byte(addr, value):Writes a byte (8 bits) to the specified address
- read_word(addr):Read a word (16 bits) using Little-Endian
- write_word(addr, value):Write a word (16 bits) using Little-Endian
Design decisions
- Linear storage:For now, a
bytearraysimple 65536 bytes. Later the regions will be separated. - Address masking:All addresses are masked with
& 0xFFFFto ensure they are in the valid range (0x0000-0xFFFF). This allows automatic wrap-around if an address is passed out of range. - Value masking:The values are masked to their correct size (
& 0xFFfor bytes,& 0xFFFFfor words) to simulate hardware behavior. - Explicit Little-Endian:The methods
read_wordandwrite_wordThey explicitly implement Little-Endian with detailed comments explaining byte order. - Wrap-around in limits:If a word is read/written at 0xFFFE, the second byte is read/written at 0xFFFF (wrap-around to 0x0000 does not apply here, but is handled correctly).
Package structure
The file was created__init__.pyinsrc/memory/to export the classMMUand convert the folder into a valid Python package.
Affected Files
src/memory/__init__.py- Memory module initialization (new)src/memory/mmu.py- MMU class with read/write methods (new, 185 lines)tests/test_mmu.py- Complete unit test suite (new, 195 lines)COMPLETE_REPORT.md- Log update (modified)docs/bitacora/index.html- Index update (changed)docs/bitacora/entries/2025-12-16__0002__mmu-basica.html- This entry (new)
Tests and Verification
A complete suite of unit tests was implemented withpytest:
- test_read_write_byte:Verifies basic byte read/write
- test_write_byte_wraps_value:Verify that values > 0xFF do wrap-around correctly
- test_write_byte_negative_value:Verify that negative values are converted correctly
- test_read_word_little_endian:CRITICAL Test - Verifies that read_word reads correctly in Little-Endian format (0xCD in addr, 0xAB in addr+1 → 0xABCD)
- test_write_word_little_endian:CRITICAL Test - Verifies that write_word writes correctly in Little-Endian format (0x1234 → 0x34 in addr, 0x12 in addr+1)
- test_read_write_word_roundtrip:Verify that writing and reading a word returns the original value
- test_write_word_wraps_value:Verify that values > 0xFFFF do wrap-around correctly
- test_address_wrap_around:Verify that out-of-range addresses do wrap-around
- test_read_word_at_boundary:Verifies reading of words at the space limit (0xFFFE)
- test_write_word_at_boundary:Check word writing at the edge of space
- test_memory_initialized_to_zero:Verify that memory is initialized to zero
- test_multiple_writes_same_address:Verify that multiple writes overwrite correctly
- test_little_endian_example_from_docs:Check the specific example mentioned in the documentation
Result:13 tests in total, all passing correctly (0.29s).
It was verified that there are no linting errors in the created files.
Sources consulted
- Bread Docs:Game Boy Memory Map - Description of the address space and its regions
- Bread Docs:Endianness - Little-Endian on the Game Boy explained
- General knowledge of architecture:Memory concepts in 8-bit systems and endianness
Note: Implementation was based on general knowledge of memory architecture and the Game Boy memory map specifications. The use of Little-Endian is a known feature of the actual Game Boy hardware.
Educational Integrity
What I Understand Now
- Little-Endian:The least significant byte (LSB) is stored at the lowest address. This is critical for all 16-bit operations. The correct implementation is:
(msb<< 8) | lsbwhen reading, and separating withvalue & 0xFF(LSB) and(value >> 8) & 0xFF(MSB) when writing. - Memory map:Address space is not just storage, but a map where different ranges activate different components. This will be important when we implement region-specific mapping.
- Wrap-around:Addresses and values that exceed their valid range must be wrapped around using bitwise masks. This simulates the behavior of real hardware.
- Memory initialized to zero:By default, all memory is initialized to 0x00. This is important for the initial behavior of the system.
What remains to be confirmed
- Starting values for specific regions:Some memory regions (such as I/O ports) may have boot-specific initial values. Pending verification with documentation or tests of permitted ROMs.
- Behavior of protected regions:Some regions are read-only (ROM) or have write restrictions. This will be implemented when we separate the regions.
- Bank Switching:The exact mechanism for changing cartridge ROM/RAM banks. Will be implemented when cartridge support is added.
- Echo RAM:The exact behavior of the Echo RAM region (0xE000-0xFDFF) that mirrors WRAM. Pending verification if there are subtle differences.
Hypotheses and Assumptions
It is assumed that the use of abytearraylinear is enough to start running simple opcodes.
This is a reasonable assumption for a first iteration, but will be validated when implementing opcodes that access different memory regions.
Wrap-around behavior using bitwise masks is assumed to be correct for all operations. This is consistent with how 8-bit systems work, but will be fully validated when opcodes that use indirect addressing and address arithmetic are implemented.
Next Steps
- [ ] Implement the basic CPU instruction cycle (Fetch-Decode-Execute)
- [ ] Implement simple opcode decoding (start with NOP, LD, etc.)
- [ ] Connect the CPU to the MMU so that it can read instructions from memory
- [ ] Implement separation of memory regions (ROM, VRAM, WRAM, I/O, etc.)
- [ ] Implement ROM loading system in the ROM region
- [ ] Implement specific mapping for basic I/O ports