Skip to content
← All projects

Project / Digital Logic

FPGA Whack-A-Mole

Role
Team (2 people)
Timeline
Spring 2026
Context
EC311 — Digital Logic
Tools
Verilog Vivado Nexys4 DDR

At a glance

Problem
Implement a real-time Whack-A-Mole game entirely in hardware — no processor, no firmware — using only synchronous digital logic on an FPGA.
Approach
Distributed FSM architecture: 16 identical mole modules instantiated via a generate loop, a 16-bit LFSR for random spawning, a custom VGA renderer for the 4×4 mole grid, and 7-segment display for score and countdown timer.
Outcome
Fully playable on the Nexys4 DDR board — 16 switch-controlled moles, 640×480 VGA output, 90-second timed rounds, score tracking up to 99, and a red game-over flash.
board / VGA display photo
Fig. 1 — Nexys4 DDR running Whack-A-Mole. Switches 0–15 map to the 4×4 mole grid shown on the VGA monitor; active moles light their corresponding LED green.

Overview

This is a fully digital Whack-A-Mole game implemented in Verilog and synthesized onto a Nexys4 DDR FPGA (Xilinx Artix-7 XC7A100T). All game logic — mole spawning, hit detection, scoring, timing, and display — runs entirely in hardware using finite state machines and synchronous digital design. There is no soft-core processor; every behavior, from switch debouncing to VGA pixel output, comes from combinational and sequential logic synthesized directly onto the FPGA fabric.

Players have 90 seconds to hit as many moles as possible. Each of the 16 switches corresponds to one mole; when a mole spawns, its LED lights and its cell on the VGA display turns green. Toggling the switch before the 1-second window expires registers a hit. A 16-bit LFSR pseudorandomly selects which mole spawns next and adds jitter to the spawn interval, keeping the sequence unpredictable. Score and remaining time appear on the 7-segment display throughout the round. When the timer reaches zero, LED16_R turns red and the VGA screen floods to solid red — game over.

Design decisions

Distributed mole FSMs via generate loop

Rather than a single monolithic game FSM, each mole is an independent mole module with its own active flag, 1-second countdown timer, and hit register. The top level instantiates all 16 with a genvar loop, wiring each to its corresponding switch, LED, and spawn-bus bit. This keeps the per-mole logic self-contained — adding moles means only changing the loop bound — and lets each module handle its own reset independently when the game ends.

LFSR for mole selection and spawn-interval jitter

