Video summary

Gameboy Emulator Development - Part 02

Main summary

Key takeaways

Technology

Goal for this part

Move from cartridge loading/parsing to CPU emulation scaffolding—specifically setting up:

  • The memory bus
  • ROM access
  • The instruction fetch pipeline (instruction decode + operand fetch)

Execution is still mostly a placeholder for now.


Bus + memory access setup

  • Introduces the system bus as the only path for reading/writing memory-mapped data.
  • Adds a development placeholder:
    • If a memory region/peripheral isn’t implemented yet, the emulator prints "not yet implemented" to stderr and exits with a negative error code.
  • Current bus behavior (ROM-only for now):
    • If address < 0x8000 → read from cartridge ROM (cart read)
    • Writes still route through cartridge (cart write) because some cartridges may react to writes.

Bus rule of thumb: bus mediates access; cartridge decides what ROM write/react behavior is possible.


Cartridge interface changes (ROM-only for now)

  • Adds cartridge methods:
    • cart read
    • cart write
  • Assumes ROM-only (no bank switching yet):
    • Reads directly from an in-memory ROM byte array.
    • Writes are not treated as normal RAM writes at this stage, but still exist because cartridge hardware can react to writes.

CPU state / registers representation

  • Creates CPU register structures:
    • 8-bit registers: AF, BC, DE, HL broken down into component registers (A/F, B/C, D/E, H/L via fields like A, F, B, C, etc.)
    • 16-bit registers:
      • SP (stack pointer)
      • PC (program counter)
  • Adds a CPU context struct to store emulator state, including:
    • current fetch data
    • memory destination info and whether writing to memory
    • current opcode
    • CPU flags such as halted and stepping mode
    • an emulated cycles counter placeholder for later CPU/PPU/timer synchronization

Instruction system modeling (types + addressing modes)

  • Defines an instruction abstraction with fields such as:
    • type (e.g., no-op, load, increment, decrement, jump, etc.)
    • mode (addressing mode) describing how operands are fetched
    • reg1, reg2 (optional register operands)
    • condition (optional for conditional ops like JP NZ, CALL NC, etc.)
    • additional parameters for special cases (not fully implemented yet)
  • Conceptual walk-through of example opcodes and their effects on:
    • program counter movement (instruction length)
    • register changes
    • flag behavior (e.g., Z/H/C for XOR, LD, DEC-related ops)

Example instruction encoding behavior

  • 0x00 NOP
    • implied addressing, does nothing
  • 0xC3 JP a16
    • uses a 16-bit absolute address: opcode + two operand bytes
  • XOR A (example via AF)
    • affects flags: sets Z, clears others
  • LD C, d8 (opcode 0x0E d8)
    • loads immediate byte into C
  • DEC B (example)
    • can set Z/H
    • leaves carry unchanged (demonstrates overflow/roll behavior nuances)

Instruction table + decoder

  • Creates an instruction definition table (array sized around 0x100) mapping opcodes to instruction metadata.
  • Initially fills only a few entries (e.g., NOP, JP, XOR-ish, LD C,d8, DEC B).
  • Adds a decoder function to retrieve instruction metadata by opcode:
    • If the table entry is unknown (e.g., type == none), it treats it as an error.

CPU step flow (fetch + operand fetch, execution stub)

Conceptual CPU step loop:

  1. Fetch instruction opcode from the bus at PC, then increment PC
  2. Fetch operand data based on the addressing mode
  3. Execute (currently mostly placeholder / “not executing yet”)

Operand fetching logic (implemented subset)

  • implied
    • no operand fetch
  • register
    • reads operand from a register (planned/partially stubbed)
  • immediate 8-bit (d8)
    • maps to a register
  • immediate 16-bit (d16)
    • reads low byte then high byte from PC
    • assembles into a 16-bit value using shifts
    • increments PC accordingly

CPU utility for register access (byte/word mixing)

  • Adds cputil.c with helper functions:
    • read registers based on a register type enum (8-bit vs 16-bit)
    • a “reverse” function described as combining/splitting bytes to reconstruct larger values from high/low parts
  • Supports returning correct values for pairs like AF/BC/DE/HL based on individual bytes.

Debugging / early correctness checks

  • Early runs show unknown instruction errors because not all opcodes are implemented in the instruction table (e.g., opcodes like 0x3C, 0xCE appear).
  • Adds debug logging inside execution (printing opcode and PC) to confirm the CPU is fetching correctly.
  • Fix: sets ctx.regs.pc = 0x100 at CPU init to match the Game Boy ROM entry point used in the example program.
  • After adjustments:
    • PC increments properly
    • the emulator reads expected opcodes (e.g., reads NOP then 0xC3 for JP)
  • Undefined opcodes are expected to fail at this stage—the emphasis is on getting fetch/decode working first.

Main speakers / sources

  • Creator / speaker: Low-level developer on the “low level dev” YouTube channel
    • Gameboy Emulator Development - Part 02

Original video