Skip to content
Kanishk Sama
← Projects

FPGA Rhythm Game

An arcade rhythm game built as a modular Verilog engine on a Basys 3 — a controller FSM and datapath handling scoring, timing, input, and display, with exact pause/resume semantics.

RTLFPGAVerification
Period
Summer 2026
Role
Solo
Stack
Verilog · Vivado · Basys 3 · Self-checking testbenches
5-state
controller FSM
Vectorized
simultaneous hit detection
Exact
pause / resume via enable gating

The problem

A rhythm game is a real-time system with a hard correctness requirement hiding inside something that looks like a toy. Inputs arrive whenever the player hits them, possibly several at once. Scoring depends on when an input arrives relative to a moving target. And the whole thing has to keep running while a display is being driven.

Nothing here is difficult in software. In hardware, on a Basys 3, each of those is a timing and state problem, and they interact.

What I built

A modular Verilog game engine split the conventional way — a 5-state controller FSM sequencing the game, and a datapath handling scoring, timing, input, and display. Keeping control and data separate is what made the FSM small enough to reason about; the scoring arithmetic never lives inside the state machine.

Two decisions carried the design:

Enable-driven state control for pause/resume. Pausing by freezing a clock is the obvious approach and the wrong one — it creates a second clock domain and everything that comes with it. Instead the state advances on an enable signal, so pause is simply withholding enable. State is preserved exactly, resume continues from precisely where it stopped, and there is still one clock in the design.

Vectorized hit detection. Inputs are evaluated as a vector rather than sequentially, so multiple simultaneous presses are all detected in the same cycle. Handling inputs one at a time would make simultaneous hits order-dependent, which the player would experience as dropped inputs.

Verification

Self-checking testbenches on the controller and display logic, plus integration tests covering full-game state transitions, pause/resume, round completion, and display sequencing. Integration-level tests matter more than usual here: the individual blocks were straightforward, and essentially all the difficulty lived in how they behaved together over a full game.

Bugs found on hardware

The three real bugs were reset, wraparound, and stale state — and all three are the category that simulation is worst at catching, because each depends on a condition a testbench doesn’t think to create:

  • Reset — behavior when the design is brought up, rather than during steady-state play.
  • Wraparound — a counter crossing its maximum. A test that plays a normal-length round never reaches it.
  • Stale state — a value left over from a previous round being read as current. Only observable across a boundary a single-round test never crosses.

The through-line: my testbenches were checking that the game worked, and the bugs were all in what happened at the edges of a game — startup, rollover, and the seam between rounds. That’s the lesson I took from this project, and it’s the reason the accelerator work that came after leans on randomized stimulus rather than only reference cases.

What I’d do next

  • Constrained-random input sequences. Every bug above would have been reachable with randomized play patterns and long-running sequences; my directed tests encoded my own assumptions about how a round goes.
  • Assertions on the FSM. Illegal state transitions and stale-read conditions are exactly what concurrent assertions are for, and would have caught two of the three at simulation time.