Deep Dive: Game Boy CPU and Cartridge Emulation

Following the GPU implementation, I tackled the heart of the Game Boy: its Sharp LR35902 CPU (called the DMG-CPU by nintendo) and the Memory Bank Controller (MBC) systems that define cartridge behavior. This post explores the CPU's architecture, cartridge mapping intricacies, and the trials of aligning hardware quirks with modern emulation.


CPU Architecture and Registers

The LR35902 is a hybrid of the Z80 and Intel 8080, operating at 4.19 MHz. Key registers include:

  • 8-bit General Purpose: A, F, B, C, D, E, H, L.
  • 16-bit Special: SP (Stack Pointer), PC (Program Counter).
  • Flags (F Register): Zero (Z), Carry (C), Half-Carry (H), Subtract (N).

The CPU executes instructions in machine cycles, with each cycle tied to the system clock. Common operations include:

  • LD: Load values between registers or memory.
  • ADD/ADC: Arithmetic operations with and without carry.
  • JP/CALL: Control flow via absolute addresses.
  • PUSH/POP: Stack manipulation for 16-bit values.

Interrupt handling was particularly nuanced, with VBlank, LCD STAT, Timer, and Joypad interrupts requiring precise timing to avoid missed frames or input lag.


Cartridge Logic and MBC Breakdown

Cartridges determine how ROM and RAM are mapped into the Game Boy's memory. Memory Bank Controllers (MBCs) extend the limited address space by banking ROM/RAM dynamically.

No-MBC (ROM ONLY)

Used by: Tetris, Dr. Mario

ROM Limit: 32KB
RAM: None or fixed 8KB (0xA000–0xBFFF)

These simple cartridges have no banking logic. ROM is mapped directly to 0x0000–0x7FFF. If RAM is present, it's always accessible at 0xA000–0xBFFF with no need to enable or bank it.

MBC1

Used by: Super Mario Land 2, Final Fantasy Adventure

Max ROM: 2MB (128 banks)
Max RAM: 32KB (4 banks)

Registers:

  • 0x0000–0x1FFF: RAM Enable (write 0x0A to enable, anything else disables)
  • 0x2000–0x3FFF: Lower 5 bits of ROM bank number (bank 0 is not selectable here)
  • 0x4000–0x5FFF: RAM bank number or upper bits of ROM bank (depending on mode)
  • 0x6000–0x7FFF: Banking mode (0 = ROM banking, 1 = RAM banking)

In ROM banking mode, only ROM is switchable. In RAM banking mode, you can switch between 4 banks of RAM at 0xA000–0xBFFF. Switching to ROM bank 0 results in bank 1 due to hardware constraints.

MBC3 + RTC

Used by: Pokémon Gold/Silver/Crystal, Harvest Moon

Max ROM: 2MB
Max RAM: 32KB
Extra: Real-Time Clock (RTC)

Registers:

  • 0x0000–0x1FFF: RAM and RTC Enable
  • 0x2000–0x3FFF: ROM Bank select (7 bits, up to 128 banks)
  • 0x4000–0x5FFF: RAM Bank (0–3) or RTC Register Select (0x08–0x0C)
  • 0x6000–0x7FFF: Latch Clock Data (write 0x00 then 0x01)

The RTC provides real-time seconds, minutes, hours, and day counters. To read consistent values, a latch sequence must be performed. This emulation is tricky due to the need to simulate real-world time accurately and persist state across emulator sessions.

MBC5

Used by: Pokémon Crystal (Rev 1), Wario Land II, Cannon Fodder

Max ROM: 8MB (512 banks)
Max RAM: 128KB (16 banks)

Registers:

  • 0x0000–0x1FFF: RAM Enable
  • 0x2000–0x2FFF: Lower 8 bits of ROM bank
  • 0x3000–0x3FFF: 9th ROM bank bit (bit 8)
  • 0x4000–0x5FFF: RAM Bank select (0–15)

MBC5 simplifies ROM banking: it supports a full 9-bit ROM bank number across two registers, avoiding the weird bank-0 quirks of MBC1. It's common in late-generation titles due to its large capacity and cleaner implementation. It also supports rumble but the emulator doesn't implement this feature because most we do not have the hardware neccessary for it.

Bank switching is triggered by writing to designated memory regions. Timing and value masking are critical. For example, failing to mask the 9-bit ROM bank in MBC5 can result in undefined behavior or corrupted memory access.

Many MBC modes have a battery-backed state for saving and loading a game. This is done by writing to the RAM region and then saving the state to a file. It sounds intimidating at first but it is actually quite simple. The emulator will save the state to a file when the game is closed and load it when the game is opened, done.


Bugs & Challenges

The most anoying part of this project was debugging the "final" tests. I spent days trying to figure out why ROMS like Link's Awakening would crash or not load. I thought initially that is was a problem with my MBC implementation but it turned out to be a problem with the memory controller. I had not implemented the writes to the cartridge ram yet. This was a simple fix but it took me a while to figure out.

After the issue was resolved, I was able to run the game and see that the graphics were not rendering properly. It looked like a frame rate issue, so I started looking into the GPU and main emulation loop. I found that the GPU was not rendering the graphics properly because I had some sprite finding issues. It was not in the correct spot in the GPU cycle which caused things to load slowly and skip frames. This was again a simple fix but also took me a while to figure out.

The most anoying bug was by far the initial one I had to deal with. When trying to test NoMBC cartridges (since they were the first ones I implemented), the ROMS would just show a white screen. I knew this had to be an issue with the GPU but I could not figure out what was going on. After a long day of debugging, I found that my variables were unsigned when they should have been signed, they were also too small ints and were overflowing. This was such a simple fix but yet not obvious at all.


Conclusion

That wraps up the Game Boy emulator project. From figuring out undocumented CPU quirks to getting MBC1 ROM/RAM banking working, this whole thing was a mix of reverse engineering, trial and error, and way too many hours reading technical docs.

It wasn’t always fun — debugging silent white screens or chasing down timing bugs can get pretty frustrating — but finally seeing games boot and run properly made it all worth it. It’s been cool getting to understand how all the parts of the system fit together, from the boot ROM to the PPU to the APU.

I learned a lot, especially about low-level programming, hardware timing, and just how weird old consoles can be.

If you’re thinking about writing an emulator, give it a shot. You’ll learn more than you expect.

The full implementation is available on GitHub. Special thanks to the Pan Docs community for their invaluable resources.


Posted by: Aidan Vidal

Posted on: May 3, 2025