Introduction

Welcome to the foundational stage of our Game Boy emulator! In this chapter, we’re going to construct the very brain of our system: the Central Processing Unit (CPU). The Game Boy uses a custom 8-bit CPU, often referred to as the “SM83,” which is a hybrid between a Zilog Z80 and Intel 8080. Understanding and accurately emulating its behavior is paramount to running any Game Boy software.

This milestone is critical because every single action within a Game Boy gameβ€”from moving a character to calculating damageβ€”is ultimately a sequence of CPU instructions. Building the CPU core correctly is non-negotiable for a functional emulator. By the end of this chapter, you will have a functional, albeit minimal, CPU core capable of storing its state (registers and flags) and executing a few fundamental instructions. This forms the bedrock upon which we’ll build memory access, graphics, and more complex logic.

Project Overview: Building a Game Boy Emulator

The overarching goal of this project is to build a functional Game Boy emulator from first principles using F#. This involves recreating the behavior of the original hardware components: the CPU, Memory Management Unit (MMU), Picture Processing Unit (PPU), Audio Processing Unit (APU), and input system. We’ll start with the CPU, as it dictates the flow of execution for all other components.

Tech Stack Deep Dive

For this project, we’re leveraging the following technologies:

  • F# (Version 8.0): Our primary programming language. F# is a functional-first language on the .NET platform, offering strong typing, immutability by default, and powerful abstractions that are well-suited for modeling complex hardware components and their interactions.
  • .NET SDK (Version 8.0): The runtime and development tools for F#. As of 2026-05-05, .NET 8.0 is the latest stable Long-Term Support (LTS) release, providing excellent performance and cross-platform capabilities.
  • SDL Bindings (e.g., Silk.NET.SDL, Veldrid): While not directly used in this chapter, a cross-platform graphics library will be essential for rendering the Game Boy’s display later. We will explore modern .NET bindings for SDL (like Silk.NET.SDL) or other low-level graphics libraries when we reach the PPU implementation.

Milestones and Build Plan

This chapter focuses on getting the CPU’s fundamental state management and instruction execution working. Here’s our plan:

  1. Model CPU Registers and Flags: Define F# records to represent the CPU’s internal state.
  2. Implement Flag Management Helpers: Create functions to precisely set, clear, and check individual flags within the F register. This is critical for accurate arithmetic and logic operations.
  3. Simulate Memory Access: Introduce a basic IMemory interface and a MockMemory implementation. This allows us to test CPU instructions without needing a full Memory Management Unit yet.
  4. Implement Basic Instructions: Code the fetch-decode-execute cycle for a handful of simple, representative Game Boy opcodes.
  5. Verify Execution: Run a small test program to observe register and flag changes, confirming our CPU’s initial correctness.

By the end of this chapter, you’ll have a working CPU core that can interpret and execute basic instructions, laying the groundwork for more complex Game Boy emulation.

Planning & Design: Modeling the CPU State

Before we write any code, let’s outline how we’ll represent the CPU. The SM83 CPU has a set of registers, which are small, fast storage locations used for calculations and addressing memory. It also has a set of flags that indicate the results of operations (e.g., if a result was zero, or if an overflow occurred).

CPU Registers

The Game Boy CPU has the following primary registers:

  • General-Purpose 8-bit Registers: A, B, C, D, E, H, L. These can also be paired for 16-bit operations: BC, DE, HL.
  • Accumulator (A): The primary register for arithmetic and logic operations.
  • Flags Register (F): An 8-bit register where individual bits represent specific flags.
  • Stack Pointer (SP): A 16-bit register pointing to the current top of the stack in memory.
  • Program Counter (PC): A 16-bit register pointing to the memory address of the next instruction to be executed.

CPU Flags (within the F register)

The F register is special. Only the upper four bits are used, representing four important flags. The lower four bits (0-3) are always zero.

  • Z (Zero Flag - Bit 7): Set if the result of an operation is zero.
  • N (Subtract Flag - Bit 6): Set if the last instruction was a subtraction.
  • H (Half Carry Flag - Bit 5): Set if there was a carry from bit 3 to bit 4 (useful for BCD arithmetic).
  • C (Carry Flag - Bit 4): Set if there was a carry from bit 7 or a borrow.

F# Representation

In F#, we’ll model the CPU’s state using immutable records. This aligns perfectly with functional programming principles, ensuring that state changes are explicit and predictable.

