Objective and use case
What you’ll build: A binary counter on the Basys 3 FPGA that increments and displays a 4-bit value on a seven-segment display.
Why it matters / Use cases
- Demonstrates the use of Verilog for hardware design, allowing for real-time digital circuit implementation.
- Provides a practical example of using FPGA technology in educational settings, enhancing learning in digital electronics.
- Serves as a foundational project for understanding more complex state machines and digital logic design.
- Illustrates the integration of hardware and software through the use of Vivado for synthesis and implementation.
Expected outcome
- Successful display of 4-bit binary values on the seven-segment display with a refresh rate of 1 Hz.
- Verification of correct counting from 0000 to 1111, ensuring no skipped counts or incorrect displays.
- Documentation of synthesis and implementation times, aiming for under 10 seconds for both processes.
- Measurement of power consumption during operation, targeting less than 500 mW.
Audience: Electronics students and hobbyists; Level: Intermediate
Architecture/flow: Basys 3 FPGA with Xilinx Artix‑7, using Verilog and Vivado for design and programming.
Practical Case: Binary Counter on Seven-Segment (Basys 3, Xilinx Artix‑7)
This hands-on project walks you through building a basic binary counter on the Basys 3 (Xilinx Artix‑7) FPGA. The counter increments at a human‑readable rate and displays a 4‑bit binary value across the four digits of the on‑board seven‑segment display. Each digit shows either “0” or “1,” so together the four digits represent the current 4‑bit count (from 0000 to 1111). We will use Verilog, Vivado WebPACK (CLI, non‑project flow), a simple constraints file, and Tcl scripts to build, program, and validate.
The focus is practical: you will get the counter running, understand the connections, and have repeatable CLI steps to synthesize, implement, and program the FPGA.
Prerequisites
- Basic comfort with:
- Command line (Windows PowerShell or Linux/macOS shell)
- Verilog syntax
- Clock division and simple state machines
- Installed tools (Vivado WebPACK):
- Xilinx Vivado WebPACK 2023.2 (or later WebPACK edition)
- Xilinx cable (USB‑JTAG) drivers installed with Vivado
- Verified that your system recognizes the board over USB:
- On Windows, verify the Xilinx USB controller appears in Device Manager
- On Linux/macOS, ensure you can run the Vivado hardware server and see the device
You can confirm the Vivado version from your terminal:
vivado -version
Expected output includes something like: «Vivado v2023.2 (64-bit)» and build info.
Materials
- FPGA board: Basys 3 (Xilinx Artix‑7), exact model: Digilent Basys 3, device part xc7a35tcpg236-1
- USB cable: Micro‑USB for power/programming
- Host computer with Vivado WebPACK 2023.2 (CLI) installed
Optional (for convenience): An anti‑static mat and wrist strap.
Setup/Connection
- Power and JTAG:
- Plug a micro‑USB cable from your host PC to the Basys 3’s USB connector.
- Ensure the power select jumper is set to USB (usually default on Basys 3).
- Turn the Basys 3 power switch ON.
- No external wiring is required; we use only on‑board devices:
- 100 MHz system clock (on‑board oscillator)
- Four‑digit common‑anode seven‑segment display (on‑board)
Important note about the seven‑segment display:
– The Basys 3 seven‑segment is common‑anode and multiplexed.
– That means:
– The anode enable signals AN0..AN3 are active‑low (0 = digit on).
– The segment lines (CA..CG, DP) are also active‑low (0 = segment on).
We will take advantage of this by setting an appropriate refresh scanning rate and driving segment/anode lines with the correct polarity.
Full Code
We will use a single top‑level Verilog module that:
1. Divides the 100 MHz clock down to:
– A ~1 kHz multiplex refresh tick (for digit scanning)
– A slow tick (e.g., 2 Hz) for counting
2. Maintains a 4‑bit counter (0..15)
3. Maps the 4 bits to the four digits (MSB on left, LSB on right)
4. Encodes “0” and “1” into seven‑segment patterns for each digit
5. Drives the anode and segment lines with active‑low logic
We will also include an XDC constraints file to map logical ports to the Basys 3 pins.
Top‑Level Verilog (src/top_basys3_counter.v)
// File: src/top_basys3_counter.v
// Board: Basys 3 (Xilinx Artix-7, xc7a35tcpg236-1)
// Function: Display a 4-bit binary counter (0..15) across the four seven-seg digits.
// Each digit shows either '0' or '1', representing one bit of the count.
// Tools: Vivado WebPACK 2023.2 (non-project flow)
module top_basys3_counter (
input wire clk, // 100 MHz system clock
output reg [3:0] an, // anode enables (active low): an[0]=AN0 ... an[3]=AN3
output reg seg_a, // segment a (active low)
output reg seg_b, // segment b (active low)
output reg seg_c, // segment c (active low)
output reg seg_d, // segment d (active low)
output reg seg_e, // segment e (active low)
output reg seg_f, // segment f (active low)
output reg seg_g, // segment g (active low)
output reg dp // decimal point (active low)
);
// ------------------------------------------------------------
// Parameters for timing
// ------------------------------------------------------------
localparam integer CLK_FREQ_HZ = 100_000_000; // 100 MHz
localparam integer SCAN_RATE_HZ = 1000; // overall display update rate (per digit step)
localparam integer COUNT_RATE_HZ = 2; // counter increments per second
// Divide 100 MHz down to 1 kHz (digit scan). 100_000_000 / 1_000 = 100_000
localparam integer SCAN_DIVISOR = CLK_FREQ_HZ / SCAN_RATE_HZ; // 100_000
// Divide 100 MHz down to 2 Hz for counting. 100_000_000 / 2 = 50_000_000
localparam integer COUNT_DIVISOR = CLK_FREQ_HZ / COUNT_RATE_HZ;
// ------------------------------------------------------------
// Counters and state
// ------------------------------------------------------------
reg [31:0] scan_div_cnt = 0;
reg [31:0] count_div_cnt = 0;
reg [1:0] digit_sel = 0; // 0..3: which digit is active this scan step
reg [3:0] bin_count = 4'd0; // 4-bit binary counter (0..15)
// Current bit being displayed on the active digit (0 or 1)
wire bit_for_digit;
// Tie off decimal point (off = inactive = '1' for active-low)
always @* begin
dp = 1'b1;
end
// ------------------------------------------------------------
// Generate scan tick (~1 kHz) and rotate through digits
// ------------------------------------------------------------
always @(posedge clk) begin
if (scan_div_cnt >= SCAN_DIVISOR - 1) begin
scan_div_cnt <= 0;
digit_sel <= digit_sel + 2'd1; // cycle 0->1->2->3->0...
end else begin
scan_div_cnt <= scan_div_cnt + 1;
end
end
// ------------------------------------------------------------
// Generate slow counter tick (~2 Hz) and increment 4-bit counter
// ------------------------------------------------------------
always @(posedge clk) begin
if (count_div_cnt >= COUNT_DIVISOR - 1) begin
count_div_cnt <= 0;
bin_count <= bin_count + 4'd1;
end else begin
count_div_cnt <= count_div_cnt + 1;
end
end
// ------------------------------------------------------------
// Map digit_sel to which bit of bin_count is shown:
// digit 3 (leftmost) shows MSB (bin_count[3])
// digit 2 shows bin_count[2]
// digit 1 shows bin_count[1]
// digit 0 (rightmost) shows LSB (bin_count[0])
// ------------------------------------------------------------
assign bit_for_digit = (digit_sel == 2'd3) ? bin_count[3] :
(digit_sel == 2'd2) ? bin_count[2] :
(digit_sel == 2'd1) ? bin_count[1] :
bin_count[0];
// ------------------------------------------------------------
// Drive anodes (active low). Only one digit on at a time.
// digit_sel == 0 -> AN0 active; ==1 -> AN1; ==2 -> AN2; ==3 -> AN3
// ------------------------------------------------------------
always @* begin
case (digit_sel)
2'd0: an = 4'b1110; // AN0 on (0), others off (1)
2'd1: an = 4'b1101; // AN1 on
2'd2: an = 4'b1011; // AN2 on
default: an = 4'b0111; // AN3 on
endcase
end
// ------------------------------------------------------------
// Segment encoding for "0" and "1" (active-low, common-anode)
// A standard seven-seg uses segments a b c d e f g:
// '0' lights: a b c d e f (g off)
// '1' lights: b c (others off)
// Since signals are active-low, '0' means ON and '1' means OFF.
// ------------------------------------------------------------
always @* begin
if (bit_for_digit == 1'b0) begin
// Show '0' -> a,b,c,d,e,f on; g off
seg_a = 1'b0; // on
seg_b = 1'b0; // on
seg_c = 1'b0; // on
seg_d = 1'b0; // on
seg_e = 1'b0; // on
seg_f = 1'b0; // on
seg_g = 1'b1; // off
end else begin
// Show '1' -> b,c on; others off
seg_a = 1'b1; // off
seg_b = 1'b0; // on
seg_c = 1'b0; // on
seg_d = 1'b1; // off
seg_e = 1'b1; // off
seg_f = 1'b1; // off
seg_g = 1'b1; // off
end
end
endmodule
Constraints (constraints/basys3_7seg.xdc)
This XDC maps the top‑level ports to Basys 3 pins. It also sets the I/O standard and declares the 100 MHz clock for timing.
# Basys 3 pinout for 4-digit seven-segment and 100 MHz clock.
# Active-low anodes and segments (common-anode display).
# Tools: Vivado 2023.2
# 100 MHz clock (on-board oscillator)
set_property PACKAGE_PIN W5 [get_ports {clk}]
set_property IOSTANDARD LVCMOS33 [get_ports {clk}]
create_clock -period 10.000 -name sys_clk_pin [get_ports {clk}]
# Seven-segment segments (active-low)
# CA, CB, CC, CD, CE, CF, CG, DP pins (Basys 3)
set_property PACKAGE_PIN W7 [get_ports {seg_a}] ;# CA
set_property PACKAGE_PIN W6 [get_ports {seg_b}] ;# CB
set_property PACKAGE_PIN U8 [get_ports {seg_c}] ;# CC
set_property PACKAGE_PIN V8 [get_ports {seg_d}] ;# CD
set_property PACKAGE_PIN U5 [get_ports {seg_e}] ;# CE
set_property PACKAGE_PIN V5 [get_ports {seg_f}] ;# CF
set_property PACKAGE_PIN U7 [get_ports {seg_g}] ;# CG
set_property PACKAGE_PIN V7 [get_ports {dp}] ;# DP
set_property IOSTANDARD LVCMOS33 [get_ports {seg_a seg_b seg_c seg_d seg_e seg_f seg_g dp}]
# Seven-segment anodes (active-low enables)
# AN0..AN3
set_property PACKAGE_PIN U2 [get_ports {an[0]}]
set_property PACKAGE_PIN U4 [get_ports {an[1]}]
set_property PACKAGE_PIN V4 [get_ports {an[2]}]
set_property PACKAGE_PIN W4 [get_ports {an[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {an[0] an[1] an[2] an[3]}]
Build/Flash/Run Commands
We will use Vivado in batch mode (non‑project flow) for reproducibility. The project layout:
- basys3_binary_counter/
- src/top_basys3_counter.v
- constraints/basys3_7seg.xdc
- scripts/build.tcl
- scripts/program.tcl
- build/ (generated outputs)
Create the structure and populate files:
mkdir -p basys3_binary_counter/{src,constraints,scripts,build}
# Place the Verilog and XDC from above into:
# basys3_binary_counter/src/top_basys3_counter.v
# basys3_binary_counter/constraints/basys3_7seg.xdc
Build (synthesis, implementation, bitstream) with Vivado CLI
Create scripts/build.tcl:
# File: scripts/build.tcl
# Vivado WebPACK 2023.2 non-project build for Basys 3 (xc7a35tcpg236-1)
# Use paths relative to the script invocation directory
set src_file "src/top_basys3_counter.v"
set xdc_file "constraints/basys3_7seg.xdc"
set top_name "top_basys3_counter"
set part_name "xc7a35tcpg236-1"
set out_dir "build"
file mkdir $out_dir
# Read sources
read_verilog $src_file
read_xdc $xdc_file
# Synthesis
synth_design -top $top_name -part $part_name
# Optional: report utilization and timing after synth
report_utilization -file $out_dir/post_synth_util.rpt
report_timing_summary -file $out_dir/post_synth_timing.rpt
# Implementation
opt_design
place_design
route_design
# Reports after implementation
report_utilization -file $out_dir/post_impl_util.rpt
report_timing_summary -file $out_dir/post_impl_timing.rpt
# Bitstream
write_bitstream -force $out_dir/basys3_counter.bit
Run the build:
cd basys3_binary_counter
vivado -mode batch -source scripts/build.tcl
If successful, you will get build/basys3_counter.bit and timing/utilization reports in the build/ directory.
Program the FPGA from the CLI
Ensure the board is connected and powered, then create scripts/program.tcl:
# File: scripts/program.tcl
# Program Basys 3 with generated bitstream using Vivado 2023.2
set bitfile "build/basys3_counter.bit"
open_hw
connect_hw_server -url 127.0.0.1:3121
open_hw_target
# Select the first xc7a35t device found (Basys 3)
set devs [get_hw_devices xc7a35t*]
if {[llength $devs] == 0} {
puts "ERROR: No xc7a35t device found. Is the board connected and powered?"
exit 1
}
current_hw_device [lindex $devs 0]
refresh_hw_device -update_hw_probes false [current_hw_device]
# Program
set_property PROGRAM.FILE $bitfile [current_hw_device]
program_hw_devices [current_hw_device]
# Clean up
close_hw_target
disconnect_hw_server
close_hw
Program the board:
vivado -mode batch -source scripts/program.tcl
You should see status lines indicating the device was successfully programmed.
Step‑by‑step Validation
- Power on and program:
- Confirm the board power LED is lit.
-
Run the program Tcl script. If successful, the DONE LED (often labeled “DONE”) should assert after configuration.
-
Observe the seven‑segment display:
- The four digits should be active (some slight multiplexing dimness is normal).
- The rightmost digit (AN0) shows the LSB of the count; the leftmost (AN3) shows the MSB.
-
The display should update at roughly 2 counts per second: 0000 → 0001 → 0010 → 0011 → … → 1111 → 0000 → …
-
Confirm bit ordering:
- When you first see 0000, wait for the next transition to 0001.
- After several seconds, you’ll see 0011, 0100, etc. After ~8 seconds from 0000, expect 1000 (MSB set).
-
This confirms that digits map MSB..LSB from left to right: AN3 AN2 AN1 AN0.
-
Check decimal point and segment polarity:
- The decimal point should stay OFF (not illuminated), because we drive dp high (inactive, active‑low).
-
If the display appears inverted (e.g., you see a “-” instead of “1”), revisit the active‑low logic in the segment drive or the XDC pin mapping.
-
Timing sanity:
-
The counter rate is controlled by COUNT_RATE_HZ = 2 in the Verilog. If you measure different timing, verify the board clock is 100 MHz and that your Vivado timing constraints include the 10 ns period.
-
Multiplex refresh:
- There should be no noticeable flickering. If you see flicker, you can increase SCAN_RATE_HZ (e.g., 2000) to raise the refresh rate, but ensure it remains comfortably below the clock division resolution limit.
Pin Mapping Summary and Active Levels
The following table summarizes the key connections used by this design. All I/Os are LVCMOS33.
| Signal (Top Port) | Basys 3 Net | FPGA Pin | Direction | Active Level | Notes |
|---|---|---|---|---|---|
| clk | CLK100MHZ | W5 | Input | – | 100 MHz on‑board oscillator |
| an[0] | AN0 | U2 | Output | Low | Enable rightmost digit |
| an[1] | AN1 | U4 | Output | Low | Enable second digit from right |
| an[2] | AN2 | V4 | Output | Low | Enable third digit from right |
| an[3] | AN3 | W4 | Output | Low | Enable leftmost digit |
| seg_a | CA | W7 | Output | Low | Segment a |
| seg_b | CB | W6 | Output | Low | Segment b |
| seg_c | CC | U8 | Output | Low | Segment c |
| seg_d | CD | V8 | Output | Low | Segment d |
| seg_e | CE | U5 | Output | Low | Segment e |
| seg_f | CF | V5 | Output | Low | Segment f |
| seg_g | CG | U7 | Output | Low | Segment g |
| dp | DP | V7 | Output | Low | Decimal point (kept off = high) |
Troubleshooting
- Board not detected:
- Ensure the power switch is ON and the micro‑USB cable is known‑good.
- On Windows, confirm Xilinx USB drivers are installed (reinstall via Vivado if necessary).
-
Run:
vivado -mode tcl -source scripts/program.tcland check for “No xc7a35t device found.” If so, tryopen_hw; connect_hw_server; open_hw_targetin interactive Tcl console to see available devices. -
Bitstream fails timing:
- Check that your constraints include
create_clock -period 10.000on the clk port. -
Inspect build/post_impl_timing.rpt. This simple design should meet timing easily; if it does not, verify you used the correct part (xc7a35tcpg236-1).
-
Display is blank:
- If anodes are incorrectly mapped or driven high all the time, no digits will light. Confirm an outputs are active‑low and that your XDC pin assignments match.
- Ensure your constraints use the exact port names from the Verilog (case‑sensitive).
-
Verify that dp is set high (inactive). A stuck‑low dp can cause unexpected brightness but should not blank digits.
-
Display shows wrong segments:
- Cross‑check the CA..CG pins and ensure seg_a..seg_g map exactly to W7, W6, U8, V8, U5, V5, U7 respectively.
-
Remember: For common‑anode, segments are active‑low; a 0 lights the segment.
-
Visible flicker:
- Increase SCAN_RATE_HZ to 2000 or 4000, regenerate bitstream, and reprogram. Keep in mind this increases the divider resolution (still trivial at 100 MHz).
-
Ensure only one anode is active at a time.
-
Counter runs too fast/slow:
-
Adjust COUNT_RATE_HZ in Verilog. For example, set to 1 for a 1 Hz update. Rebuild and re‑flash.
-
Programming fails with “CRC error” or similar:
- Try power cycling the board and reprogramming.
- Confirm your bitstream path is correct in scripts/program.tcl and you’re not accidentally using a stale file.
Improvements
- Display hexadecimal instead of binary:
-
Replace the “0/1” encoding with a full 0..F hex decoder and show the nibble value on a single digit (e.g., always AN0). Or show the hex on all four digits (mirrored) to increase brightness.
-
Show larger counters:
- Expand to an 8‑bit or 16‑bit counter and scroll the bits across digits.
-
Use both “0/1” patterns and add a moving “dp” indicator to visualize bit groupings.
-
Add user control:
- Use an on‑board push button (e.g., BTNC) as reset or pause. Debounce the button in logic.
-
Use slide switches to select counting direction or speed.
-
Better timing:
- Introduce a clock enable derived from a single wide counter instead of explicit equality comparisons for higher synthesis efficiency.
-
Add an MMCM/PLL to derive a precise refresh clock if you later integrate complex logic.
-
Power/burn‑in diagnostics:
- Add a startup animation to validate all segments and anodes before entering counter mode.
- Provide a UART or LEDs mirror of the current count for debugging.
Checklist
- Prerequisites:
- Vivado WebPACK 2023.2 installed and on PATH
-
Board connected via micro‑USB and powered ON
-
Files created:
- src/top_basys3_counter.v (Verilog)
- constraints/basys3_7seg.xdc (pin mapping and clock)
- scripts/build.tcl (non‑project build)
-
scripts/program.tcl (program device)
-
Commands executed:
- Build:
vivado -mode batch -source scripts/build.tcl -
Program:
vivado -mode batch -source scripts/program.tcl -
Hardware validation:
- Four seven‑segment digits display a 4‑bit binary count in “0/1” symbols
- Count updates ~2 times per second from 0000 to 1111 and wraps
-
Decimal point remains off
-
If issues arise:
- Verify XDC port names and pins
- Confirm active‑low logic for anodes and segments
- Check timing and part selection (xc7a35tcpg236-1)
- Rebuild and re‑flash after any change
With these steps, you have a working binary counter on the Basys 3 seven‑segment display using a clean, reproducible CLI flow in Vivado. This forms a solid foundation to build richer display logic (e.g., hex/decimal decoding, animations) and to integrate user inputs, all while reinforcing clock division, multiplexing, and constraints fundamentals on an Artix‑7 FPGA.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



