Introduction

So far, our Game Boy emulator has a basic Memory Management Unit (MMU) that can handle the fixed 64KB memory map. This is sufficient for very small ROMs, but most commercial Game Boy games exceed this limit, often by megabytes. How did the original hardware manage this? Through a clever piece of hardware called a Memory Bank Controller (MBC).

In this chapter, we’ll extend our MMU to support MBCs. This is a critical milestone because it unlocks the ability to load and run a vast majority of Game Boy ROMs. We’ll focus on implementing the MBC1 type, which is one of the most common and fundamental MBCs. By the end of this chapter, your emulator will be able to dynamically switch between different ROM and external RAM banks, allowing it to access much larger cartridge data.

Planning & Design

The Game Boy’s CPU (a custom Z80 derivative) can only address 64KB of memory at any given time. However, Game Boy cartridges can contain up to 8MB of ROM and 32KB of external RAM. To bridge this gap, MBCs act as hardware components within the cartridge that intercept memory accesses to specific regions and “bank in” different parts of the larger ROM or RAM.

Game Boy Memory Map Review

Let’s quickly recap the relevant memory regions:

  • 0x0000 - 0x3FFF: ROM Bank 0 (fixed, always accessible)
  • 0x4000 - 0x7FFF: Switchable ROM Bank (controlled by MBC)
  • 0xA000 - 0xBFFF: Switchable External RAM Bank (controlled by MBC)

The key insight is that writes to specific addresses within the ROM regions (0x0000-0x7FFF) are not actually writing to ROM data. Instead, they are interpreted by the MBC as commands to switch banks, enable/disable RAM, or configure other MBC features.

MBC Architecture

We’ll introduce a new F# module, Gb.Mbc, to encapsulate all MBC-related logic. The Gb.Mbc module will hold the current state of the MBC (e.g., which ROM bank is selected, if RAM is enabled) and provide functions to:

  1. Initialize: Determine the MBC type from the cartridge header and set up initial state.
  2. Handle Writes: Process writes to MBC control registers within the ROM address space (0x0000-0x7FFF). These writes will update the internal MbcState.
  3. Map Addresses: Translate a logical Game Boy address (e.g., 0x4000) into a physical offset within the loaded cartridge ROM or external RAM data, based on the current MbcState.

The Gb.Mmu will then delegate these responsibilities to the Gb.Mbc module when accessing the relevant memory regions.

F# Type Design for MBCs

We’ll need a way to represent different MBC types and their state. A discriminated union is perfect for this:

type MbcType =
    | NoMbc
    | Mbc1
    | Mbc1Ram
    | Mbc1RamBattery
    // ... other MBC types as we implement them

type MbcState = {
    MbcType : MbcType
    RomBanks : byte [] // The entire ROM data from the cartridge
    RamBanks : byte [] // The entire external RAM data
    CurrentRomBank : int
    CurrentRamBank : int
    RamEnabled : bool
    BankingMode : int // 0 for ROM banking, 1 for RAM banking (MBC1 specific)
}

The MbcState will be part of our overall Gb.Mmu state, allowing the MMU to pass it around and update it.

Data Flow

Here’s how the MMU and MBC will interact for a memory write:

flowchart TD CPU --> MMU_Write[MMU Write Byte] MMU_Write --> A{Address Range} A -->|ROM Range| B{MBC Active} B -->|Yes| MBC_Handle[Handle MBC Write] MBC_Handle --> Return_State[Return MMU State] A -->|Other Ranges| C[MMU Direct Handle] C --> Write_Memory[Write WRAM VRAM] Write_Memory --> Return_State

And for a memory read:

flowchart TD CPU --> MMU_Read[MMU Read Byte] MMU_Read --> A{Address Range} A -->|ROM0| ROM0_Path[Handle ROM0] A -->|Switchable ROM| Switchable_ROM_Path[Handle Switchable ROM] A -->|External RAM| Ext_RAM_Path[Handle External RAM] A -->|Other Ranges| Direct_Read_Path[Handle Direct Read] ROM0_Path --> Return_Byte[Return Byte] Switchable_ROM_Path --> Return_Byte Ext_RAM_Path --> Return_Byte Direct_Read_Path --> Return_Byte