For registers, we’ll create a Registers record. For the overall CPU state, we’ll have a CpuState record that includes Registers and a placeholder for memory.

CPU Core Data Flow

The basic operation of our CPU will follow a classic fetch-decode-execute cycle:

flowchart TD Start --> Fetch[Fetch Opcode] Fetch --> IncrementPC[Increment PC] IncrementPC --> Decode[Decode Opcode] Decode --> Execute[Execute Instruction] Execute --> UpdateState[Update CPU State] UpdateState --> Fetch

The CPU constantly repeats this cycle, processing one instruction at a time. This continuous loop is the heart of any emulator.

File Structure

We’ll start with a single Cpu.fs file to encapsulate all CPU-related logic for now. As the project grows, we might split it further (e.g., Registers.fs, Opcodes.fs).

GameBoyEmulator/
β”œβ”€β”€ GameBoyEmulator.fsproj
β”œβ”€β”€ Program.fs
└── Cpu.fs

Step-by-Step Implementation

Let’s start by defining our CPU’s core data structures.

1. Define Registers and CPU State

Open your GameBoyEmulator.fsproj file and ensure it targets .NET 8.0.

<!-- GameBoyEmulator.fsproj -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <WarnOn>FS3391;FS3392</WarnOn>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Cpu.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

Explanation: This standard .fsproj setup targets .NET 8.0, which is the latest stable long-term support (LTS) release as of 2026-05-05. The <WarnOn> element helps catch common F# code quality issues, promoting better F# coding practices.

Next, create a new file named Cpu.fs in your project root.

// Cpu.fs

module GameBoyEmulator.Cpu

/// Represents the 8-bit general-purpose registers and the 16-bit program counter (PC)
/// and stack pointer (SP) of the Game Boy's SM83 CPU.
/// The 'F' register holds the CPU flags.
type Registers = {
    A : byte
    F : byte // Flags register (Z N H C 0 0 0 0)
    B : byte
    C : byte
    D : byte
    E : byte
    H : byte
    L : byte
    SP : uint16 // Stack Pointer
    PC : uint16 // Program Counter
}

/// Represents the full state of the Game Boy CPU.
/// This will be expanded to include memory and other components later.
type CpuState = {
    Registers : Registers
    // Cycles : int64 // We'll add cycle counting later
    // Memory : byte array // Placeholder for memory, will be replaced by MMU
}

/// Initializes the CPU registers to their power-up state.
/// This is based on common Game Boy boot ROM behavior.
let createInitialRegisters () : Registers =
    {
        A = 0x01uy // Often 0x01 for DMG, or 0x11 for CGB
        F = 0xA0uy // ZNHC flags: Z=1, N=0, H=1, C=0 (0b10100000). Lower 4 bits are always 0.
        B = 0x00uy
        C = 0x13uy
        D = 0x00uy
        E = 0xD8uy
        H = 0x01uy
        L = 0x4Duy
        SP = 0xFFFEus // Stack pointer typically starts high
        PC = 0x0100us // PC usually starts after the boot ROM
    }

/// Initializes the CPU state.
let createInitialCpuState () : CpuState =
    {
        Registers = createInitialRegisters ()
    }

Explanation:

  • module GameBoyEmulator.Cpu: Defines a module to organize our CPU-related code. Modules help prevent name collisions and improve code organization within larger projects.
  • type Registers = { ... }: This F# record defines the structure for our CPU registers. Each field is immutable by default, which is a core F# principle. byte is used for 8-bit registers, and uint16 for 16-bit ones. Using uint16 for PC and SP is important as Game Boy addresses are 16-bit.
  • type CpuState = { ... }: This record will hold the entire CPU state. For now, it just contains Registers. We’ve added comments for Cycles and Memory as a reminder of future additions, demonstrating forward-thinking design.
  • let createInitialRegisters (): This function provides the initial power-up values for the Game Boy’s CPU registers. These are specific values that the hardware starts with, crucial for compatibility with the boot ROM. We use uy and us suffixes to denote unsigned byte and unsigned short literals, respectively. The F register is set to 0xA0uy (10100000 in binary), which means the Zero (Z) flag is set, Half-Carry (H) is set, and Subtract (N) and Carry (C) flags are cleared. The lower 4 bits are always 0.
  • let createInitialCpuState (): A simple function to create an initial CpuState by calling createInitialRegisters.

