Q LeAll projects

An FPGA game in Verilog

FPGA game

CSE 100 · Logic design · Basys 3 · Verilog · VGA · Winter 2025

An osmosis game made entirely out of logic: eight molecules, four red and four blue, bounce around a VGA screen, and you sort them with a membrane down the middle before the clock runs out. No processor, just Verilog on a Basys 3.

A win on the lab monitor: blues left, reds right, green border lit. Basys 3 in front, March 2025.
A win on the lab monitor: blues left, reds right, green border lit. Basys 3 in front, March 2025.

How it plays

You set a time from 1 to 255 seconds on the switches, and it counts down on the seven-segment display. Press start and the molecules scramble for 8 seconds before the membrane comes alive. Holding the left button lets blue through and the right button lets red through. Get all four blues on one side and all four reds on the other and the border flashes green. If the timer hits zero first, the display flashes instead. Either way the membrane locks until you play again.

Getting it built

I built most of it in a three-day, energy-drink-fuelled sprint in February 2025. A lot broke along the way, and honestly I don’t remember most of it. The labs built up from our own parts, so the counters here are the 4-bit counters from an earlier lab, chained into 8- and 16-bit ones.

Finishing the base game early left room for the optional bonus: a button that re-randomises every molecule’s start position, direction and speed. Only a handful of people in the class did it.

Drawing the screen

Two 16-bit counters sweep a cursor over an 800×525 frame, and a sync module turns that cursor into horizontal and vertical sync pulses. I put the sync outputs through a flip-flop so they switch cleanly, but that adds a clock of delay. To cancel it, each pulse is decoded one pixel early: column 655 instead of the textbook 656, and row 489 instead of 490.

Pixel-address block: two up/down counters with resets at 800 and 525.
Pixel-address block: two up/down counters with resets at 800 and 525.
Simulation: sync pulses landing on the right pixel addresses.
Simulation: sync pulses landing on the right pixel addresses.

Moving the molecules

Each molecule is its own module holding X and Y counters and two direction bits. A bit flips when the molecule touches a wall, or the membrane while its colour isn’t being let through. Every frame it steps one pixel. A fast molecule gets a second, edge-detected pulse and steps two. It draws itself red or blue whenever the cursor is over it, so there’s no frame buffer at all.

Eight random molecules from one generator

The randomness comes from a single 21-bit linear-feedback shift register. The problem is that if all eight molecules read it at the same time, they all get the same number. So when you hit randomise, it reads the register on eight different frames, two frames apart, and each molecule saves its own copy as a 22-bit start-up word. All the molecules are held in reset while the values load. Without randomising, they fall back to a fixed starting layout.

Each sample is split into fields: 10 bits of X, 9 bits of Y, 2 bits of direction and 1 bit of speed. A position past the edge of the play area wraps back into range and is then shifted in from the border, so nothing spawns inside a wall.

Game loop

A one-hot state register runs five states: setup, 8-second scramble, play, win and lose. Counting 60 vertical syncs gives a 1-second tick, so the scramble is 480 frames. The win check compares all four blue molecules against one side of the membrane and all four red against the other. For testing, I wired the state to the LEDs and added cheat switches that show the timer or open both sides of the membrane at once.

Game state diagram: setup, 8-second scramble, play, win and lose.
Game state diagram: setup, 8-second scramble, play, win and lose.

Timing

The Basys 3’s 100 MHz oscillator is divided down by the course’s clock module, and the whole game runs on the result: a roughly 25 MHz clock, one tick per pixel, which is what an 800×525 sweep at 60 frames a second needs. At that clock the design meets timing with zero failing endpoints out of 1,322. The worst setup slack is 25.6 ns, so the slowest path uses only about 14 ns of its 40 ns, and the worst hold slack is 0.126 ns.

Vivado design timing summary. All user-specified timing constraints are met.
Vivado design timing summary. All user-specified timing constraints are met.