Step-by-Step Implementation

We’ll start by defining the MbcState and a helper to determine the MbcType. Then, we’ll build out the Gb.Mbc module to handle MBC1 logic. Finally, we’ll integrate it into our existing Gb.Mmu.

1. Define MBC Types and State

First, let’s create a new file Gb.Mbc.fs in your src/Gb directory.

src/Gb/Gb.Mbc.fs

module Gb.Mbc

open System

/// Represents the various Memory Bank Controller types supported by Game Boy cartridges.
type MbcType =
    | NoMbc // Cartridge without an MBC (small ROMs only)
    | Mbc1
    | Mbc1Ram
    | Mbc1RamBattery
    | Mbc2
    | Mbc2Battery
    | Mbc3
    | Mbc3Ram
    | Mbc3RamBattery
    | Mbc3TimerBattery
    | Mbc3TimerRamBattery
    | Mbc5
    | Mbc5Ram
    | Mbc5RamBattery
    | Mbc5Rumble
    | Mbc5RumbleRam
    | Mbc5RumbleRamBattery
    | MbcUnknown of byte // For unknown MBC types

/// Represents the current state of the Memory Bank Controller.
/// This state changes based on writes to specific memory addresses.
type MbcState = {
    MbcType : MbcType
    RomBanks : byte [] // The entire ROM data from the cartridge
    RamBanks : byte [] // The entire external RAM data (if any)
    CurrentRomBank : int // The currently selected switchable ROM bank (0x4000-0x7FFF)
    CurrentRamBank : int // The currently selected switchable external RAM bank (0xA000-0xBFFF)
    RamEnabled : bool // True if external RAM is enabled for read/write
    BankingMode : int // MBC1 specific: 0 for ROM banking mode, 1 for RAM banking mode
}

/// Determines the MBC type from the cartridge header byte at 0x0147.
let getMbcType (mbcByte : byte) =
    match mbcByte with
    | 0x00uy -> NoMbc
    | 0x01uy -> Mbc1
    | 0x02uy -> Mbc1Ram
    | 0x03uy -> Mbc1RamBattery
    | 0x05uy -> Mbc2
    | 0x06uy -> Mbc2Battery
    | 0x0Fuy -> Mbc3TimerBattery
    | 0x10uy -> Mbc3TimerRamBattery
    | 0x11uy -> Mbc3
    | 0x12uy -> Mbc3Ram
    | 0x13uy -> Mbc3RamBattery
    | 0x19uy -> Mbc5
    | 0x1Auy -> Mbc5Ram
    | 0x1Buy -> Mbc5RamBattery
    | 0x1Cuy -> Mbc5Rumble
    | 0x1Duy -> Mbc5RumbleRam
    | 0x1Euy -> Mbc5RumbleRamBattery
    | _      -> MbcUnknown mbcByte

/// Initializes the MBC state based on the cartridge ROM and type.
let init (mbcType : MbcType) (romData : byte []) (ramSize : int) =
    let ramBanks = Array.zeroCreate ramSize // Create RAM array if RAM exists
    { MbcType = mbcType
      RomBanks = romData
      RamBanks = ramBanks
      CurrentRomBank = 1 // ROM bank 1 is default on startup (0 is fixed)
      CurrentRamBank = 0
      RamEnabled = false
      BankingMode = 0 } // Default to ROM banking mode for MBC1

Explanation:

  • MbcType enumerates known MBCs. We’ve included many for future expansion, but will focus on Mbc1.
  • MbcState is a record holding all dynamic MBC configuration.
    • RomBanks and RamBanks store the raw data.
    • CurrentRomBank and CurrentRamBank track which bank is currently mapped into the CPU’s address space.
    • RamEnabled controls external RAM access.
    • BankingMode is specific to MBC1 and dictates how the CurrentRomBank and CurrentRamBank values are combined.
  • getMbcType reads the byte at 0x0147 in the cartridge header, which defines the MBC type.
  • init sets up the initial MbcState for a given cartridge. Note CurrentRomBank starts at 1 because 0 is fixed.

2. Implement MBC1 Logic

Now, let’s add the core logic for MBC1 within the Gb.Mbc module. This involves functions to handle writes to control registers and to map addresses.

src/Gb/Gb.Mbc.fs (add to existing file)

// ... (previous code)

/// Handles writes to MBC control registers (addresses 0x0000-0x7FFF).
/// This function updates the MbcState based on the address and value.
let handleMbc1Write (state : MbcState) (addr : int) (value : byte) =
    match addr with
    | _ when addr >= 0x0000 && addr <= 0x1FFF -> // RAM Enable/Disable
        { state with RamEnabled = (value &&& 0x0Fuy) = 0x0Auy }

    | _ when addr >= 0x2000 && addr <= 0x3FFF -> // ROM Bank Number (Lower 5 bits)
        let newRomBank = int (value &&& 0x1Fuy) // Mask to get lower 5 bits
        let newRomBank = if newRomBank = 0 then 1 else newRomBank // Bank 0 is always fixed, so 0 becomes 1
        let currentHigherBits = (state.CurrentRomBank >>> 5) &&& 0x03 // Get current higher 2 bits
        let finalRomBank = (currentHigherBits <<< 5) ||| newRomBank
        { state with CurrentRomBank = finalRomBank }

    | _ when addr >= 0x4000 && addr <= 0x5FFF -> // RAM Bank Number / ROM Bank Number (Higher 2 bits)
        let newBankValue = int (value &&& 0x03uy) // Mask to get lower 2 bits
        if state.BankingMode = 0 then // ROM Banking Mode
            let currentLowerBits = state.CurrentRomBank &&& 0x1F // Get current lower 5 bits
            let finalRomBank = (newBankValue <<< 5) ||| currentLowerBits
            { state with CurrentRomBank = finalRomBank }
        else // RAM Banking Mode
            { state with CurrentRamBank = newBankValue }

    | _ when addr >= 0x6000 && addr <= 0x7FFF -> // Banking Mode Select
        { state with BankingMode = int (value &&& 0x01uy) }

    | _ -> state // No relevant MBC1 action for other addresses

/// General MBC write handler that dispatches to specific MBC types.
let write (state : MbcState) (addr : int) (value : byte) =
    match state.MbcType with
    | Mbc1 | Mbc1Ram | Mbc1RamBattery -> handleMbc1Write state addr value
    | NoMbc -> state // No MBC, writes to ROM area are ignored
    | _ -> 
        // For other MBC types or unknown, we just ignore writes to these control registers for now
        // A full emulator would implement these specifically.
        state

/// Maps a logical ROM address (0x4000-0x7FFF) to a physical offset in the RomBanks array.
let mapRomAddress (state : MbcState) (addr : int) =
    let romBankSize = 0x4000 // 16KB per ROM bank
    let bankOffset = (state.CurrentRomBank % (state.RomBanks.Length / romBankSize)) * romBankSize
    let addressInBank = addr - 0x4000 // Offset within the 16KB bank
    bankOffset + addressInBank

/// Maps a logical external RAM address (0xA000-0xBFFF) to a physical offset in the RamBanks array.
let mapRamAddress (state : MbcState) (addr : int) =
    if state.RamEnabled then
        let ramBankSize = 0x2000 // 8KB per RAM bank
        let bankOffset = (state.CurrentRamBank % (state.RamBanks.Length / ramBankSize)) * ramBankSize
        let addressInBank = addr - 0xA000 // Offset within the 8KB bank
        Some (bankOffset + addressInBank)
    else
        None // RAM is not enabled