πŸ“Œ Key Idea: Using immutable records for Registers and CpuState means that every operation that changes the CPU’s state will return a new CpuState instance, rather than modifying an existing one. This makes state transitions explicit and easier to reason about.

2. Flag Management

Manipulating individual bits within the F register requires bitwise operations. Let’s define helper functions for setting, clearing, and checking flags. By encapsulating these operations, we make our code cleaner and less error-prone.

Add the following functions to Cpu.fs after createInitialCpuState.

// Cpu.fs (continued)

// Flag masks for the F register
let FlagZ = 0x80uy // Zero Flag (Bit 7)
let FlagN = 0x40uy // Subtract Flag (Bit 6)
let FlagH = 0x20uy // Half Carry Flag (Bit 5)
let FlagC = 0x10uy // Carry Flag (Bit 4)

/// Sets a specific flag in the F register.
let setFlag flag (registers: Registers) : Registers =
    { registers with F = registers.F ||| flag }

/// Clears a specific flag in the F register.
let clearFlag flag (registers: Registers) : Registers =
    { registers with F = registers.F &&& (~~~flag) }

/// Checks if a specific flag is set in the F register.
let isFlagSet flag (registers: Registers) : bool =
    (registers.F &&& flag) = flag

/// Updates the Z flag based on a value.
let updateZFlag value (registers: Registers) : Registers =
    if value = 0x00uy then setFlag FlagZ registers
    else clearFlag FlagZ registers

/// Updates the N flag based on whether the last operation was a subtraction.
let updateNFlag isSubtraction (registers: Registers) : Registers =
    if isSubtraction then setFlag FlagN registers
    else clearFlag FlagN registers

/// Updates the H flag based on an 8-bit addition or subtraction.
/// For addition, checks if bit 3 carried to bit 4.
/// For subtraction, checks if bit 3 borrowed from bit 4.
let updateHFlag8bit isSubtraction val1 val2 (registers: Registers) : Registers =
    let halfCarry =
        if isSubtraction then
            // Half borrow check: if lower nibble of val1 is less than lower nibble of val2
            ((val1 &&& 0x0Fu) < (val2 &&& 0x0Fu))
        else
            // Half carry check: if sum of lower nibbles overflows into bit 4
            (((val1 &&& 0x0Fu) + (val2 &&& 0x0Fu)) &&& 0x10u) = 0x10u
    if halfCarry then setFlag FlagH registers
    else clearFlag FlagH registers

/// Updates the C flag based on an 8-bit addition or subtraction.
/// For addition, checks if bit 7 carried out.
/// For subtraction, checks if there was a borrow.
let updateCFlag8bit isSubtraction val1 val2 (registers: Registers) : Registers =
    let carry =
        if isSubtraction then
            // Borrow check: if val1 is less than val2
            (val1 < val2)
        else
            // Carry check: if adding val2 to val1 results in overflow
            (uint16 val1 + uint16 val2) > 0xFFu
    if carry then setFlag FlagC registers
    else clearFlag FlagC registers

Explanation:

  • Flag Masks: We define FlagZ, FlagN, FlagH, FlagC as byte constants. These are bitmasks to isolate or manipulate the specific flag bits within the F register.
  • setFlag / clearFlag: These functions use bitwise OR (|||) to set a bit and bitwise AND (&&&) with the bitwise NOT (~~~) of the flag to clear a bit. They return a new Registers record with the updated F field, preserving immutability.
  • isFlagSet: Checks if a flag is currently set.
  • updateZFlag, updateNFlag, updateHFlag8bit, updateCFlag8bit: These are crucial for correctly updating flags after arithmetic operations. The logic for half-carry and carry flags in 8-bit operations is specific to the SM83 and needs careful implementation based on documentation.
    • isSubtraction parameter is vital as flag logic often differs between addition and subtraction.
    • updateHFlag8bit checks for carry/borrow between the lower and upper nibble (bits 3 and 4).
    • updateCFlag8bit checks for overflow/borrow for the full 8-bit value.

🧠 Important: The exact logic for flag updates can be tricky and is a common source of bugs in emulators. Always consult detailed SM83 documentation (like Pan Docs) for precise behavior. The Game Boy CPU’s flag behavior is not always intuitive and differs from other CPUs like the Z80 in subtle ways. Incorrect flag emulation can lead to subtle bugs in game logic that are extremely hard to debug later.

