Objective and use case
What you’ll build: A simple LED blinker on the Basys 3 FPGA using a clock divider to convert the 100 MHz clock into a visible blink on LED0.
Why it matters / Use cases
- Demonstrates the fundamentals of FPGA programming and hardware design.
- Provides a practical introduction to using Verilog for hardware description.
- Illustrates the importance of clock management in digital circuits.
- Serves as a foundation for more complex projects involving sensors and communication protocols.
Expected outcome
- LED0 blinks at a visible rate, confirming successful clock division.
- Completion of the project within 1 hour, demonstrating efficiency.
- Validation of design through simulation and programming on the Basys 3 board.
- Understanding of XDC constraints and their role in FPGA design.
Audience: Beginners in FPGA programming; Level: Introductory
Architecture/flow: One Verilog source file, one XDC constraint file, and two Tcl scripts for programming.
Practical Case: LED Blink with Clock Divider on Basys 3 (Xilinx Artix-7)
This beginner-friendly, hands-on exercise guides you through building a simple LED blinker on the Basys 3 (Xilinx Artix-7) FPGA board. You will implement a clock divider that turns the 100 MHz onboard clock into a human-visible blink on LED0, then build, program, and validate the design using the Vivado WebPACK command-line tools.
The flow is entirely non-GUI, reproducible, and minimal: one Verilog source file, one constraint file (XDC), and two short Tcl scripts. Expect to complete the entire project in under an hour.
Prerequisites
- Operating system:
- Linux (Ubuntu 20.04/22.04 or similar) or Windows 10/11
- Xilinx Vivado WebPACK installed with Artix-7 device support:
- Version: Vivado WebPACK 2023.2
- Confirm installation:
- Linux: /opt/Xilinx/Vivado/2023.2/bin/vivado
- Windows: C:\Xilinx\Vivado\2023.2\bin\vivado.bat
- USB drivers for Digilent USB-JTAG:
- Windows: Installed during Vivado setup (Digilent Cable Drivers)
- Linux: Install cable drivers from Vivado package (see Troubleshooting)
- One available USB port
- Basic familiarity with a shell or Command Prompt
Verify Vivado is accessible:
– Linux:
/opt/Xilinx/Vivado/2023.2/bin/vivado -version
– Windows (PowerShell):
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -version
Expected output includes “Vivado v2023.2 (64-bit) …” and the copyright line.
Materials (with exact model)
- FPGA board: Basys 3 (Xilinx Artix-7), Digilent part number: 410-183
- FPGA device: xc7a35t-1cpg236 (Vivado part code: xc7a35tcpg236-1)
- Onboard clock: 100 MHz system oscillator
- Micro-USB cable (data-capable), connected to the Basys 3 “PROG/UART” micro-USB port
- Host PC with Vivado WebPACK 2023.2
Setup/Connection
- Power off the Basys 3 (slide the power switch to OFF).
- Connect the micro-USB cable from the PC to the Basys 3 “PROG/UART” micro-USB connector.
- Switch Basys 3 power to ON. The board should power from USB; the power LED (ON) should light.
- Ensure programming mode is set to JTAG (default on Basys 3; no extra jumpers required for typical use).
- Leave all onboard switches in their default positions; this project only uses the 100 MHz clock input and LED0.
No external jumper wires are used in this project; all resources are on-board.
Pin Mapping and Constraints Overview
We will use only two FPGA pins: the 100 MHz clock and LED0. The mapping below is specific to Basys 3.
| Signal (Top Port) | Resource (Basys 3) | Package Pin | I/O Standard | Notes |
|---|---|---|---|---|
| CLK100MHZ | 100 MHz clock | W5 | LVCMOS33 | Primary system clock |
| LED0 | LED LD0 | U16 | LVCMOS33 | Active-high user LED |
We will constrain these in an XDC file and define the 100 MHz timing constraint (10.000 ns period).
Full Code
We’ll create a minimal project with this directory structure (you can place it anywhere; the example uses a Linux home directory):
- ~/fpga/basys3_led_blink/
- src/top.v
- constr/basys3_led_blink.xdc
- tcl/build.tcl
- tcl/program.tcl
Create the directories, then add the following files.
1) Verilog top-level (src/top.v)
This module divides the 100 MHz clock to make LED0 toggle every 0.5 seconds (i.e., LED blinks at 1 Hz: 0.5 s on, 0.5 s off). A 26-bit counter is sufficient to count up to 50,000,000−1.
// File: src/top.v
// Target: Basys 3 (Xilinx Artix-7)
// Function: Blink LED0 at ~1 Hz by dividing 100 MHz clock.
module top (
input wire CLK100MHZ, // 100 MHz onboard clock (pin W5)
output reg LED0 // LED LD0 (pin U16), active-high
);
// Divide 100 MHz to 1 Hz blink:
// We want LED0 to toggle every 0.5 seconds => 0.5 s * 100,000,000 = 50,000,000 cycles.
localparam integer DIV_COUNT = 50_000_000; // half-period in clock cycles
// 50,000,000 fits within 26 bits (2^26 = 67,108,864)
reg [25:0] counter = 26'd0;
// Initialize LED0 low (Xilinx allows register init at configuration time)
initial LED0 = 1'b0;
always @(posedge CLK100MHZ) begin
if (counter == DIV_COUNT - 1) begin
counter <= 26'd0;
LED0 <= ~LED0; // toggle every 0.5 s (1 Hz blink)
end else begin
counter <= counter + 1'b1;
end
end
endmodule
Notes:
– This design uses synchronous logic only.
– No reset is required; the initial values are sufficient for this basic case.
2) Constraints (constr/basys3_led_blink.xdc)
Constrain the clock and LED pin for the Basys 3 and define the clock’s timing requirement. These settings are standard for LVCMOS33 on user I/O.
# Basys 3 (Xilinx Artix-7) constraints for LED blink
# Clock: 100 MHz system oscillator on pin W5
set_property PACKAGE_PIN W5 [get_ports {CLK100MHZ}]
set_property IOSTANDARD LVCMOS33 [get_ports {CLK100MHZ}]
create_clock -name sys_clk -period 10.000 [get_ports {CLK100MHZ}]
# LED0: LD0 on pin U16
set_property PACKAGE_PIN U16 [get_ports {LED0}]
set_property IOSTANDARD LVCMOS33 [get_ports {LED0}]
set_property DRIVE 8 [get_ports {LED0}]
set_property SLEW SLOW [get_ports {LED0}]
Build/Flash/Run Commands
We will use pure Vivado CLI (batch mode) with Tcl scripts for project creation, synthesis, implementation, bitstream generation, and programming.
Directory layout recap:
– src/top.v
– constr/basys3_led_blink.xdc
– tcl/build.tcl
– tcl/program.tcl
1) Build script (tcl/build.tcl)
This script creates a new project targeting the exact Artix-7 part on Basys 3, adds sources/constraints, runs synthesis and implementation, and writes the bitstream.
# File: tcl/build.tcl
# Vivado 2023.2 batch build for Basys 3 LED blink
# Set a reproducible project directory (adjust as needed)
set proj_dir [file normalize [file join [pwd] ".."]]
set proj_name "basys3_led_blink"
set proj_path [file join $proj_dir $proj_name]
# Clean prior runs if desired (optional)
if { [file exists $proj_path] } {
puts "INFO: Project directory already exists: $proj_path"
}
# Create project
create_project $proj_name $proj_path -part xc7a35tcpg236-1 -force
# Add design sources and constraints
add_files -fileset sources_1 [file normalize [file join $proj_dir "src" "top.v"]]
add_files -fileset constrs_1 [file normalize [file join $proj_dir "constr" "basys3_led_blink.xdc"]]
# Ensure correct compilation order
update_compile_order -fileset sources_1
# Elaborate (optional check)
# synth_design will elaborate and synthesize automatically; we proceed.
# Run synthesis and implementation to bitstream
launch_runs synth_1 -jobs 4
wait_on_run synth_1
launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1
# Write useful reports (timing/utilization)
open_run impl_1
report_timing_summary -file [file join $proj_path "timing_summary_impl.txt"] -delay_type max -max_paths 10
report_utilization -file [file join $proj_path "utilization_impl.txt"]
# Print bitstream location
set bitfile [file join $proj_path "$proj_name.runs" "impl_1" "top.bit"]
if { [file exists $bitfile] } {
puts "INFO: Bitstream ready: $bitfile"
} else {
puts "ERROR: Bitstream not found where expected."
exit 1
}
exit
2) Programming script (tcl/program.tcl)
This script connects to the local hardware server, detects the Basys 3 over USB-JTAG, and programs the bitstream.
# File: tcl/program.tcl
# Program Basys 3 over JTAG in Vivado 2023.2
# Locate the bitstream produced by build.tcl
set proj_dir [file normalize [file join [pwd] ".."]]
set proj_name "basys3_led_blink"
set bitfile [file join $proj_dir $proj_name "$proj_name.runs" "impl_1" "top.bit"]
if { ! [file exists $bitfile] } {
puts "ERROR: Bitstream not found: $bitfile"
exit 1
}
open_hw
connect_hw_server -url localhost:3121
# Open the local JTAG target
open_hw_target
# Select the first detected device (Basys 3: xc7a35t)
set devs [get_hw_devices]
if { [llength $devs] == 0 } {
puts "ERROR: No hardware devices found. Check USB, power, drivers."
exit 1
}
current_hw_device [lindex $devs 0]
refresh_hw_device -update_hw_probes false [current_hw_device]
# Program the FPGA
set_property PROGRAM.FILE $bitfile [current_hw_device]
program_hw_devices [current_hw_device]
puts "INFO: Programmed device with: $bitfile"
close_hw_target
disconnect_hw_server
close_hw
exit
3) Running the commands
Use the appropriate Vivado binary path for your OS. Adjust the working directory to inside the tcl folder.
-
Linux:
cd ~/fpga/basys3_led_blink/tcl
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source build.tcl
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source program.tcl -
Windows (PowerShell):
cd $HOME\fpga\basys3_led_blink\tcl
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source build.tcl
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source program.tcl
If successful, LED0 (LD0) on the Basys 3 will blink at approximately 1 Hz.
Step‑by‑step Validation
- Visual check:
- Confirm Basys 3 is powered (ON LED lit).
-
After programming completes, observe LED0 (the leftmost LED labeled LD0) blinking on and off at about once per second.
-
Timing sanity check:
- The 100 MHz clock has a 10 ns period.
- The design counts 50,000,000 cycles before toggling LED0.
- 50,000,000 cycles × 10 ns = 0.5 s per toggle, so 1 full period (on+off) = 1.0 s.
-
Use a stopwatch or phone timer to crudely verify: count 10 blinks; it should take ~10 seconds.
-
Adjust blink rate for quick verification (optional test):
- Edit src/top.v: change
DIV_COUNTto 5_000_000 (10× faster). - Re-run build and program commands.
-
LED should now blink at 10 Hz (faster flash). This confirms the divider is effective.
-
Basic functional assertions:
- LED must be dark or lit at power-up but change state every 0.5 s after programming.
-
If LED is steady or erratic, revisit constraints and ensure the correct clock pin W5 is used.
-
Verify timing status:
- Open the generated timing report:
- Linux:
less ~/fpga/basys3_led_blink/basys3_led_blink/timing_summary_impl.txt - Look for “Design Timing Summary” and “All user specified timing constraints were met.”
- Linux:
- For this simple design, there should be no timing violations at 100 MHz.
Troubleshooting
- Board not detected (no devices in program.tcl):
- Check USB cable and try a different port.
- Make sure the board is powered (switch ON).
- Windows: Open Device Manager; “Digilent USB Device” should appear. Reinstall Digilent Cable Drivers via Vivado if needed.
-
Linux: Install cable drivers and replug:
sudo /opt/Xilinx/Vivado/2023.2/data/xicom/cable_drivers/lin64/install_script/install_drivers
Then re-run program.tcl. -
Bitstream not found:
- Ensure you ran build.tcl first.
- Verify the path in program.tcl matches your project structure.
-
Check for typos in directory names (src, constr, tcl).
-
LED not blinking:
- Confirm the LED pin (U16) and clock pin (W5) constraints match the Basys 3 board.
- Open the utilization report to ensure I/O ports are correctly bound (LED0 and CLK100MHZ should be listed).
- Verify the LED is not obscured or stuck by any other design (there is only one top.v).
-
If you edited the code, ensure the counter comparison is
DIV_COUNT - 1and the counter resets to 0 on match. -
Wrong device selected:
- The Basys 3 uses part xc7a35tcpg236-1. Ensure build.tcl uses
-part xc7a35tcpg236-1. -
Using a different part can cause constraint mismatches or implementation failures.
-
Timing warnings:
-
The design is simple; any timing failures likely indicate wrong clock constraint. Make sure
create_clock -period 10.000 [get_ports {CLK100MHZ}]is in the .xdc. -
Permission issues on Linux:
-
If Vivado cannot access the USB device, check user permissions or run vivado with sudo (not recommended generally). Prefer installing correct udev rules via the driver install script shown above.
-
Multiple Vivado versions installed:
- Explicitly call Vivado 2023.2 binary path in commands to avoid PATH ambiguity.
Improvements
Once the basic LED blink works, consider the following incremental enhancements:
- Parameterized Divider:
- Replace the fixed
DIV_COUNTwith a parameter and allow selection via synthesis parameters or switches. - For example, map two switches (SW0–SW1) to scale the divider for different blink rates.
-
You can extend the constraints file to include those switches. Basys 3 slide switches are easily constrained if you add their pins (refer to Digilent’s Basys 3 master XDC).
-
Wider LED Patterns:
- Drive multiple LEDs with different divider tap points (e.g., use bits of a free-running counter) to create a “binary counter” display across all 16 LEDs.
-
This is a good way to check clock correctness and confirm that each LED pin is functioning.
-
Reset/Enable Logic:
- Add a synchronous reset input (e.g., from BTNC) to clear the counter.
-
Implement an enable signal to pause/resume blinking.
-
Use Xilinx Clocking Wizard:
-
Generate a derived clock (e.g., 1 MHz or 10 MHz) via MMCM/PLL using the Clocking Wizard IP, and then build your divider from that slowed clock for finer control.
-
Integrate a Testbench and Run Simulation:
- Create a simple testbench that toggles CLK100MHZ at 100 MHz (10 ns period) for a few millisecond-equivalents and asserts LED0 toggles at the expected intervals.
-
Use xsim for command-line simulation and waveform capture.
-
Hardware Debugging with an ILA:
-
For more advanced debugging, add an Integrated Logic Analyzer (ILA) core to observe the counter and LED toggle in hardware.
-
Packaging:
- Create a single Tcl script that both builds and programs, accepting parameters for
DIV_COUNTor output names, improving repeatability.
Final Checklist
Use this checklist to confirm you have completed each step correctly.
- Prerequisites
- Vivado WebPACK 2023.2 is installed and accessible from CLI.
- Digilent USB-JTAG drivers installed (Windows) or cable drivers installed (Linux).
-
You can run
vivado -versionsuccessfully. -
Materials
- Basys 3 (Xilinx Artix-7) connected via micro-USB to the PROG/UART port.
-
Board power switch set to ON.
-
Directory Structure
- Created folders: src, constr, tcl in your project directory.
-
Files present:
- src/top.v
- constr/basys3_led_blink.xdc
- tcl/build.tcl
- tcl/program.tcl
-
Code and Constraints
- top.v includes a 26-bit counter dividing the 100 MHz clock to generate a 1 Hz blink on LED0.
-
XDC file maps CLK100MHZ to W5 and LED0 to U16, with proper IOSTANDARD and create_clock.
-
Build
- Ran build script:
- Linux:
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source tcl/build.tcl - Windows:
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source tcl/build.tcl
- Linux:
- Bitstream generated without critical errors.
-
Timing summary shows constraints met.
-
Program
- Ran program script:
- Linux:
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source tcl/program.tcl - Windows:
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source tcl/program.tcl
- Linux:
-
Device detected and programmed successfully.
-
Validation
- LED0 blinks visibly at approximately 1 Hz.
-
Optional: adjusted divider and confirmed blink rate changes accordingly.
-
Troubleshooting (if needed)
- Verified connections, drivers, correct part number, and accurate pin constraints.
If every item is checked, you have successfully completed a basic FPGA project on the Basys 3 (Xilinx Artix-7), using Vivado WebPACK CLI, achieving the objective “led-blink-with-clock-divider.” This foundation prepares you for more complex designs involving multiple I/Os, peripherals, and clocking strategies.
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.