/// Reads a byte from the mapped external RAM, if enabled.
let readRam (state : MbcState) (addr : int) =
    match mapRamAddress state addr with
    | Some physicalAddr when physicalAddr < state.RamBanks.Length ->
        state.RamBanks.[physicalAddr]
    | _ ->
        0xFFuy // Return 0xFF if RAM is disabled or address is out of bounds

Explanation of MBC1 Logic:

  • RAM Enable (0x0000-0x1FFF): Writing 0x0A (10 decimal) to any address in this range enables external RAM. Any other value disables it. We mask the value (value &&& 0x0Fuy) just in case.
  • ROM Bank Number (Lower 5 bits) (0x2000-0x3FFF): This range sets the lower 5 bits of the CurrentRomBank. Since ROM bank 0 is always fixed, writing 0 actually selects bank 1. We combine these 5 bits with the higher 2 bits (set in the next range) to form the full 7-bit ROM bank number.
  • RAM Bank Number / ROM Bank Number (Higher 2 bits) (0x4000-0x5FFF): This range’s behavior depends on BankingMode.
    • If BankingMode is 0 (ROM Banking Mode), these 2 bits become the higher bits of the CurrentRomBank, allowing access to larger ROMs (up to 2MB with 7 bits).
    • If BankingMode is 1 (RAM Banking Mode), these 2 bits select one of 4 external RAM banks (up to 32KB).
  • Banking Mode Select (0x6000-0x7FFF): Writing 0 selects ROM Banking Mode, 1 selects RAM Banking Mode.
  • mapRomAddress: Calculates the physical offset in RomBanks for a given logical address in the switchable ROM bank region. It uses CurrentRomBank.
  • mapRamAddress: Calculates the physical offset in RamBanks for a given logical address in the external RAM region, only if RAM is enabled. It returns an option type to indicate if RAM access is valid.
  • readRam: A helper to safely read from external RAM.

3. Integrate MBC into MMU

Now, we need to update our Gb.Mmu module to use the Gb.Mbc functionality.

First, ensure Gb.Mmu.fs references Gb.Mbc. You might need to adjust your .fsproj file to ensure Gb.Mbc.fs is compiled before Gb.Mmu.fs.

Gb.fsproj (example order)

<ItemGroup>
    <Compile Include="Gb/Gb.Mbc.fs" />
    <Compile Include="Gb/Gb.Mmu.fs" />
    <Compile Include="Gb/Gb.Cpu.fs" />
    <!-- ... other files ... -->
</ItemGroup>

Next, modify Gb.Mmu.fs.

src/Gb/Gb.Mmu.fs

module Gb.Mmu

open System
open Gb.Mbc // Add this line to import the MBC module

/// Represents the Game Boy's entire memory state.
type MmuState = {
    // ... (existing fields like VRAM, WRAM, OAM, IoRegisters, etc.) ...
    BootRom : byte []
    CartridgeRom : byte [] // This will store the *entire* ROM data
    MbcState : MbcState // Add the MBC state here
}

/// Initializes the MMU with a given boot ROM and cartridge ROM.
let init (bootRomData : byte []) (cartridgeRomData : byte []) =
    let mbcType = Gb.Mbc.getMbcType cartridgeRomData.[0x0147] // Get MBC type from cartridge header
    let ramSize =
        match cartridgeRomData.[0x0149] with // Cartridge RAM size byte
        | 0x00uy -> 0 // No RAM
        | 0x01uy -> 0x00002000 // 8KB (MBC2 specific)
        | 0x02uy -> 0x00008000 // 8KB
        | 0x03uy -> 0x00020000 // 32KB
        | 0x04uy -> 0x00080000 // 128KB
        | 0x05uy -> 0x00040000 // 64KB
        | _      -> 0 // Unknown or unsupported RAM size

    let mbcState = Gb.Mbc.init mbcType cartridgeRomData ramSize

    { // ... (existing initializations for VRAM, WRAM etc.) ...
      BootRom = bootRomData
      CartridgeRom = cartridgeRomData // Store the full ROM
      MbcState = mbcState }