⚑ Real-world insight: The ||| (bitwise OR), &&& (bitwise AND), and ~~~ (bitwise NOT) operators are fundamental for low-level system programming. Understanding how to manipulate individual bits efficiently is a core skill for emulator development.

3. Basic Instruction Execution

Now, let’s implement the execute function for a few simple instructions. For this chapter, we’ll hardcode a few opcodes directly. Later, we’ll read them from memory via a proper Memory Management Unit (MMU).

First, we need a way to simulate reading from memory for our CPU. For now, we’ll use a very basic mock.

Add the following type and function to Cpu.fs (after the flag functions).

// Cpu.fs (continued)

/// A simple type to represent our memory interface.
/// In Chapter 3, this will be replaced by a full Memory Management Unit (MMU).
type IMemory =
    abstract member ReadByte : uint16 -> byte
    abstract member WriteByte : uint16 -> byte -> unit

/// A mock memory implementation for initial CPU testing.
/// It's just a byte array for now, representing 64KB of addressable space.
type MockMemory (size : int) =
    let data = Array.zeroCreate<byte> size

    interface IMemory with
        member _.ReadByte addr =
            if addr < uint16 data.Length then
                data[int addr]
            else
                0xFFuy // Default for unmapped memory reads on Game Boy is 0xFF

        member _.WriteByte addr value =
            if addr < uint16 data.Length then
                data[int addr] <- value
            // else ignore write to unmapped memory for now, a real MMU would handle this

/// The main CPU execution function.
/// It takes the current CPU state and a memory interface,
/// fetches an opcode, decodes it, executes it, and returns the new state.
let executeCycle (cpuState: CpuState) (memory: IMemory) : CpuState =
    let currentRegisters = cpuState.Registers
    let pc = currentRegisters.PC
    let opcode = memory.ReadByte pc // Fetch the opcode at the current Program Counter

    // Increment PC for the next instruction. Most instructions are 1 byte.
    // Multi-byte instructions will increment PC further during their execution.
    let registersAfterFetch = { currentRegisters with PC = pc + 1us }

    // Decode and Execute
    match opcode with
    | 0x00uy -> // NOP: No Operation
        // Pan Docs: "No operation. Does nothing."
        // Cycles: 4
        // Flags: None affected
        { cpuState with Registers = registersAfterFetch }

    | 0x06uy -> // LD B, n: Load 8-bit immediate into B
        // Pan Docs: "Load 8-bit immediate value n into register B."
        // Cycles: 8
        // Flags: None affected
        let value = memory.ReadByte (registersAfterFetch.PC) // Read the immediate value
        let newRegisters = { registersAfterFetch with B = value; PC = registersAfterFetch.PC + 1us } // Increment PC past the immediate
        { cpuState with Registers = newRegisters }

    | 0x0Euy -> // LD C, n: Load 8-bit immediate into C
        // Pan Docs: "Load 8-bit immediate value n into register C."
        // Cycles: 8
        // Flags: None affected
        let value = memory.ReadByte (registersAfterFetch.PC)
        let newRegisters = { registersAfterFetch with C = value; PC = registersAfterFetch.PC + 1us }
        { cpuState with Registers = newRegisters }

    | 0x04uy -> // INC B: Increment register B
        // Pan Docs: "Increment register B. Flags: Z 0 H -"
        // Cycles: 4
        let oldValue = registersAfterFetch.B
        let newValue = oldValue + 1uy
        let newRegisters =
            registersAfterFetch
            |> (updateZFlag newValue) // Update Z flag based on newValue
            |> (updateNFlag false)    // N flag is always cleared for INC
            |> (updateHFlag8bit false oldValue 1uy) // Check half carry for addition (increment by 1)
            |> (fun r -> { r with B = newValue }) // Update B register
        { cpuState with Registers = newRegisters }

    | 0x05uy -> // DEC B: Decrement register B
        // Pan Docs: "Decrement register B. Flags: Z 1 H -"
        // Cycles: 4
        let oldValue = registersAfterFetch.B
        let newValue = oldValue - 1uy
        let newRegisters =
            registersAfterFetch
            |> (updateZFlag newValue) // Update Z flag based on newValue
            |> (updateNFlag true)     // N flag is always set for DEC
            |> (updateHFlag8bit true oldValue 1uy) // Check half carry for subtraction (decrement by 1)
            |> (fun r -> { r with B = newValue }) // Update B register
        { cpuState with Registers = newRegisters }

    // Default case for unimplemented opcodes
    | _ ->
        // For now, we'll just throw an exception for unknown opcodes.
        // Later, we'll implement a full opcode map or a more robust error handling mechanism.
        failwithf "Unimplemented opcode: 0x%02X at PC: 0x%04X" opcode pc

