Pacman on an FPGA! (in SystemVerilog)
Writing UNDER PROGRESS
THIS PAGE ISN’T YET COMPLETE!! I will finish it once I have time!
Coolest Project in the history of projects!!
Well, this is one of my coolest projects, even beating my RayTracer! If you are wondering why would someone go through the pain of making a game at RTL/hardware level, it is because I love hardware so much!!! (Actually it was a course project, I was forced to do it haha.)
Choosing a game
Given the Nexys A7 FPGA to implement any game of my choice, me and my colleague had to pick a game, and after lots of arguing, we picked pacman as it sounded challenging yet doable.
“All the computer games available at the time were of the violent type - war games and space invader types. There were no games that everyone could enjoy, and especially none for women. I wanted to come up with a “comical” game women could enjoy.”
- Toru Iwatani, Pac-Man creator
What a nice quote by Toru. Fun Fact: the creator of Pacman is still alive at the nice age of 69 at the time of writing. ( ͡° ͜ʖ ͡°)
I would say I am glad my colleague (and friend) persuaded me not to implement DOOM or Chess, as a simple pacman game was time consuming enough.
Le Design Process
A typical start to any project that uses a display, would be to start by implementing the display first, which I already have done a year ago on Altera Quartus . This time it was a matter of using Vivado, which I soon came to hate…
VGAs, a quick primer
While there are more or less infinite blog already on VGAs (check this by project F), I will quickly go over how a VGA works :) (Anyway I will have to write this section for my project report, which sucks)
TODO:::
The so called Pain Development Cycle
Without getting into politics, Vivado does ban many countries due to US export control. This means that students can’t install it on their local machines, and instead have to connect to a remote workstation through the browser and use Vivado.
In case you can’t tell how horrible the last sentence was, let me elaborate. Imagine you are a Vim/Emacs user. Your editors is so light that you expect things to happen instantaneously the moment you click a keyboard button. You consider using the mouse inefficient and use a Tiling Window Manager on a very lightweight operating system (I use Hyprland on Archlinux btw, yes this is a flex).
Now from such an efficient, lightweight and configurable system, you are forced to use a super slow Vivado IDE, on an Ubuntu/XFCE virtual machine, that is streamed ON A WEB BROWSER!! There goes all your key binds since browser don’t support them. There goes your sanity with every single interaction as you experience the slow latency of the stream combined with the shit hole that is Vivado. Then worst of all: the COMPILE TIMES!!!!
Now, maybe, just for the sake for argument, maybe you could actually
manage to endure through this pain. Ask yourself, how would your
program your FPGA from a virtual machine over a browser? Can you
pass-through your USB? (I hope this ins’t possible!!!). Obviously you
send your generated .bin
to your local computer, and then program
your FPGA, disconnect your monitor and then use it to test the
FPGA. Having to type this was painful, I don’t understand how other
students were able to do it.
Needless to say, I would never wish this setup on my worst enemies. Something had to be done ASAP!!
Verilator to the rescue!
Luckily, I recall reading ZipCPU’s article on Verilator, which ended saving what remained of my sanity. Project F wrote a verilog simulator with Verilator and SDL, which I couldn’t get to work on linux for some reason. I ported that simulator to SFML which I am more familiar with instead of debugging SDL.
Now what is Verilator? On a basic level, it compiles Verilog/SystemVerilog into C++. Now, one can wrap his simulation code around the C++ code produced by Verilator.
Not only is this comfortable, but FAST! A clean build will take
❯ cmake -B build -G Ninja
❯ time ninja -C build
97.65s user 6.08s system 997% cpu 10.400 total
i.e., around 10 seconds. (97.65 CPU seconds on multiple cores,
translate to 10 seconds of real time) TODO: Check if my interpretation
of time
is correct
However, subsequent builds are even much faster since non-changed objects are cached.
According to Verilator’s webpage:
Verilator may not be the best choice if you are expecting a full-featured replacement for a closed-source Verilog simulator, need SDF annotation, mixed-signal simulation, or are doing a quick class project (we recommend Icarus Verilog for classwork). However, if you are looking for a path to migrate SystemVerilog to C++/SystemC, or want high-speed simulation of designs, Verilator is the tool for you.
Well, I am glad I used it for a class project :)
A simple Checkboard VGA test pattern took ~5 seconds for a clean build, and ran at around 1FPS!
However it isn’t smooth sailing as one would expect, the SystemVerilog
codebase is now littered with `ifdef`
statements. This is
mainly due to 3 reasons:
- A VGA simulator expects
sx
/sy
coordinates, while a real VGA display would infer them fromHSYNC
andVSYNC
signals. It also expects a pixel clock, while a real display doesn’t. While I could have made my simulator act exactly like a VGA, it would slow down the simulator without any meaningful gain. It doesn’t have to be accurate, after all, it is just a simulator.
module top (
`ifdef VERILATOR
output logic [H_ADDR_WIDTH-1:0] sx,
output logic [V_ADDR_WIDTH-1:0] sy,
output logic display_enabled,
output logic pix_clk,
`endif
input logic CLK100MHZ,
input logic CPU_RESETN,
output logic [3:0] VGA_R, VGA_G, VGA_B,
output logic VGA_HS, VGA_VS
);
- Vivado’s IPs can’t be simulated in Vivado, thus I would have to rewrite them in verilog and use them conditionally, this influences the total number of IPs that I have used (Spoiler: I used only 1 IP, a PLL.)
`ifdef VERILATOR
assign CLK25MHZ = CLK100MHZ;
`else
clk_wiz_0 clk25 (
.clk_out1(CLK25MHZ),
.clk_in1 (CLK100MHZ)
);
`endif
Note: I assigned 100MHZ
to 25MHZ
, this makes the simulator 4x
faster, at no real behavior mismatch.
- Vivado and Verilator handle file paths differently (for example in
$readmem()
). This forced every single path to be written twice, Maybe I could have forced Vivado to accept all paths as absolute, relative to the project directory, but I didn’t try.
module m (
`ifdef VERILATOR
.INITIAL_MEM_FILE("rtl/mem/orange_monster.mem"),
`else
.INITIAL_MEM_FILE("../mem/orange_monster.mem"),
`endif
)
module #(param INITIAL_MEM_FILE = "") ();
$readmemh(INITIAL_MEM_FILE, ram);
endmodule
Vivado Sucks for Git/Github
It sucks so much in every single way for use with Git that I don’t
even know where to start. The only good thing Vivado has going is that
Tcl
scripts can be used to generate and build an entire project
without having touch Vivado’s GUI. I use a Pacman.tcl
script to
generate my project from src, which is rather simple:
# Get a list of all .sv files in rtl/ directory (including subdirectories)
set files [glob -nocomplain "${origin_dir}/rtl/ip/*.sv"]
# Add the found files to the Vivado project
set added_files [add_files -fileset sources_1 $files]
# Loop through all the added files and set their file type to SystemVerilog
foreach file $files {
# Normalize the file path to get an absolute path
set normalized_file [file normalize $file]
# Get the file object for the current file
set file_obj [get_files -of_objects [get_filesets sources_1] [list $normalized_file]]
# Set the file type to SystemVerilog
set_property -name "file_type" -value "SystemVerilog" -objects $file_obj
}
And no, I haven’t committed to learn Tcl scripting, I used ChatGPT(4o), and started with an exported Tcl by Vivado-Git wrapper.
The Actual Game Design
Well, I spent such a large chunk of the post without actually getting into any of the actual design! In case you haven’t realized, I am a tooling guy. I will spend a week perfecting my tooling and sharpening my knife before even using it. This is a double edged sword, in which both edges require sharpening ;)
While it would have been ideal to go through each and every design decision, I am writing this after finishing my project, thus an explanation of the current design is all you readers will get :(. Any way, going the design decision that went into 180+ commits and ~4k thousand lines of code will be incredibly boring to read.
If you are motivated enough dear readers, I advice you go to read this excellent pacman game description. Pacman is far more complicated than I thought. For example, ghost actually try to gang up on the player from multiple direction, and each ghost has it’s own tracking and following algorithm!
TODO I guess
- Tcl
- tree for file hierarchy
───────────────────────────────────────────────────────────────────────────────
Language Files Lines Blanks Comments Code Complexity
───────────────────────────────────────────────────────────────────────────────
SystemVerilog 29 3178 391 830 1957 371
Python 2 107 19 21 67 13
C++ 1 146 23 20 103 14
CMake 1 71 17 22 32 1
Coq 1 0 0 0 0 0
License 1 289 64 0 225 0
Markdown 1 25 10 0 15 0
TCL 1 619 83 108 428 105
───────────────────────────────────────────────────────────────────────────────
Total 37 4435 607 1001 2827 504
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop (organic) $80,442
Estimated Schedule Effort (organic) 5.28 months
Estimated People Required (organic) 1.35
───────────────────────────────────────────────────────────────────────────────
Processed 147430 bytes, 0.147 megabytes (SI)
───────────────────────────────────────────────────────────────────────────────