/// Reads a byte from memory at the given address.
let readByte (state : MmuState) (addr : int) =
    match addr with
    // ... (existing boot ROM and other fixed memory region checks) ...
    | _ when addr >= 0x0000 && addr <= 0x3FFF -> // ROM Bank 0 (fixed)
        if state.BootRomEnabled && addr < state.BootRom.Length then
            state.BootRom.[addr]
        else
            state.CartridgeRom.[addr]

    | _ when addr >= 0x4000 && addr <= 0x7FFF -> // Switchable ROM Bank
        // Delegate to MBC to map the address
        let physicalAddr = Gb.Mbc.mapRomAddress state.MbcState addr
        state.CartridgeRom.[physicalAddr]

    | _ when addr >= 0xA000 && addr <= 0xBFFF -> // External RAM (switchable)
        match Gb.Mbc.readRam state.MbcState addr with
        | value -> value
        | exception _ -> 0xFFuy // Fallback if readRam throws (e.g., if MBC type isn't fully implemented)

    // ... (rest of existing readByte match cases) ...

/// Writes a byte to memory at the given address.
let writeByte (state : MmuState) (addr : int) (value : byte) =
    match addr with
    // ... (existing boot ROM and other fixed memory region checks) ...
    | _ when addr >= 0x0000 && addr <= 0x7FFF -> // MBC Control Registers (ROM area)
        // Writes to these addresses control the MBC, not the ROM data itself
        let newMbcState = Gb.Mbc.write state.MbcState addr value
        { state with MbcState = newMbcState }

    | _ when addr >= 0xA000 && addr <= 0xBFFF -> // External RAM (switchable)
        if state.MbcState.RamEnabled then
            match Gb.Mbc.mapRamAddress state.MbcState addr with
            | Some physicalAddr when physicalAddr < state.MbcState.RamBanks.Length ->
                state.MbcState.RamBanks.[physicalAddr] <- value
                state // No change to MmuState, only MbcState.RamBanks is mutable
            | _ -> state // RAM disabled or out of bounds, ignore write
        else
            state // RAM is not enabled, ignore write

    // ... (rest of existing writeByte match cases) ...

Key Changes in Gb.Mmu.fs:

  1. open Gb.Mbc: Imports the new module.
  2. MmuState update: Added MbcState : MbcState to hold the MBC’s dynamic configuration.
  3. init function:
    • Reads the MBC type from cartridgeRomData.[0x0147].
    • Determines RAM size from cartridgeRomData.[0x0149].
    • Initializes MbcState using Gb.Mbc.init.
  4. readByte function:
    • For 0x4000-0x7FFF (switchable ROM), it now calls Gb.Mbc.mapRomAddress to get the correct physical offset into CartridgeRom.
    • For 0xA000-0xBFFF (External RAM), it calls Gb.Mbc.readRam to safely read from the mapped RAM if enabled.
  5. writeByte function:
    • For 0x0000-0x7FFF (ROM area), it now calls Gb.Mbc.write to update the MbcState. This is crucial as these writes are MBC commands.
    • For 0xA000-0xBFFF (External RAM), it checks state.MbcState.RamEnabled and then uses Gb.Mbc.mapRamAddress to write to the correct location in state.MbcState.RamBanks (which is a mutable array).

โšก Quick Note: Mutable RAM

Notice that state.MbcState.RamBanks.[physicalAddr] <- value is a mutable operation. While F# strongly favors immutability, hardware emulation often requires mutable arrays for memory regions like RAM for performance and directness. We encapsulate this mutability within the MbcState record and the Gb.Mmu module, ensuring that the mutation is explicit and contained. The writeByte function still returns the same MmuState record, but its internal MbcState.RamBanks array has been modified.

4. Update Cartridge Loading

