Skip to content

Repository files navigation

DMG-CPU B Game Boy Simulation

SystemVerilog code for simulating a Game Boy system with Icarus Verilog. Most of the code is generated from the netlist files in msinger/dmg-schematics.

Files in this repo

File(s) Description
./dmg_cpu_b/cells/*.sv Modules implementing all standard cells of the DMG-CPU B chip, including the RAM and ROM blocks.
./dmg_cpu_b/dmg_cpu_b.sv The DMG-CPU B chip.
./sm83/cells/*.sv Modules implementing all cells in the SM83 CPU core, including the huge decoders.
./sm83/sm83.sv The SM83 CPU core.
./dmg_cpu_b_gameboy.sv Top level module that simulates a complete Game Boy system with DMG-CPU B chip, WRAM, VRAM, LCD, audio and cartridge.
./snd_dump.sv Code for dumping the APU's sound output to a file.
./vid_dump.sv Code for dumping the PPU's video signals to a file.
./mkvid/mkimgs.c C code for extracting raw RGB frames from video signal dumps.
./mkvid/mkvid.sh Bash script for combining dumped sound output and extracted RGB frames to a video.
./boot/quickboot.s Assembly code for a boot ROM that boots in less than 0.2 seconds. Leaves the system in the same state as the original boot ROM.

Requirements

Of course, you need Icarus Verilog and GNU Make.

If you want to generate video files, you also need GCC, ImageMagick and FFmpeg.

The original boot ROM is not part of this repository. If you want to simulate the original boot ROM, you need to copy a boot ROM image into the root of this repository with the name DMG_ROM.bin. Or you can place it anywhere else and overwrite the make variable BOOTROM like BOOTROM=/path/to/bootrom.

Usage

The default make target (sim-gameboy) simulates a complete Game Boy system (dmg_cpu_b_gameboy.sv). Run

make sim-gameboy

or just

make

to start the simulation. This simulates a Game Boy without cartridge, executing the boot ROM in ./DMG_ROM.bin.

The simulation can produce any of the following files:

File(s) Description
./dmg_cpu_b_gameboy.snd The APU's sound output. All four channels mixed into one 16 bit PCM file with 65536 Hz stereo.
./dmg_cpu_b_gameboy_ch[1-4].snd One 8 bit PCM file with 65536 Hz mono for each channel. Only if CH_DUMP=y is set on make command line.
./dmg_cpu_b_gameboy.vid The PPU's video signal dump. Can be used to extract images for a video.
./dmg_cpu_b_gameboy.fst Dump of all signals in the system. Can be opened with GTKWave or any other wave viewer.
./dmg_cpu_b_gameboy.sav Cartridge SRAM gets dumped every 0.1 seconds.

To produce a playable video file from those dumps, run

make dmg_cpu_b_gameboy.mkv

Make variables for configuration

BOOTROM=<path-to-binary>:
Specifies the boot ROM binary that is loaded into the boot ROM memory on startup. The path defaults to DMG_ROM.bin. The file needs to have a size of 256 bytes. If it is shorter, the memory will have unknown ('x) values in simulation.

ROM=<path-to-binary>:
Specifies a Game Boy ROM file that will be used as a cartridge. By default, the system will be simulated without any cartridge inserted. As of now, the simulation supports no-MBC, MBC1 and MBC5 cartridges.

SECS=<number-of-seconds>:
Specifies how many seconds will be simulated until the simulation terminates. The default is 6.0 seconds. The simulation takes about 150 minutes of real time per simulated second on a Ryzen 5 3600 (with TIMING=default and SIMPLIFIED_OAM=y).

DUMP=fst (default), DUMP=vcd, DUMP=:
By default, all internal signals are dumped in FST format. To dump in VCD, add DUMP=vcd to the make command line. DUMP= disables dumping of internal signals. When signal dumps are disabled, the simulation runs faster. So when you want to generate a video, then this may be useful.

TIMING=default (default), TIMING=nodelay:
Selects which timing model to use. TIMING=default simulates the system with (hopefully) realistic signal propagation delays. Delays are calculated based on individual transistor sizes and wire lengths. TIMING=nodelay simulates the system with 0 delays instead. Without delays, the simulation may run faster, but the exact behavior of the real device depends on glitches that emerge from "bad timing". You need to run make clean when changing this variable, because delays in Verilog are compile time constants, meaning the code needs to be recompiled.

SIMPLIFIED_OAM=y (default), SIMPLIFIED_OAM=:
The default (SIMPLIFIED_OAM=y) greatly increases simulation speed. It simulates the SRAM blocks of the OAM with reduced complexity. With this simplification, the infamous OAM bug is not present though. With SIMPLIFIED_OAM=, the simplification is disabled and the simulation can predict which RAM rows will be corrupted by the OAM bug. You need to run make clean when changing this variable, because the code needs to be recompiled.

SIMPLIFIED_WAVERAM=y (default), SIMPLIFIED_WAVERAM=:
The default (SIMPLIFIED_WAVERAM=y) greatly increases simulation speed. It simulates the SRAM block of the Wave RAM with reduced complexity. With this simplification, Blargg's dmg_sound test No. 10 won't pass. With SIMPLIFIED_WAVERAM=, the simplification is disabled and the simulation can predict which RAM rows or bytes will be corrupted by the wave restart bug. You need to run make clean when changing this variable, because the code needs to be recompiled.

MBC_TYPE=<hex>:
By default, the MBC/cartridge type gets read from ROM address 0x147. This variable overrides that value.

RAM_SIZE=<hex>:
By default, the cartridge SRAM size gets read from ROM address 0x149. This variable overrides that value.

EVENT_FILE=<path-to-event-file>:
You can provide an optional text file as input to the simulation, containing events that should happen at specified clock ticks. See Event File section below for more details about the file format.

"Quickboot" boot ROM

It takes a lot of time to simulate the Game Boy, so if you want to minimize the time that the simulation spends with running the boot ROM, you can use quickboot.bin in the boot folder. This ROM takes less than 200 milliseconds of simulation time before it enters cartridge code. It recreates the same system state that the original DMG-CPU A/B/C boot ROM creates, but with one exception: The VRAM gets zeroed, it does not fill it with the Nintendo logo. The DIV register and the PPU get precisely synchronized with the boot ROM exit so that test ROMs that test for this will pass just fine. You can rebuild the binary from source using the SM83 Binutils.

To use this boot ROM, you can either make it the default by moving it to the root of the repository and rename it to DMG_ROM.bin, or by adding the variable BOOTROM=boot/quickboot.bin to the make command line.

Example usage

Simulate 120 seconds of Zelda with quickboot ROM:

make sim-gameboy BOOTROM=boot/quickboot.bin \
                 ROM=~/GB\ roms/Legend\ of\ Zelda\,\ The\ -\ Link\'s\ Awakening\ DX.gbc \
                 SECS=120.0 \
                 DUMP=

Wait a few days (seriously!), and the run

make dmg_cpu_b_gameboy.mkv

to generate a video file of the Zelda intro.

Tests

The tables below show the results of all applicable Blargg and Mooneye GB tests run with default timing (TIMING=default). In earlier commits these tables had an additional column for TIMING=nodelay. I removed this column, because nodelay simulation is unreliable due to logic loops in the circuit. Those loops can cause the simulator to hang, depending on the processing order of events. When Icarus hangs in such a loop, it keeps allocating more and more memory until the OOM killer ends it.

Results of Blargg's tests:

Test Result
cgb_sound n/a
cpu_instrs
dmg_sound/01-registers
dmg_sound/02-len ctr
dmg_sound/03-trigger
dmg_sound/04-sweep
dmg_sound/05-sweep details
dmg_sound/06-overflow on trigger
dmg_sound/07-len sweep period sync
dmg_sound/08-len ctr during power
dmg_sound/09-wave read while on
dmg_sound/10-wave trigger while on 🚫**
dmg_sound/11-regs after power
dmg_sound/12-wave write while on
halt_bug
instr_timing
interrupt_time n/a
mem_timing
oam_bug 🚫*

* oam_bug tests depend on small differences in signal delays which could change with temperature. According to SameBoy some edge cases indeed behave nondeterministic on real hardware, so this will most likely never behave "correctly" in a purely digital simulation.

** Re-triggering the wave channel produces a corruption inside the wave RAM that is very similar to the OAM bug. The simulation can't always predict how exactly the bits in the RAM change.

Note: The Blargg tests suffixed with -2 are not listed here, because they run exactly the same test code as the versions without the suffix, but with much slower boilerplate code stitched around them, so they just waste a lot of CPU time. (Running all the test ROMs through the simulation already takes over ten days! 🐌🐌🐌)

Results of Mooneye GB tests:

Test Result
acceptance/add_sp_e_timing
acceptance/bits/mem_oam
acceptance/bits/reg_f
acceptance/bits/unused_hwio-GS
acceptance/boot_div-dmg0 n/a
acceptance/boot_div-dmgABCmgb
acceptance/boot_div-S n/a
acceptance/boot_div2-S n/a
acceptance/boot_hwio-dmg0 n/a
acceptance/boot_hwio-dmgABCmgb
acceptance/boot_hwio-S n/a
acceptance/boot_regs-dmg0 n/a
acceptance/boot_regs-dmgABC
acceptance/boot_regs-mgb n/a
acceptance/boot_regs-sgb n/a
acceptance/boot_regs-sgb2 n/a
acceptance/call_cc_timing
acceptance/call_cc_timing2
acceptance/call_timing
acceptance/call_timing2
acceptance/di_timing-GS
acceptance/div_timing
acceptance/ei_sequence
acceptance/ei_timing
acceptance/halt_ime0_ei
acceptance/halt_ime0_nointr_timing
acceptance/halt_ime1_timing
acceptance/halt_ime1_timing2-GS
acceptance/if_ie_registers
acceptance/instr/daa
acceptance/interrupts/ie_push
acceptance/intr_timing
acceptance/jp_cc_timing
acceptance/jp_timing
acceptance/ld_hl_sp_e_timing
acceptance/oam_dma/basic
acceptance/oam_dma/reg_read
acceptance/oam_dma/sources-dmgABCmgbS
acceptance/oam_dma_restart
acceptance/oam_dma_start
acceptance/oam_dma_timing
acceptance/pop_timing
acceptance/ppu/hblank_ly_scx_timing-GS
acceptance/ppu/intr_1_2_timing-GS
acceptance/ppu/intr_2_0_timing
acceptance/ppu/intr_2_mode0_timing
acceptance/ppu/intr_2_mode0_timing_sprites
acceptance/ppu/intr_2_mode3_timing
acceptance/ppu/intr_2_oam_ok_timing
acceptance/ppu/lcdon_timing-dmgABCmgbS
acceptance/ppu/lcdon_write_timing-GS
acceptance/ppu/stat_irq_blocking
acceptance/ppu/stat_lyc_onoff
acceptance/ppu/vblank_stat_intr-GS
acceptance/push_timing
acceptance/rapid_di_ei
acceptance/ret_cc_timing
acceptance/ret_timing
acceptance/reti_intr_timing
acceptance/reti_timing
acceptance/rst_timing
acceptance/serial/boot_sclk_align-dmgABCmgb
acceptance/timer/div_write
acceptance/timer/rapid_toggle
acceptance/timer/tim00
acceptance/timer/tim00_div_trigger
acceptance/timer/tim01
acceptance/timer/tim01_div_trigger
acceptance/timer/tim10
acceptance/timer/tim10_div_trigger
acceptance/timer/tim11
acceptance/timer/tim11_div_trigger
acceptance/timer/tima_reload
acceptance/timer/tima_write_reloading
acceptance/timer/tma_write_reloading
madness/mgb_oam_dma_halt_sprites
manual-only/sprite_priority

Timing Model

The simulation itself is running on gate/cell level. The signal delays however are calculated on transistor level, taking into account every transistor within each cell to calculate total delays through cells.

The signal delays are calculated based on the following properties:

  • Total resistance and capacity of the net. The resistance and capacity are calculated from the net's length.
  • Resistance (driving strength) and type (NMOS/PMOS) of the transistor that drives the net. The resistance is calculated from the transistor width.
  • Total capacity of all transistor gates driven by the net. The capacity is calculated from the widths of the transistors.

Very simplified example of a net:

In this example, the inputs A, B and C can be driven by drivers A or B through the blue net. For each of the two drivers of the net the signal delay is calculated separately. Driver A calculates the delay based on driver A's width, the net's total length, and the total width of the three gate inputs A, B and C. Driver B calculates the delay mostly the same, but instead of driver A's width, it uses driver B's width of course.

One would think that driver A has less resistance than driver B, since driver A is twice as wide, but this would only be the case when comparing transistors of the same type. Driver A in this example is a PMOS transistor. PMOS transistors are only half as efficient as NMOS transistors. Therefore, a PMOS with twice the width of an NMOS has roughly the same resistance as the NMOS, due to its halved efficiency.

The model does not account for the fact that the signal delay should be shorter for the path from driver B to input C. It always uses the total length of the net for the calculation. This model has more limitations: It does not discriminate between materials (metal, poly, active), and it always assumes driving transistors or chains of driving transistors are sourced by VDD or GND directly. This leads to way too short delays when modelling transmission gates. I had to manually adjust this in some flip-flop cells that contain muxers implemented with transmission gates.

I tuned the constants in dmg_cpu_b/timing-default.sv and sm83/timing-default.sv to make the simulation fit as best as I could with reality. I made the following measurements on the cartridge port of a real Game Boy DMG-CPU B:

Name Description
rd‑glitch The width of the 0-hazard glitches that happen on the /RD line when the CPU writes to the internal HRAM or memory mapped registers.
phi‑rise‑rd‑rise Delay between rising PHI and rising /RD at the beginning of a cartridge write access.
phi‑rise‑rd‑fall Delay between rising PHI and falling /RD at the end of a cartridge write access.
phi‑rise‑a15‑rise Delay between rising PHI and rising A15 at the end of each ROM access.
phi‑rise‑a15‑fall Delay between rising PHI and falling A15 at the beginning of each ROM access.
phi‑rise‑cs‑rise Delay between rising PHI and rising /CS at the end of RAM access.
wr‑rise‑phi‑rise Delay between rising /WR and rising PHI at the end of a cartridge write access.
phi‑fall‑wr‑fall Delay between falling PHI and falling /WR at the beginning (or rather in the very middle) of a cartridge write access.
phi‑rise‑a14‑fall‑ext Delay between rising PHI and falling A14 at the end of a cartridge access.
phi‑rise‑a14‑fall‑int Delay between rising PHI and falling A14 at the end of internal HRAM access.
a8‑rise‑a7‑rise Delay between rising A8 and rising A7 at the beginning of internal HRAM access.

Here is the comparison of the measured delays vs. the delays in simulation:

Name Measured Simulation Result Deviation
rd‑glitch 18.6 ns 20.160 ns +8.3%
phi‑rise‑rd‑rise 148.4 ns 141.849 ns -4.5%
phi‑rise‑rd‑fall 23.0 ns 26.071 ns +13.3%
phi‑rise‑a15‑rise 3.6 ns 2.860 ns -20.6%
phi‑rise‑a15‑fall 241.0 ns 241.534 ns +0.2%
phi‑rise‑cs‑rise 4.4 ns 2.848 ns -35.3%
wr‑rise‑phi‑rise 115.6 ns 116.216 ns +0.5%
phi‑fall‑wr‑fall 5.0 ns 5.758 ns +15.1%
phi‑rise‑a14‑fall‑ext 28.0 ns 20.548 ns -26.7%
phi‑rise‑a14‑fall‑int 150.0 ns 143.725 ns -4.2%
a8‑rise‑a7‑rise 4.2 ns 4.207 ns +0.1%

Event File

The event file you can optionally provide using the makefile variable EVENT_FILE must contain one event per line in the following format:

<HALF-TICK> <COMMAND> <ARGUMENT>

<HALF-TICK> specifies the half-tick number at which the command executes. A number greater than zero specifies an absolute half-tick number. The counter that counts the systems half-ticks starts counting at 0 when the simulation starts and gets incremented each time the clk signal changes. The counter can be reset to 0 by the origin command. If <HALF-TICK> is less than or equal to zero, its magnitude specifies a half-tick change relative to the previous line.

<ARGUMENT> is an integer and the meaning depends on <COMMAND>.

<COMMAND> can be one of the following commands:

origin:
Sets the half-tick counter back to 0. <ARGUMENT> is ignored.

exit:
Terminates the simulation with the exit code <ARGUMENT>.

wait_sys_reset_done:
Suspends the half-tick counter until the signal sys_reset falls for the first time. The execution of the event list also pauses until the reset is done. <ARGUMENT> is ignored.

htick_len:
Overrides the time delay between the current and the next half-tick with <ARGUMENT> picoseconds. Can be used to introduce overclocking events.

print_cpu_regs:
Prints the CPU registers to STDOUT. <ARGUMENT> is ignored.

Example file:

   0  wait_sys_reset_done      0
 100  htick_len            20000
   0  print_cpu_regs           0
  -8  exit                     0

This example waits until the system reset is done, then half-tick number 100 gets overclocked by reducing its width to 20 ns. During this overclocked half-tick the CPU registers get printed. Then eight half-ticks after that, the simulation terminates with exit code 0.

About

SystemVerilog files for simulating a complete Game Boy system with DMG-CPU B chip

Topics

Resources

Stars

19 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages