The CPU you started building in the last chapter is blind without memory. It can execute instructions, but it can’t load programs, store data, or interact with any of the Game Boy’s peripherals like the screen or sound chip. This is where the Memory Management Unit (MMU) comes in.
This chapter guides you through creating the Game Boy’s core memory system, the Memory Management Unit (MMU). You’ll learn about the Game Boy’s memory map, how to model different memory regions, and implement the fundamental readByte and writeByte operations crucial for any emulator. By the end, your emulator will be able to load a Game Boy ROM into its virtual memory, a significant step towards running actual games.
Introduction
In any computer system, the CPU doesn’t directly access every component. Instead, it interacts with a unified address space, and a component called the MMU translates those addresses to the correct physical memory location or hardware register. For the Game Boy, this system is relatively simple but absolutely critical.
This milestone focuses on building a functional Memory module that encapsulates the Game Boy’s 64KB (0x0000-0xFFFF) address space. You’ll define the different memory regions, implement logic to handle reads and writes to these regions, and crucially, enable loading a cartridge ROM into the system.
By the end of this chapter, your emulator will:
- Have a structured representation of the Game Boy’s memory.
- Be able to load a Game Boy ROM file into the designated memory region.
- Support basic
readByteandwriteByteoperations across various memory areas, allowing the CPU to fetch instructions and interact with RAM.
Planning & Design
The Game Boy’s CPU (a custom Sharp SM83) has a 16-bit address bus, meaning it can address 2^16 = 65,536 bytes (64KB) of memory. This 64KB is divided into distinct regions, each serving a specific purpose:
Game Boy Memory Map (Simplified Overview):
| Address Range | Size | Description | Access |
|---|---|---|---|
0x0000 - 0x7FFF | 32KB | Cartridge ROM (Bank 0 and Switchable Banks) | Read Only |
0x8000 - 0x9FFF | 8KB | VRAM (Video RAM) | Read/Write |
0xA000 - 0xBFFF | 8KB | External RAM (Cartridge RAM, if present) | Read/Write |
0xC000 - 0xDFFF | 8KB | WRAM (Work RAM) | Read/Write |
0xE000 - 0xFDFF | 7.5KB | Echo RAM (Mirror of 0xC000-0xDFFF) | Read/Write |
0xFE00 - 0xFE9F | 160 bytes | OAM (Object Attribute Memory, for sprites) | Read/Write |
0xFEA0 - 0xFEFF | 96 bytes | Unusable/Forbidden | No Access |
0xFF00 - 0xFF7F | 128 bytes | I/O Registers (LCD, Joypad, Sound, Timers) | Read/Write |
0xFF80 - 0xFFFE | 127 bytes | HRAM (High RAM) | Read/Write |
0xFFFF | 1 byte | Interrupt Enable Register | Read/Write |
๐ Key Idea: The MMU’s job is to act as a switchboard. When the CPU asks for data at address 0x1000, the MMU knows to fetch it from the cartridge ROM. If it asks for 0xD000, it goes to WRAM.
Designing the Memory Module
We’ll model the MMU as an F# record type holding byte arrays for each distinct, addressable memory region. This approach offers clear separation and allows us to use F#’s powerful pattern matching for efficient address decoding.
File Structure
We’ll create a new file, Mmu.fs, to house our memory logic. We might also create a MemoryMap.fs for constants, though for now, we can keep constants within Mmu.fs for simplicity.
GameboyEmulator/
โโโ src/
โ โโโ GameboyEmulator/
โ โ โโโ Cpu.fs
โ โ โโโ Mmu.fs <-- New file for Memory Management Unit
โ โ โโโ Program.fs
โ โ โโโ GameboyEmulator.fsproj
Step-by-Step Implementation
1. Define Memory Constants
First, let’s define the memory region boundaries as constants. These will be crucial for dispatching read/write operations.
Create a new file src/GameboyEmulator/Mmu.fs and add the following:
// src/GameboyEmulator/Mmu.fs
module Mmu
/// Defines the Game Boy's memory map boundaries and sizes.
module MemoryMap =
let RomStart = 0x0000u
let RomEnd = 0x7FFFu // 32KB
let VramStart = 0x8000u
let VramEnd = 0x9FFFu // 8KB
let ExtRamStart = 0xA000u
let ExtRamEnd = 0xBFFFu // 8KB
let WramStart = 0xC000u
let WramEnd = 0xDFFFu // 8KB
let EchoRamStart = 0xE000u
let EchoRamEnd = 0xFDFFu // 7.5KB - mirror of WRAM
let OamStart = 0xFE00u
let OamEnd = 0xFE9Fu // 160 bytes
let UnusableStart = 0xFEA0u
let UnusableEnd = 0xFEFFu // 96 bytes - should not be accessed
let IoStart = 0xFF00u
let IoEnd = 0xFF7Fu // 128 bytes
let HramStart = 0xFF80u
let HramEnd = 0xFFFEu // 127 bytes
let IeRegister = 0xFFFFu // 1 byte
let WramSize = (WramEnd - WramStart + 1u) |> int
let VramSize = (VramEnd - VramStart + 1u) |> int
let OamSize = (OamEnd - OamStart + 1u) |> int
let IoSize = (IoEnd - IoStart + 1u) |> int
let HramSize = (HramEnd - HramStart + 1u) |> int
// Note: ROM size varies based on cartridge. We'll handle a default 32KB for now.
// External RAM also varies. We'll reserve 8KB for now.
let ExtRamSize = (ExtRamEnd - ExtRamStart + 1u) |> int
Explanation:
module Mmuandmodule MemoryMap: We’re using nested modules to organize our code, keeping memory-related constants withinMemoryMapfor clarity.usuffix: Denotes an unsigned 16-bit integer (uint16), which is appropriate for Game Boy addresses.intconversion: Used when calculating array sizes, as F# arrays expectintfor their length.
2. Define the Memory Record
Now, let’s define the Memory record type. This record will hold the byte arrays representing our different memory regions.
Append this to src/GameboyEmulator/Mmu.fs:
// src/GameboyEmulator/Mmu.fs (continued)
/// Represents the Game Boy's entire memory space.
type Memory = {
rom : byte array // Cartridge ROM (first 32KB, more with MBCs)
vram : byte array // Video RAM
extRam : byte array // External RAM (on cartridge)
wram : byte array // Work RAM
oam : byte array // Object Attribute Memory
io : byte array // I/O Registers
hram : byte array // High RAM
ie : byte // Interrupt Enable Register
// Add other components as they are implemented
}
Explanation:
type Memory = { ... }: This F# record defines the state of our MMU. Each field is abyte arrayfor memory regions or a singlebytefor specific registers.rom: This will initially hold the first 32KB of the cartridge. Later, we’ll introduce Memory Bank Controllers (MBCs) to swap in larger ROMs.ie: This is a single byte register, so it’s directly represented asbyte.
3. Initialize Memory
We need a way to create an initial Memory state, typically filled with default values (like 0xFF for uninitialized memory, or 0x00).
Append this to src/GameboyEmulator/Mmu.fs:
// src/GameboyEmulator/Mmu.fs (continued)
/// Creates an initial Memory state, typically with all bytes set to 0xFF.
let init () : Memory =
{
rom = Array.create (MemoryMap.RomEnd - MemoryMap.RomStart + 1u |> int) 0x00uy // Default to 0x00 for ROM, will be overwritten
vram = Array.create MemoryMap.VramSize 0xFFuy
extRam = Array.create MemoryMap.ExtRamSize 0xFFuy
wram = Array.create MemoryMap.WramSize 0xFFuy
oam = Array.create MemoryMap.OamSize 0xFFuy
io = Array.create MemoryMap.IoSize 0xFFuy
hram = Array.create MemoryMap.HramSize 0xFFuy
ie = 0x00uy // Interrupt Enable Register typically starts at 0
}
Explanation:
let init () : Memory: A function that returns a newMemoryrecord.Array.create size value: F# function to create a byte array of a givensize, initialized withvalue.0xFFuy: Theuysuffix denotes an unsigned byte. Game Boy memory is typically initialized to0xFFunless specified otherwise, representing an “open bus” state. We initialize ROM to0x00as it will be immediately overwritten.
4. Load Cartridge ROM
The core purpose of the MMU is to load the game itself. This function takes the raw bytes of a Game Boy ROM file and places them into the rom region of our Memory state.
Append this to src/GameboyEmulator/Mmu.fs:
// src/GameboyEmulator/Mmu.fs (continued)
/// Loads a Game Boy ROM byte array into the memory's ROM region.
let loadRom (romData: byte array) (memory: Memory) : Memory =
let romSize = min romData.Length memory.rom.Length // Don't write beyond allocated ROM
Array.blit romData 0 memory.rom 0 romSize
{ memory with rom = memory.rom } // Return new memory state with updated ROM
Explanation:
loadRom (romData: byte array) (memory: Memory) : Memory: This function takes the ROM data and the currentMemorystate, returning a newMemorystate with the ROM loaded. This adheres to functional programming principles of immutability.Array.blit: Efficiently copies a section of one array to another.romData: Source array.0: Source start index.memory.rom: Destination array.0: Destination start index.romSize: Number of elements to copy.
{ memory with rom = memory.rom }: This is F#’s record update syntax. It creates a newMemoryrecord, copying all fields from the originalmemorybut explicitly settingromto the (now modified)memory.romarray. While the array itself is mutable, theMemoryrecord reference is immutable, which is a common F# pattern for performance-critical mutable data structures within immutable records.
5. Implement readByte
This is the central function for fetching data from memory. Given an address, it determines which memory region that address falls into and returns the byte from that region.
Append this to src/GameboyEmulator/Mmu.fs:
// src/GameboyEmulator/Mmu.fs (continued)
/// Reads a single byte from the specified memory address.
let readByte (addr: uint16) (memory: Memory) : byte =
match addr with
| a when a >= MemoryMap.RomStart && a <= MemoryMap.RomEnd ->
memory.rom.[int (a - MemoryMap.RomStart)]
| a when a >= MemoryMap.VramStart && a <= MemoryMap.VramEnd ->
memory.vram.[int (a - MemoryMap.VramStart)]
| a when a >= MemoryMap.ExtRamStart && a <= MemoryMap.ExtRamEnd ->
memory.extRam.[int (a - MemoryMap.ExtRamStart)]
| a when a >= MemoryMap.WramStart && a <= MemoryMap.WramEnd ->
memory.wram.[int (a - MemoryMap.WramStart)]
| a when a >= MemoryMap.EchoRamStart && a <= MemoryMap.EchoRamEnd ->
// Echo RAM is a mirror of WRAM (0xC000-0xDFFF)
let wramAddr = a - MemoryMap.EchoRamStart + MemoryMap.WramStart
memory.wram.[int (wramAddr - MemoryMap.WramStart)]
| a when a >= MemoryMap.OamStart && a <= MemoryMap.OamEnd ->
memory.oam.[int (a - MemoryMap.OamStart)]
| a when a >= MemoryMap.UnusableStart && a <= MemoryMap.UnusableEnd ->
// Reading from unusable area returns 0xFF
0xFFuy
| a when a >= MemoryMap.IoStart && a <= MemoryMap.IoEnd ->
memory.io.[int (a - MemoryMap.IoStart)]
| a when a >= MemoryMap.HramStart && a <= MemoryMap.HramEnd ->
memory.hram.[int (a - MemoryMap.HramStart)]
| a when a = MemoryMap.IeRegister ->
memory.ie
| _ ->
// Unknown or unhandled address, typically returns 0xFF (open bus)
// In a real emulator, this might log a warning or be more specific.
0xFFuy // Default for unmapped memory
Explanation:
match addr with | a when ... ->: This is F#’s powerful pattern matching. We match theaddragainst ranges defined inMemoryMap.a - MemoryMap.RomStart: We subtract the base address of the region to get the correct offset within that region’s dedicated byte array.int (...): Converts theuint16offset to anintfor array indexing.Echo RAM: This region (0xE000-0xFDFF) is a direct mirror of WRAM (0xC000-0xDFFF). Reads/writes to Echo RAM directly affect WRAM. OurreadBytelogic correctly redirects the access.Unusable/Unknown: For regions that shouldn’t be accessed or are not yet implemented, we return0xFFuy, which is common behavior for “open bus” or uninitialized memory in hardware.
6. Implement writeByte
Similar to readByte, this function handles writing data to the correct memory region based on the address.
Append this to src/GameboyEmulator/Mmu.fs:
// src/GameboyEmulator/Mmu.fs (continued)
/// Writes a single byte to the specified memory address.
let writeByte (addr: uint16) (value: byte) (memory: Memory) : Memory =
match addr with
| a when a >= MemoryMap.RomStart && a <= MemoryMap.RomEnd ->
// ROM is read-only, writes are typically ignored or modify MBC registers
// For now, we'll just ignore it.
memory // Return unchanged memory
| a when a >= MemoryMap.VramStart && a <= MemoryMap.VramEnd ->
memory.vram.[int (a - MemoryMap.VramStart)] <- value
memory // Return unchanged memory (array modified in place)
| a when a >= MemoryMap.ExtRamStart && a <= MemoryMap.ExtRamEnd ->
memory.extRam.[int (a - MemoryMap.ExtRamStart)] <- value
memory
| a when a >= MemoryMap.WramStart && a <= MemoryMap.WramEnd ->
memory.wram.[int (a - MemoryMap.WramStart)] <- value
memory
| a when a >= MemoryMap.EchoRamStart && a <= MemoryMap.EchoRamEnd ->
// Echo RAM is a mirror of WRAM
let wramAddr = a - MemoryMap.EchoRamStart + MemoryMap.WramStart
memory.wram.[int (wramAddr - MemoryMap.WramStart)] <- value
memory
| a when a >= MemoryMap.OamStart && a <= MemoryMap.OamEnd ->
memory.oam.[int (a - MemoryMap.OamStart)] <- value
memory
| a when a >= MemoryMap.UnusableStart && a <= MemoryMap.UnusableEnd ->
// Writes to unusable area are ignored
memory
| a when a >= MemoryMap.IoStart && a <= MemoryMap.IoEnd ->
memory.io.[int (a - MemoryMap.IoStart)] <- value
memory
| a when a >= MemoryMap.HramStart && a <= MemoryMap.HramEnd ->
memory.hram.[int (a - MemoryMap.HramStart)] <- value
memory
| a when a = MemoryMap.IeRegister ->
{ memory with ie = value } // Return new memory state with updated IE register
| _ ->
// Unknown or unhandled address, ignore write for now
memory
Explanation:
writeByte (addr: uint16) (value: byte) (memory: Memory) : Memory: Takes the address, value to write, and currentMemorystate, returning a newMemorystate.<-: This is the F# mutable assignment operator. Sincebyte arrayis a mutable reference type, we can modify its contents directly.{ memory with ie = value }: For theie(Interrupt Enable) register, which is a singlebytefield in theMemoryrecord, we use the record update syntax to create a newMemoryrecord with the updatedievalue. This maintains immutability for the record itself.- ROM Write: Writes to ROM (
0x0000-0x7FFF) are generally ignored by the hardware, or they control Memory Bank Controllers (MBCs) which we’ll implement later. For now, we simply return the originalmemoryunchanged. - Unusable/Unknown: Writes to these areas are also ignored.
Testing & Verification
Now that we have our Memory module, let’s verify its basic functionality. We’ll add some simple tests to Program.fs to ensure ROM loading and memory R/W operations work as expected.
Open src/GameboyEmulator/Program.fs and modify it:
// src/GameboyEmulator/Program.fs
open System
open System.IO
open Mmu // Import our new Mmu module
open Cpu // Assuming you have Cpu.fs from the previous chapter
// Represents the overall state of the Game Boy system
type GameBoy = {
cpu : Cpu.State
memory : Mmu.Memory
// Other components will be added here (PPU, APU, etc.)
}
[<EntryPoint>]
let main argv =
printfn "Starting Game Boy Emulator..."
// --- MMU Testing ---
let initialMemory = Mmu.init ()
printfn "Memory initialized."
// Create a dummy ROM for testing
let dummyRom = [| 0xC3uy; 0x00uy; 0x01uy; 0xDEuy; 0xADuy; 0xBEuy; 0xEFuy |] // Simple JR 0x01, then some data
let memoryWithRom = Mmu.loadRom dummyRom initialMemory
printfn "Dummy ROM loaded."
// Verify ROM read
let romByte0 = Mmu.readByte 0x0000u memoryWithRom
let romByte1 = Mmu.readByte 0x0001u memoryWithRom
let romByte2 = Mmu.readByte 0x0002u memoryWithRom
printfn "Read from ROM: 0x%02X, 0x%02X, 0x%02X (Expected: 0xC3, 0x00, 0x01)" romByte0 romByte1 romByte2
// Expected: 0xC3, 0x00, 0x01
// Verify WRAM write/read
let addressWram = Mmu.MemoryMap.WramStart + 0x10u // Example address in WRAM
let memoryAfterWramWrite = Mmu.writeByte addressWram 0xAAuy memoryWithRom
let readWram = Mmu.readByte addressWram memoryAfterWramWrite
printfn "Wrote 0xAA to WRAM at 0x%04X, Read: 0x%02X (Expected: 0xAA)" addressWram readWram
// Expected: 0xAA
// Verify Echo RAM write/read (should affect WRAM)
let addressEchoRam = Mmu.MemoryMap.EchoRamStart + 0x10u // Echo of WRAM 0xC010
let memoryAfterEchoWrite = Mmu.writeByte addressEchoRam 0xBBuy memoryAfterWramWrite
let readEchoRam = Mmu.readByte addressEchoRam memoryAfterEchoWrite
let readWramViaEcho = Mmu.readByte addressWram memoryAfterEchoWrite // Read WRAM directly
printfn "Wrote 0xBB to Echo RAM at 0x%04X, Read: 0x%02X (Expected: 0xBB)" addressEchoRam readEchoRam
printfn "WRAM at 0x%04X now contains: 0x%02X (Expected: 0xBB)" addressWram readWramViaEcho
// Expected: 0xBB for both
// Verify ROM write attempt (should be ignored)
let memoryAfterRomWriteAttempt = Mmu.writeByte 0x0000u 0xFFuy memoryAfterEchoWrite
let romByte0AfterWrite = Mmu.readByte 0x0000u memoryAfterRomWriteAttempt
printfn "Attempted to write 0xFF to ROM at 0x0000. Read back: 0x%02X (Expected: 0xC3)" romByte0AfterWrite
// Expected: 0xC3 (original value)
// Verify IE register write/read
let memoryAfterIeWrite = Mmu.writeByte Mmu.MemoryMap.IeRegister 0x01uy memoryAfterRomWriteAttempt
let ieValue = Mmu.readByte Mmu.MemoryMap.IeRegister memoryAfterIeWrite
printfn "Wrote 0x01 to IE register, Read: 0x%02X (Expected: 0x01)" ieValue
// Expected: 0x01
// Instantiate the GameBoy state
let gameBoyState = {
cpu = Cpu.init ()
memory = memoryAfterIeWrite // Use the memory state after all tests
}
// You can now access gameBoyState.cpu and gameBoyState.memory
printfn "GameBoy system state initialized."
0 // return an integer exit code
Verification Steps:
- Save all files (
Cpu.fs,Mmu.fs,Program.fs). - Open your terminal in the
src/GameboyEmulator/directory. - Run
dotnet run.
Expected Output (console):
Starting Game Boy Emulator...
Memory initialized.
Dummy ROM loaded.
Read from ROM: 0xC3, 0x00, 0x01 (Expected: 0xC3, 0x00, 0x01)
Wrote 0xAA to WRAM at 0xC010, Read: 0xAA (Expected: 0xAA)
Wrote 0xBB to Echo RAM at 0xE010, Read: 0xBB (Expected: 0xBB)
WRAM at 0xC010 now contains: 0xBB (Expected: 0xBB)
Attempted to write 0xFF to ROM at 0x0000. Read back: 0xC3 (Expected: 0xC3)
Wrote 0x01 to IE register, Read: 0x01 (Expected: 0x01)
GameBoy system state initialized.
If your output matches, congratulations! Your basic MMU is working. The CPU can now fetch instructions and interact with memory.
Production Considerations
Performance
readByte and writeByte are arguably the most frequently called functions in an emulator’s core loop. Every CPU instruction fetch and data access goes through them.
- F# Pattern Matching: While expressive, using
matchstatements with many ranges can introduce a slight overhead compared to directif/elifchains or lookup tables. For now, this is perfectly acceptable and readable. If profiling shows this as a bottleneck later, we can optimize. - Array Access: F# arrays (
byte array) are efficient for raw byte storage. - Immutability vs. Mutability: We’ve used a hybrid approach. The
Memoryrecord itself is treated immutably (functions return new records), but the underlyingbyte arrayinstances are mutable. This is a pragmatic choice in F# when performance is critical for large, stateful data structures. Creating new byte arrays on every write would be prohibitively slow.
Maintainability
- Clear Memory Map: Defining constants in
Mmu.MemoryMapmakes the code readable and easy to update if specific address ranges need adjustment. - Modular Design: Keeping MMU logic in
Mmu.fsseparates concerns, making it easier to reason about and extend (e.g., adding MBCs).
Future Enhancements: Memory Bank Controllers (MBCs)
- Larger ROMs: Many Game Boy games are larger than 32KB. They use MBCs, which are specialized chips on the cartridge that swap different “banks” of ROM (and sometimes external RAM) into the
0x0000-0x7FFFand0xA000-0xBFFFregions. Our currentromarray only holds 32KB. Implementing MBCs will be a significant future step, requiringwriteByteto specific addresses in the ROM region to trigger bank switching. - Boot ROM: The Game Boy actually has a small 256-byte internal boot ROM that runs on startup. This ROM is mapped to
0x0000-0x00FFinitially, then unmapped after execution. We’ll address this when we set up the system’s boot process.
Common Issues & Solutions
Off-by-one Errors in Address Ranges:
- Issue: Incorrectly calculating array offsets (e.g.,
a - MemoryMap.RomStart - 1u). This can lead toIndexOutOfRangeException. - Solution: Double-check your subtraction logic. Remember that
RomStartis the base, soa - RomStartgives the 0-indexed offset. The+ 1uin size calculations ensures the end address is included. Use explicituint16types to prevent implicit integer conversions that might hide issues.
- Issue: Incorrectly calculating array offsets (e.g.,
Incorrect Handling of Read-Only Regions:
- Issue: Accidentally allowing writes to ROM or unusable areas, which can corrupt the game’s code or lead to unexpected behavior.
- Solution: Ensure your
writeBytefunction explicitly handles read-only regions by doing nothing or logging a warning. Our current implementation ignores writes to ROM, which is generally correct for the hardware’s behavior.
Performance Bottlenecks with
readByte/writeByte:- Issue: If your emulator is slow, and profiling points to these functions, the current
matchstatement might be too slow for extremely tight loops. - Solution: For now, optimize only if profiling indicates it’s a problem. Future optimizations might include:
- A single
byte arrayrepresenting the entire 64KB, with direct indexing (though this makes managing distinct regions harder). - Pre-calculating a lookup table for faster address-to-region mapping.
- Using
inlinefor these functions in F# to reduce call overhead (though the compiler often does this automatically).
- A single
- Issue: If your emulator is slow, and profiling points to these functions, the current
Summary & Next Step
You’ve successfully built the foundation for the Game Boy’s memory system!
- You’ve mapped out the Game Boy’s 64KB address space.
- You’ve implemented a
Memoryrecord in F# to hold different memory regions. - Crucially, you’ve created
readByteandwriteBytefunctions that correctly dispatch memory accesses to the appropriate regions. - Your emulator can now load a basic ROM and interact with its internal RAM.
This MMU is the glue that connects the CPU to the rest of the Game Boy’s hardware. With this in place, the CPU can now fetch instructions and data, bringing us closer to executing real Game Boy code.
The next step will be to integrate this Memory module with our Cpu module. We’ll modify the CPU to use Mmu.readByte to fetch opcodes and operands, and Mmu.writeByte to store results, finally enabling our CPU to execute a small program.
๐ง Check Your Understanding
- Why is an MMU necessary in a system like the Game Boy, rather than the CPU directly addressing components?
- Explain the difference between WRAM and Echo RAM in the Game Boy’s memory map. Why are they implemented this way?
- In our F#
Memoryrecord, why do we return a newMemoryrecord when updatingie, but return the samememoryrecord when modifyingmemory.wram.[...]?
โก Mini Task
Modify the writeByte function to print a console warning (printfn "Warning: Write to unhandled address 0x%04X" addr) if an address falls into the _ -> (catch-all) case. This can be useful for debugging later.
๐ Scenario
You are debugging a Game Boy ROM that occasionally crashes with an IndexOutOfRangeException when trying to access memory. You suspect it’s related to an incorrect address calculation in your readByte function. The crash occurs when the CPU tries to read from 0x8000.
- Which memory region is
0x8000supposed to be in? - What specific line of code in
readByteshould you investigate for potential off-by-one errors or incorrect range checks for that region? - How would you add a temporary logging statement to pinpoint the exact value of
(a - MemoryMap.VramStart)right before the array access?
๐ TL;DR
- The Game Boy MMU maps the CPU’s 64KB address space to various hardware components.
- Key memory regions include ROM, VRAM, WRAM, OAM, I/O, and HRAM.
readByteandwriteBytefunctions are critical for all CPU memory interactions.
๐ง Core Flow
- Define memory map constants for address ranges.
- Create an F#
Memoryrecord holding byte arrays for each region. - Implement
initto set up initial memory state. - Implement
loadRomto copy cartridge data into the ROM region. - Implement
readByteandwriteByteusing pattern matching over address ranges to dispatch to correct memory regions.
๐ Key Takeaway
A well-designed MMU is the foundational abstraction layer for any emulator. It manages the complex interplay between the CPU and diverse memory-mapped hardware, providing a consistent interface for the CPU while abstracting away the underlying physical layout.
References
- Pan Docs - The Ultimate Game Boy Technical Manual
- F# Language Reference - Records
- F# Language Reference - Pattern Matching
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.