Finally, ensure your main application (e.g., Program.fs or Gb.Emulator.fs) loads the full cartridge ROM data and passes it to the Mmu.init function. Previously, you might have only loaded the first 64KB. Now, you need to load the entire file.

src/Gb/Gb.Emulator.fs (or similar main file)

module Gb.Emulator

open System.IO
open Gb.Cpu
open Gb.Mmu
open Gb.Ppu
open Gb.Input
// ... other modules

type EmulatorState = {
    Cpu : CpuState
    Mmu : MmuState
    Ppu : PpuState
    Input : InputState
    // ... other components
    Cycles : int6}

/// Loads a Game Boy ROM file into the emulator.
let loadRom (filePath : string) =
    let romData = File.ReadAllBytes filePath // Read the entire ROM file
    let bootRomData = File.ReadAllBytes "path/to/your/bootrom.gb" // Ensure you have a boot ROM

    let initialMmu = Mmu.init bootRomData romData
    let initialCpu = Cpu.init
    let initialPpu = Ppu.init
    let initialInput = Input.init

    { Cpu = initialCpu
      Mmu = initialMmu
      Ppu = initialPpu
      Input = initialInput
      Cycles = 0L }

// ... rest of your emulator loop

Testing & Verification

Now that we’ve implemented MBC1, it’s time to test it.

1. Obtain Test ROMs

The most reliable way to test MBC functionality is using specific test ROMs, such as those from Blargg’s Game Boy CPU/MMU Tests. You can find these by searching for “Blargg Game Boy MBC1 tests”.

Specifically, look for ROMs like:

  • mbc1/rom_512kb.gb
  • mbc1/rom_1mb.gb
  • mbc1/rom_2mb.gb
  • mbc1/ram_256kb.gb (if your MBC1 implementation supports RAM)

2. Run Test ROMs

Load one of these test ROMs into your emulator. If your MBC1 implementation is correct, the ROM should boot up and display messages indicating successful bank switching or RAM access. Blargg’s tests are designed to output specific messages to the serial port (which we haven’t emulated yet) or to the screen. You’ll primarily rely on observing the screen output.

3. Debugging Checks

  • Console Logging: Add printfn statements in Gb.Mbc.fs within handleMbc1Write to log when a bank switch occurs:
    // ... inside handleMbc1Write
    | _ when addr >= 0x2000 && addr <= 0x3FFF ->
        let newRomBank = int (value &&& 0x1Fuy)
        let newRomBank = if newRomBank = 0 then 1 else newRomBank
        let currentHigherBits = (state.CurrentRomBank >>> 5) &&& 0x03
        let finalRomBank = (currentHigherBits <<< 5) ||| newRomBank
        printfn "MBC1: ROM Bank changed to %d" finalRomBank // <--- Add this
        { state with CurrentRomBank = finalRomBank }
    
    This will give you visibility into whether your MBC logic is being triggered and if the CurrentRomBank is being updated as expected.
  • Memory Inspection: If your emulator has a debugging mode or can dump memory, inspect the contents of 0x4000-0x7FFF after a bank switch. The data there should change.
  • RAM Access: If testing with MBC1+RAM, ensure that writing to 0xA000-0xBFFF only works when RamEnabled is true, and that reads return the expected values.

Production Considerations

MBCs are fundamental to Game Boy emulation. Getting them right is critical for compatibility.

  • Performance: The MMU’s readByte and writeByte functions are called millions of times per second. While the indirection through Gb.Mbc is necessary, ensure that the banking logic within mapRomAddress and mapRamAddress is as efficient as possible. Avoid unnecessary allocations or complex computations in these hot paths.
  • Completeness: While we focused on MBC1, a production-grade emulator needs to support MBC3 (for RTC and larger ROMs) and MBC5 (for larger ROMs and rumble). Each MBC has its own quirks and control registers. Design your Gb.Mbc module to easily extend with new MbcType cases and corresponding handlers.
  • Maintainability: The use of discriminated unions for MbcType makes it explicit which MBC is active. When adding new MBCs, you’ll extend the MbcType union and add new functions like handleMbc3Write and mapMbc3Address, then dispatch to them from the general write and read functions in Gb.Mbc.

