Road Rage: An FPGA Real-Time Game in SystemVerilog
Overview
ECE 4515 — Digital Design II · Virginia Tech · Spring 2026 Team project with Rahul Muthuraman Shanmugam.
A top-down lane-dodge racing game, implemented entirely in hardware on a Terasic DE1-SoC (Cyclone V 5CSEMA5F31C6). No processor, no software — the game is a state machine driving datapath modules at pixel rate.
Why build a game in RTL
A game is a deceptively good digital design exercise. It forces real-time constraints that cannot be missed (a VGA scanline waits for nobody), multiple interacting state machines, memory-backed graphics, asynchronous inputs that must be synchronised and debounced, and a pipeline whose latency has to be accounted for exactly — because a one-cycle error shows up as a visible column of wrong pixels.
System architecture
Thirteen RTL modules under a single top level:
1final_project.sv (top)
2├── pll_25mhz — 50 MHz → 25.175 MHz pixel clock (Quartus IP)
3├── vga_controller — 640×480 timing; h_count, v_count, blank_n, hs, vs
4├── road_graphics — perspective road background, 1-cycle registered
5├── sprite_overlay — player + 3 enemy sprites from ROM, 2-cycle pipeline
6├── game_fsm — game state, score, speed, animation frame
7├── player_ctrl — player X position and pose
8├── enemy_ctrl — 3 enemies: Y scroll, recycling, lane assignment
9├── collision — combinational AABB overlap
10├── button_debounce×4 — 20 ms debounce; pulse and held outputs
11├── lfsr — 8-bit LFSR for random lane selection
12├── bcd_converter — 14-bit binary → 4-digit BCD
13└── seg7_decoder×4 — BCD → active-LOW seven-segment
Two clock domains
The design runs the game logic at 50 MHz and graphics at 25.175 MHz off a PLL. Splitting them keeps the pixel pipeline at exactly VGA rate while leaving logic on the board's native clock, and it is what makes the resource numbers below achievable.
The consequence is a clock-domain crossing on the new_frame signal, which has to be synchronised deliberately rather than left to chance — the one genuinely subtle piece of the design.
Key design patterns
Frame tick. Game state advances on a 60 Hz tick derived by counting 833,333 cycles of the 50 MHz clock. Everything in game logic is synchronous to that tick, so motion is frame-rate exact rather than dependent on how long a computation took.
Perspective road. The road widens with v_count by a fixed formula evaluated per pixel — perspective without any multiplication in the critical path.
AABB collision. Axis-aligned bounding-box overlap, computed combinationally. In software this is a loop; in hardware it is four comparators and an AND.
Debouncing. 20 ms (1,000,000 cycles at 50 MHz), producing both a single-cycle pulse and a level-held output, so menu presses and steering behave differently from the same button.
Sprite ROMs. Player poses and enemy variants stored as MIF-initialised block RAM, generated by a Python script rather than written by hand.
Verification
Eight testbenches run under ModelSim/Questa via do compile_all.do, which compiles all thirteen modules and executes the full suite. Simulating before synthesising matters here — a bug found in a waveform takes minutes, the same bug found on a monitor takes an afternoon.
Results
Synthesised clean in Quartus Prime on the Cyclone V:
| Resource | Utilisation |
|---|---|
| Logic elements | 24% |
| Dedicated registers | 18% |
| Block RAM | 21% |
Timing closed at both clock domains, with the design running stably at 640×480 / 60 Hz on hardware.
Takeaway
This was the project where hardware design stopped being about writing modules and started being about budgeting — cycles, block RAM, and logic elements are finite, and every design choice spends one of them. Staying under a quarter of the device while hitting exact VGA timing was more instructive than any amount of simulation-only coursework.