Pacman on an FPGA! (in SystemVerilog)

Writing UNDER PROGRESS

THIS PAGE ISN’T YET COMPLETE!! I will finish it once I have time!

Pacman Game

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!!!!

Vivado on a Virtual Machine, Stream Through a Browser

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 :)

Checkerboard VGA test pattern

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:

  1. A VGA simulator expects sx/sy coordinates, while a real VGA display would infer them from HSYNC and VSYNC 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
);
  1. 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.

  1. 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!

Ghost Targeting Pacman

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)
───────────────────────────────────────────────────────────────────────────────