Common Issues & Solutions

  1. Incorrect Bank Calculation:
    • Issue: Off-by-one errors or incorrect masking when calculating CurrentRomBank or CurrentRamBank. Especially common with MBC1’s newRomBank = if newRomBank = 0 then 1 else newRomBank rule.
    • Solution: Double-check the bitwise operations (&&&, |||, >>>, <<<) and ensure the “ROM bank 0 becomes 1” rule is strictly followed. Use a debugger to step through the handleMbc1Write function and verify CurrentRomBank after each write.
  2. RAM Not Enabling/Disabling:
    • Issue: External RAM isn’t behaving as expected (e.g., writes don’t persist, reads return 0xFF).
    • Solution: Verify the RamEnabled flag is correctly set by writes to 0x0000-0x1FFF. Ensure the exact magic value 0x0A is being checked for. Also, confirm that mapRamAddress returns None when RamEnabled is false.
  3. Cartridge ROM/RAM Size Mismatch:
    • Issue: Emulator crashes or reads garbage when accessing ROM/RAM.
    • Solution: Ensure init correctly reads the cartridge header bytes 0x0148 (ROM size) and 0x0149 (RAM size) and allocates RomBanks and RamBanks arrays of the appropriate size. My init function for Gb.Mbc uses a simplified ramSize calculation; for a full emulator, you’d need to map 0x0148 to actual ROM bank counts and 0x0149 to actual RAM sizes more rigorously as per Pan Docs.
    • โšก Real-world insight: Cartridge header parsing is a surprisingly common source of bugs in emulators. The Pan Docs are your authoritative source for these bytes.

Summary & Next Step

In this chapter, you’ve taken a significant leap forward by implementing Memory Bank Controller (MBC) support, specifically for MBC1. Your emulator can now:

  • Correctly identify the MBC type from a cartridge header.
  • Handle writes to MBC control registers to switch ROM and RAM banks.
  • Map logical Game Boy addresses to physical offsets within larger ROM and external RAM data.

This means your emulator can now run many more Game Boy games that rely on MBC1 for memory management. You’ve also seen how to integrate mutable state (for RAM) carefully within a functional F# codebase.

Next, we’ll continue refining our emulator by diving into the Picture Processing Unit (PPU) Part 1, focusing on rendering the background and tiles.

๐Ÿง  Check Your Understanding

  • What problem do Memory Bank Controllers solve for the Game Boy?
  • Explain the role of the BankingMode register in MBC1.
  • Why is it important to handle writes to 0x0000-0x7FFF differently when an MBC is present?

โšก Mini Task

  • Research the MBC3 type (e.g., using Pan Docs) and list at least two key differences in its banking mechanism compared to MBC1.

๐Ÿš€ Scenario

You’re debugging a Game Boy ROM that uses MBC1. The game loads, displays the Nintendo logo, but then freezes on a black screen when it tries to load the main game content. You suspect an MBC issue. What are the first three things you would check in your MBC1 implementation to diagnose this problem?

๐Ÿ“Œ TL;DR

  • MBCs extend Game Boy memory by banking ROM and RAM.
  • MBC1 uses writes to ROM addresses as control commands for bank switching.
  • Gb.Mmu now delegates ROM/external RAM access to Gb.Mbc.

๐Ÿง  Core Flow

  1. MMU init reads cartridge header to determine MBC type and RAM size.
  2. MMU readByte for 0x4000-0x7FFF and 0xA000-0xBFFF calls Gb.Mbc to map physical addresses.
  3. MMU writeByte for 0x0000-0x7FFF calls Gb.Mbc.write to update MBC state.

๐Ÿš€ Key Takeaway

Effective emulation of hardware often requires carefully modeling memory management units and their controllers, translating logical CPU addresses to physical storage locations through stateful logic.


References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.