A 16-bit maximal-length LFSR (feedback taps at bits 15, 14, 12, and 3) generates a new pseudorandom value every clock cycle. The spawner uses bits [3:0] to one-hot decode which of the 16 moles to activate next, and bits [7:4] to add interval jitter: current_period = MIN_PERIOD + {rng[7:4], 23'b0}, giving a spawn delay between 1.0 and ~2.3 seconds. The LFSR is seeded once at startup with a fixed 16-bit value; the combination of a fixed seed and the maximal-length polynomial means every game session follows the same sequence, but it appears random during play.

Two-FF synchronizer and any-edge hit detection

Each switch input passes through a two-stage D flip-flop synchronizer to resolve metastability before any logic looks at it. The module then compares the current and previous synchronized values: sw_change = sw_sync1 ^ sw_prev. Any transition — press or release — within the active window registers as a hit. The others_active input, driven from the OR of all other LEDs, gates this: a hit only scores when no other moles are simultaneously lit. This prevents a single switch flick from accidentally crediting multiple moles during edge cases where two are active at once.

Custom VGA renderer — no IP blocks

The VGA display is implemented from scratch as a state machine that generates standard 640×480 @ 60 Hz sync signals and 12-bit color output. The screen is divided into a 4×4 grid of 160×120-pixel cells, one per mole. At each pixel clock, the module computes col = h_pos / 160 and row = v_pos / 120, indexes the 16-bit mole bus with mole_index = {row, col}, and paints the cell: green for an active mole, dark gray for idle, with an 8-pixel black border between cells. On game over the entire screen switches to solid red. No framebuffer or block RAM is used — the pixel color is computed combinationally each cycle directly from the mole bus.

block diagram
Fig. 2 — Top-level block diagram. The spawner consumes the LFSR output and pulses a single bit on the 16-bit spawn bus each interval; each mole module handles its own timing and hit detection independently.
VGA display / in-game photo
Fig. 3 — VGA output during an active round. Each cell is 160×120 px; green cells indicate a spawned mole awaiting a hit. The 7-segment display (not shown) simultaneously shows score and countdown.
Technical details
Parameter Value Notes
FPGA board Nexys4 DDR Rev. C Artix-7 XC7A100T
Clock 100 MHz On-board oscillator, pin E3
HDL Verilog Synthesized with Vivado
Mole positions 16 switch[15:0] / LED[15:0], 4×4 VGA grid
Mole window 1 s 99,999,999 cycles @ 100 MHz
Spawn interval 1.0 – 2.3 s MIN_PERIOD + {rng[7:4], 23'b0}
Game duration 90 s Countdown via 1 Hz clock divider
Score display 8-digit 7-seg Score (upper digits), timer (lower digits)
VGA output 640×480 @ 60 Hz Custom sync generator, 12-bit color
PRNG 16-bit LFSR Maximal-length, taps [15,14,12,3], seed 56394
Startup delay 3 s 299,999,999-cycle hold before game begins
Max score 99 8-bit counter with BCD output
Verilog — key snippets

16 mole instances — generate loop (top.v)

genvar i;
generate
    for (i = 0; i < 16; i = i + 1) begin : mole_gen
        mole m (
            .clock(clock),
            .rst(game_rst),
            .spawn(spawn_bus[i]),
            .switch_raw(switch[i]),
            .game_over(game_over),
            .others_active(|(led & ~(16'b1 << i))),
            .led(led[i]),
            .hit(hits[i])
        );
    end
endgenerate

LFSR PRNG — 16-bit maximal-length (PRNG.v)

// taps at [15,14,12,3] give a period of 2^16 - 1
wire feedback = out[15] ^ out[14] ^ out[12] ^ out[3];

always @(posedge clk or posedge rst)
    if (rst)       out <= 16'hFFFF;
    else if (load) out <= seed;        // pulse once at startup
    else           out <= {out[14:0], feedback};

Random spawn with interval jitter (spawner.v)

// rng[3:0]  → one-hot mole select (0–15)
// rng[7:4]  → interval jitter (adds 0–1.34 s on top of 1 s minimum)
if (tick_counter == current_period) begin
    tick_counter   <= 28'd0;
    spawn          <= 16'b1 << rng[3:0];
    current_period <= MIN_PERIOD + {rng[7:4], 23'b0};
end

Switch sync + any-edge hit detection (mole.v)

// two-FF metastability synchronizer
always @(posedge clock or posedge rst)
    if (rst) begin sw_sync0 <= 0; sw_sync1 <= 0; sw_prev <= 0; end
    else     begin sw_sync0 <= switch_raw;
                   sw_sync1 <= sw_sync0;
                   sw_prev  <= sw_sync1; end

wire sw_change = sw_sync1 ^ sw_prev; // detect any edge

// in active state:
if (sw_change && !others_active) begin
    hit    <= 1'b1;   // scored
    active <= 1'b0;
end else if (timer == 26'd0) begin
    active <= 1'b0;   // missed — window expired
end

VGA pixel color — combinational (vga.v)

// 4×4 grid: each cell is 160×120 px, 8 px border
wire [1:0] col        = horizontal_position / 160;
wire [1:0] row        = vertical_position   / 120;
wire [3:0] mole_index = {row, col};
wire       in_border  = (x_in_cell < 8) || (x_in_cell >= 152)
                      || (y_in_cell < 8) || (y_in_cell >= 112);

always @(*) begin
    if (game_over)          { pixel_r, pixel_g, pixel_b } = 12'hF00; // red
    else if (in_border)     { pixel_r, pixel_g, pixel_b } = 12'h000; // black
    else if (moles[mole_index]) { pixel_r, pixel_g, pixel_b } = 12'h0F0; // green
    else                    { pixel_r, pixel_g, pixel_b } = 12'h444; // gray
end