Explanation:

  • IMemory and MockMemory: We introduce a simple IMemory interface and a MockMemory implementation. This allows us to pass a memory abstraction to our CPU, decoupling it from the actual memory management unit (which we’ll build in the next chapter). For now, MockMemory is just a byte array representing the Game Boy’s 64KB address space. ReadByte returns 0xFF for out-of-bounds reads, which is common Game Boy behavior. This is a classic example of the Dependency Inversion Principle, making our code more modular and testable.
  • executeCycle function: This is the core of our CPU.
    • It takes cpuState and memory as input and returns a new CpuState. This functional approach ensures state changes are explicit.
    • Fetch: memory.ReadByte pc fetches the opcode at the current PC.
    • Increment PC: registersAfterFetch updates the PC to point to the next byte. Most instructions are 1 byte, but LD r, n (load immediate) is 2 bytes, so PC needs an additional increment within the match arm.
    • Decode & Execute: The match opcode with statement handles different opcodes.
      • NOP (0x00): Does nothing except advance PC.
      • LD B, n (0x06) / LD C, n (0x0E): These are 2-byte instructions. The first byte is the opcode, the second is the immediate value n. We read n from PC + 1 and then increment PC again to move past n.
      • INC B (0x04): Increments register B. Crucially, it updates the Z, N, and H flags. The C flag is not affected by 8-bit INC/DEC operations, which is an important detail for the SM83. We use the pipe-forward operator |> to chain flag updates, making the sequence of operations clear and readable.
      • DEC B (0x05): Decrements register B. Similar to INC, it updates Z, N, and H flags, but N is set for decrement.
    • Return New State: Each branch of the match returns a new CpuState record, reflecting the changes made by the instruction.
  • failwithf: For any unimplemented opcode, we’ll throw an exception. This helps us identify missing instructions during development.

⚑ Quick Note: The cycle counts (e.g., Cycles: 4) mentioned in the comments are for the Game Boy’s internal clock (M-cycles). We’ll use these later for proper timing and synchronization with the PPU and APU. For now, they are informational but critical for future performance modeling.

⚠️ What can go wrong: Forgetting to increment PC correctly for multi-byte instructions is a very common bug. If PC isn’t advanced past the immediate value in LD r, n, the CPU will try to interpret the data byte as an opcode, leading to immediate crashes or incorrect execution.

4. Integrate into Program.fs

Let’s modify our Program.fs to create a CPU and execute a few cycles using our MockMemory.

// Program.fs

open System
open GameBoyEmulator.Cpu

[<EntryPoint>]
let main argv =
    printfn "Starting Game Boy Emulator CPU Test..."

    // Create a mock memory of 64KB (typical Game Boy memory space)
    let memory = MockMemory(0x10000) :> IMemory

    // Load a simple test program into mock memory
    // This sequence means:
    // 0x0100: NOP
    // 0x0101: LD B, 0x0A (Load 0x0A into B)
    // 0x0103: INC B (B becomes 0x0B)
    // 0x0104: DEC B (B becomes 0x0A)
    // 0x0105: NOP
    let program = [| 0x00uy; 0x06uy; 0x0Auy; 0x04uy; 0x05uy; 0x00uy |]
    for i = 0 to program.Length - 1 do
        (memory :?> MockMemory).data[0x0100 + i] <- program[i] // Directly access internal array for mock memory

    // Initialize CPU state
    let mutable cpuState = createInitialCpuState ()
    printfn "Initial PC: 0x%04X, B: 0x%02X, F: 0x%02X" cpuState.Registers.PC cpuState.Registers.B cpuState.Registers.F

    // Execute a few cycles
    for i = 1 to 5 do // Execute 5 instructions
        printfn "\n--- Cycle %d ---" i
        let oldPc = cpuState.Registers.PC
        let opcode = memory.ReadByte oldPc
        printfn "Executing opcode 0x%02X at PC 0x%04X" opcode oldPc

        cpuState <- executeCycle cpuState memory
        printfn "New PC: 0x%04X, B: 0x%02X, F: 0x%02X" cpuState.Registers.PC cpuState.Registers.B cpuState.Registers.F

        // Check flags after relevant operations
        if opcode = 0x04uy || opcode = 0x05uy then
            printfn "  Flags: Z=%b N=%b H=%b C=%b"
                (isFlagSet FlagZ cpuState.Registers)
                (isFlagSet FlagN cpuState.Registers)
                (isFlagSet FlagH cpuState.Registers)
                (isFlagSet FlagC cpuState.Registers)

    0 // Return an exit code

