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.
| 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. |
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.
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
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.
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.
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.
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 | ✅ |
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% |
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.