Explanation:

  • open GameBoyEmulator.Cpu: Imports our CPU module, making its functions and types available.
  • MockMemory: We instantiate our MockMemory with 64KB, which is the full 16-bit addressable range of the Game Boy.
  • program: A simple array of bytes representing a short sequence of Game Boy opcodes. We manually load this into our MockMemory starting at address 0x0100, which is where the CPU’s PC typically starts after the boot ROM. This simulates a very basic game program.
  • mutable cpuState: We declare cpuState as mutable because our executeCycle function returns a new state, and we need to reassign it in the loop. This is a common pattern when combining functional updates with iterative processes in F#.
  • Execution Loop: We loop a few times, calling executeCycle and printing the CPU state (PC, B, F registers, and flags) after each instruction. This allows us to observe the CPU’s behavior and verify its correctness.

Testing & Verification

Now, let’s run our program and verify the CPU’s behavior.

  1. Build and Run: Open your terminal in the GameBoyEmulator directory and run:

    dotnet run
    
  2. Expected Output and Verification: You should see output similar to this:

    Starting Game Boy Emulator CPU Test...
    Initial PC: 0x0100, B: 0x00, F: 0xA0
    
    --- Cycle 1 ---
    Executing opcode 0x00 at PC 0x0100
    New PC: 0x0101, B: 0x00, F: 0xA0
    
    --- Cycle 2 ---
    Executing opcode 0x06 at PC 0x0101
    New PC: 0x0103, B: 0x0A, F: 0xA0
    
    --- Cycle 3 ---
    Executing opcode 0x04 at PC 0x0103
    New PC: 0x0104, B: 0x0B, F: 0x00
      Flags: Z=False N=False H=False C=False
    
    --- Cycle 4 ---
    Executing opcode 0x05 at PC 0x0104
    New PC: 0x0105, B: 0x0A, F: 0x40
      Flags: Z=False N=True H=False C=False
    
    --- Cycle 5 ---
    Executing opcode 0x00 at PC 0x0105
    New PC: 0x0106, B: 0x0A, F: 0x40
    

    Key points to verify:

    • PC Increment: Does PC correctly increment after each instruction (by 1 for NOP, by 2 for LD r, n)?
    • LD B, n: Does B register correctly load 0x0A?
    • INC B (0x04): B increments from 0x0A to 0x0B.
      • Z flag: 0x0B is not zero, so Z is cleared (from initial 1).
      • N flag: INC clears N.
      • H flag: 0x0A + 1 (0b1010 + 0b0001) does not cause a half-carry (no carry from bit 3 to bit 4), so H is cleared (from initial 1).
      • C flag: INC does not affect C, so it remains cleared (from initial 0).
      • Resulting F should be 0b00000000 = 0x00.
    • DEC B (0x05): B decrements from 0x0B to 0x0A.
      • Z flag: 0x0A is not zero, so Z is cleared.
      • N flag: DEC sets N.
      • H flag: 0x0B - 1 (0b1011 - 0b0001) does not cause a half-borrow (no borrow from bit 4 to bit 3), so H is cleared.
      • C flag: DEC does not affect C, so it remains cleared.
      • Resulting F should be 0b01000000 = 0x40.

    This verification step is crucial. If your output differs, carefully re-check your executeCycle implementation and flag logic against the Game Boy’s CPU documentation.

Operations & Performance Considerations

  • Performance: The executeCycle function will be called millions of times per second (Game Boy CPU runs at ~4.19 MHz). While F# records are immutable, creating new records on every state change can introduce overhead. For now, this is acceptable for clarity and correctness. Later, in performance-critical sections (like the main emulation loop), we might explore using mutable fields or ref cells judiciously, or optimizing specific instruction paths using techniques like inline or highly optimized loops. The goal is accurate emulation first, then optimization.
  • Maintainability: By encapsulating CPU logic in Cpu.fs and using a clear Registers record, we ensure a modular and understandable codebase. The IMemory interface is a great example of dependency inversion, making our CPU testable without a full MMU. This separation of concerns is a key principle in building complex systems.
  • Testing: Unit tests are absolutely crucial for CPU emulation. Every opcode, every flag update needs to be verified. The small program array we used is a manual form of an integration test. Later, we’ll use dedicated test ROMs like Blargg’s CPU instruction tests, which are specifically designed to validate CPU behavior against known correct hardware behavior.

πŸ”₯ Optimization / Pro tip: For very performance-sensitive parts of an emulator, you might consider using mutable arrays or ref cells for memory and registers directly, even in F#. The key is to isolate these mutable parts and manage them carefully, perhaps within a single Emulator record that wraps all mutable components. This balances functional purity with raw speed.

Common Issues & Solutions

  • Incorrect Flag Updates: This is by far the most common pitfall in emulator development. The H (Half Carry) flag logic, in particular, can be tricky as it depends on operations on the lower nibble and differs for addition and subtraction.
    • Solution: Create dedicated unit tests for flag calculations. Isolate flag logic into pure functions as we did. Consult authoritative documentation (like Pan Docs) rigorously.
  • PC Increment Errors: For multi-byte instructions, forgetting to increment PC by the correct amount will lead to reading incorrect opcodes or data, causing the emulator to quickly desynchronize.
    • Solution: Carefully read the instruction’s byte length from documentation. For each match branch, ensure PC is advanced correctly, accounting for the opcode itself and any immediate operands.
  • Misinterpreting Documentation: The SM83 is not a standard CPU, and its documentation is often reverse-engineered. Minor details can have significant impacts.
    • Solution: Cross-reference multiple sources (Pan Docs, other emulator implementations) and use test ROMs as the ultimate source of truth for behavior. Emulator development is an iterative process of reading docs, implementing, testing, and debugging.

🧠 Check Your Understanding

  • What is the purpose of the F register, and why are only its upper four bits significant?
  • Explain the concept of immutability in F# records and how it applies to our CpuState updates.
  • Why did we introduce an IMemory interface even though we’re using a simple MockMemory for now?

⚑ Mini Task

  • Implement the LD B, A (opcode 0x47) instruction in executeCycle. This instruction copies the value from register A to register B. It does not affect any flags and is a 1-byte instruction.

πŸš€ Scenario

You’ve implemented the ADD A, n (opcode 0xC6) instruction, which takes an 8-bit immediate n and adds it to register A. This instruction affects Z, N, H, and C flags. Describe how you would update executeCycle for this, specifically focusing on how you would use the updateZFlag, updateNFlag (which should be cleared), updateHFlag8bit, and updateCFlag8bit functions. Consider the number of bytes this instruction consumes.

πŸ“Œ TL;DR

  • The Game Boy’s SM83 CPU uses 8-bit registers (A, B, C, D, E, H, L) and 16-bit registers (SP, PC).
  • The F register stores four critical flags: Zero (Z), Subtract (N), Half Carry (H), and Carry (C). The lower 4 bits are unused.
  • We model CPU state using immutable F# records for clarity and functional purity, returning new state with each instruction.
  • CPU operation follows a fetch-decode-execute cycle, updating the CpuState with new register values.
  • Precise flag updates and correct PC increments are crucial for accurate emulation.

🧠 Core Flow

  1. Define Registers and CpuState records in Cpu.fs to model CPU state.
  2. Implement helper functions for managing individual CPU flags using bitwise operations.
  3. Create an IMemory interface and a MockMemory for basic, decoupled memory access.
  4. Implement executeCycle with a match statement for basic opcodes (NOP, LD r, n, INC r, DEC r), ensuring correct flag updates and PC advancement.
  5. Integrate and test the CPU in Program.fs by loading a small program into MockMemory and stepping through execution, observing register and flag changes.

πŸš€ Key Takeaway

Building an emulator starts with a meticulously crafted CPU core. Every instruction’s effect on registers, program counter, and especially flags, must be precisely emulated. F#’s strong typing and immutability help manage this complex state with confidence, but requires careful attention to detail and thorough verification against hardware documentation.


References

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