Objective and use case
What you’ll build: You will generate a 640×480 VGA test pattern on the Basys 3 FPGA board using Verilog. This practical implementation will allow you to display vertical color bars on a VGA monitor.
Why it matters / Use cases
- Testing video output capabilities of the Basys 3 FPGA in educational settings.
- Providing a visual reference for verifying the functionality of VGA output in embedded systems.
- Utilizing the VGA color bars as a diagnostic tool for monitor calibration.
- Demonstrating the integration of digital design and hardware implementation in FPGA projects.
Expected outcome
- Successful display of a 640×480 resolution at 60 Hz on a VGA monitor.
- Clear and distinct vertical color bars indicating proper color channel output (12-bit DAC).
- Measured pixel clock frequency of 25.000 MHz, derived from a 100 MHz source.
- Low latency in signal output with minimal flicker during display.
Audience: FPGA developers, students; Level: Intermediate
Architecture/flow: Verilog implementation driving a 12-bit VGA DAC on Basys 3 FPGA, utilizing Vivado for synthesis and deployment.
Hands-on Practical Case: VGA 640×480 Color Bars on Basys 3 (Xilinx Artix‑7)
This hands-on guide walks you through generating a clean 640×480@60 Hz VGA test pattern (vertical color bars) on the Digilent Basys 3 FPGA board (Xilinx Artix‑7). You will implement the VGA timing in Verilog, drive the on-board 12-bit VGA DAC (4 bits per color channel), and deploy using Vivado WebPACK from the command line.
The focus is practical and reproducible: you will get a known-good color bar test pattern on a VGA monitor, with exact build and program commands.
Prerequisites
- Basic familiarity with:
- Reading Verilog code (always blocks, parameters, modules)
- Using command-line tools
- A workstation with:
- Xilinx Vivado WebPACK 2023.2 installed
- Linux default path: /opt/Xilinx/Vivado/2023.2/bin/vivado
- Windows default path: C:\Xilinx\Vivado\2023.2\bin\vivado.bat
- USB drivers for Digilent cable (adequate when Vivado is properly installed)
- Internet access to fetch the Basys-3 master constraints file (XDC)
Notes:
– We will use a 25.000 MHz pixel clock (from 100 MHz ÷ 4). Most monitors accept this slight deviation from the nominal 25.175 MHz. If your monitor complains, see Improvements for an exact 25.175 MHz clock option using an MMCM/Clocking Wizard.
Materials
- FPGA board: Basys 3 (Xilinx Artix‑7) — exact model: Digilent Basys 3 with XC7A35T-1CPG236C
- VGA cable: Standard HD-15 male-to-male
- Monitor capable of 640×480@60 Hz (virtually any VGA-compatible monitor)
- USB Micro-B cable (for power and JTAG programming of the Basys 3)
- Host machine with Vivado WebPACK 2023.2
Setup/Connection
The Basys 3 has an on-board 15-pin VGA connector and an on-board 12-bit resistor DAC connected to FPGA pins. No breadboard wiring is needed.
- Physical connections:
- Connect a VGA cable from the Basys 3 VGA port to your monitor’s VGA input.
- Connect a USB Micro-B cable from your PC to the Basys 3 micro-USB port (JTAG and power).
-
Set the Basys 3 power switch to ON.
-
Board-level signals used (internal to Basys 3 hardware):
- VGA_HS (horizontal sync), VGA_VS (vertical sync)
-
VGA_R[3:0], VGA_G[3:0], VGA_B[3:0] (4-bit per color channel)
-
Timing standard targeted:
- 640×480@60 Hz (approx 25.175 MHz pixel clock, we’ll use 25.000 MHz)
VGA Timing Overview (640×480@60 Hz)
We adhere to the standard “VGA 640×480@60 Hz” timing (negative sync polarity). This can be summarized as:
- Pixel clock: 25.175 MHz nominal (we’ll use 25.000 MHz)
- Horizontal timing (total 800 pixels per line):
- Visible: 640
- Front porch: 16
- Sync pulse: 96 (HSYNC active low)
- Back porch: 48
- Vertical timing (total 525 lines per frame):
- Visible: 480
- Front porch: 10
- Sync pulse: 2 (VSYNC active low)
- Back porch: 33
Timing Table
| Parameter | Value | Notes |
|---|---|---|
| Pixel clock | 25.000 MHz (approx) | Nominal is 25.175 MHz; most monitors accept 25.0 MHz |
| H visible | 640 | Active pixels per line |
| H front porch | 16 | |
| H sync pulse | 96 | Active low |
| H back porch | 48 | |
| H total | 800 | 640 + 16 + 96 + 48 |
| V visible | 480 | Active lines per frame |
| V front porch | 10 | |
| V sync pulse | 2 | Active low |
| V back porch | 33 | |
| V total | 525 | 480 + 10 + 2 + 33 |
We will generate 8 vertical color bars, each 80 pixels wide (640 / 8), across the active display area.
Project Structure
Create this directory layout:
- basys3_vga_colorbars/
- src/rtl/
- top_basys3_vga.v
- vga_timing.v
- clock_divider.v
- constraints/
- Basys-3-Master.xdc
- scripts/
- build.tcl
- program.tcl
- build/ (generated by Vivado)
Full Code
1) clock_divider.v
A simple clock divider to derive 25 MHz from the 100 MHz on-board clock. This is sufficient for most displays.
// File: src/rtl/clock_divider.v
// Divides 100 MHz input to 25 MHz output by dividing by 4.
module clock_divider (
input wire clk_in_100mhz,
input wire resetn,
output reg clk_out_25mhz
);
reg [1:0] div_cnt;
always @(posedge clk_in_100mhz or negedge resetn) begin
if (!resetn) begin
div_cnt <= 2'd0;
clk_out_25mhz <= 1'b0;
end else begin
div_cnt <= div_cnt + 2'd1;
if (div_cnt == 2'd1) begin
clk_out_25mhz <= 1'b1;
end else if (div_cnt == 2'd3) begin
clk_out_25mhz <= 1'b0;
end
end
end
endmodule
2) vga_timing.v
Generates hsync, vsync, and pixel position (hcount, vcount) with an “active video” region flag.
// File: src/rtl/vga_timing.v
// VGA 640x480@60 timing generator, negative sync polarity.
module vga_timing #(
parameter H_VISIBLE = 640,
parameter H_FP = 16,
parameter H_SYNC = 96,
parameter H_BP = 48,
parameter V_VISIBLE = 480,
parameter V_FP = 10,
parameter V_SYNC = 2,
parameter V_BP = 33
)(
input wire clk_pix, // pixel clock ~25 MHz
input wire resetn,
output reg [9:0] hcount, // 0..799
output reg [9:0] vcount, // 0..524
output reg hsync, // active low
output reg vsync, // active low
output wire active // high during visible area
);
localparam H_TOTAL = H_VISIBLE + H_FP + H_SYNC + H_BP; // 800
localparam V_TOTAL = V_VISIBLE + V_FP + V_SYNC + V_BP; // 525
wire end_of_line = (hcount == H_TOTAL - 1);
wire end_of_frame = (vcount == V_TOTAL - 1);
assign active = (hcount < H_VISIBLE) && (vcount < V_VISIBLE);
always @(posedge clk_pix or negedge resetn) begin
if (!resetn) begin
hcount <= 10'd0;
vcount <= 10'd0;
end else begin
if (end_of_line) begin
hcount <= 10'd0;
if (end_of_frame)
vcount <= 10'd0;
else
vcount <= vcount + 10'd1;
end else begin
hcount <= hcount + 10'd1;
end
end
end
// Generate hsync and vsync (active low)
wire hsync_region = (hcount >= (H_VISIBLE + H_FP)) &&
(hcount < (H_VISIBLE + H_FP + H_SYNC));
wire vsync_region = (vcount >= (V_VISIBLE + V_FP)) &&
(vcount < (V_VISIBLE + V_FP + V_SYNC));
always @(posedge clk_pix or negedge resetn) begin
if (!resetn) begin
hsync <= 1'b1; // inactive high
vsync <= 1'b1; // inactive high
end else begin
hsync <= ~hsync_region;
vsync <= ~vsync_region;
end
end
endmodule
3) top_basys3_vga.v
Top-level module with ports named to match the Digilent Basys-3 Master XDC for zero-edit pin assignment. It instantiates the clock divider and timing generator, and produces 8 vertical color bars.
// File: src/rtl/top_basys3_vga.v
// Basys 3 (Xilinx Artix-7) VGA color bars at 640x480 using 25 MHz pixel clock.
module top_basys3_vga (
input wire CLK100MHZ, // Matches Basys-3 Master XDC signal name
input wire CPU_RESETN, // Use center pushbutton as active-low reset if desired
output wire VGA_HS,
output wire VGA_VS,
output wire [3:0] VGA_R,
output wire [3:0] VGA_G,
output wire [3:0] VGA_B
);
// Optional: tie CPU_RESETN to '1' if not using button (depends on XDC mapping).
wire resetn = CPU_RESETN;
// Generate ~25 MHz pixel clock from 100 MHz
wire clk_pix;
clock_divider u_div (
.clk_in_100mhz (CLK100MHZ),
.resetn (resetn),
.clk_out_25mhz (clk_pix)
);
// VGA timing
wire [9:0] hcount;
wire [9:0] vcount;
wire active;
vga_timing #(
.H_VISIBLE(640), .H_FP(16), .H_SYNC(96), .H_BP(48),
.V_VISIBLE(480), .V_FP(10), .V_SYNC(2), .V_BP(33)
) u_tmg (
.clk_pix (clk_pix),
.resetn (resetn),
.hcount (hcount),
.vcount (vcount),
.hsync (VGA_HS),
.vsync (VGA_VS),
.active (active)
);
// Generate 8 vertical color bars across 640 pixels (each stripe 80 px)
// Bar order (left to right):
// White, Yellow, Cyan, Green, Magenta, Red, Blue, Black
// Each channel is 4-bit (0..15).
reg [3:0] r, g, b;
always @* begin
if (!active) begin
r = 4'd0;
g = 4'd0;
b = 4'd0;
end else begin
if (hcount < 10'd80) begin r=4'd15; g=4'd15; b=4'd15; end // White
else if (hcount < 10'd160) begin r=4'd15; g=4'd15; b=4'd0; end // Yellow
else if (hcount < 10'd240) begin r=4'd0; g=4'd15; b=4'd15; end // Cyan
else if (hcount < 10'd320) begin r=4'd0; g=4'd15; b=4'd0; end // Green
else if (hcount < 10'd400) begin r=4'd15; g=4'd0; b=4'd15; end // Magenta
else if (hcount < 10'd480) begin r=4'd15; g=4'd0; b=4'd0; end // Red
else if (hcount < 10'd560) begin r=4'd0; g=4'd0; b=4'd15; end // Blue
else begin r=4'd0; g=4'd0; b=4'd0; end // Black
end
end
assign VGA_R = r;
assign VGA_G = g;
assign VGA_B = b;
endmodule
Notes:
– The top-level port names (CLK100MHZ, VGA_HS, VGA_VS, VGA_R, VGA_G, VGA_B) are intentionally matched to the Digilent Basys-3 Master XDC to reduce pin assignment friction.
– CPU_RESETN is also a standard name from the Master XDC (tied to the center pushbutton). You may hold it high by uncommenting the corresponding constraint; or temporarily tie resetn to 1’b1 in RTL for a quick test.
Constraints (XDC)
Use the official Digilent Basys-3 Master XDC for reliable pin mapping. Do not guess pins.
1) Download the Basys-3 Master XDC:
– URL: https://github.com/Digilent/digilent-xdc/blob/master/Basys-3-Master.xdc
– Save as: constraints/Basys-3-Master.xdc
2) In that XDC file, do the following:
– Search for lines that define:
– CLK100MHZ (100 MHz system clock)
– CPU_RESETN (active-low center button, optional)
– VGA_HS, VGA_VS
– VGA_R[3:0], VGA_G[3:0], VGA_B[3:0]
– Uncomment those lines so that Vivado applies the pin mappings.
– Ensure the port names match your top module exactly (this guide uses the Master XDC names to avoid edits).
3) (Optional) Add a timing constraint for the input clock in a small local XDC (if desired):
– Not strictly necessary since the Master XDC usually includes a proper clock constraint. If you wish to add your own, create constraints/local.xdc with:
– create_clock -name sys_clk -period 10.000 [get_ports {CLK100MHZ}]
We will rely on the Master XDC for all pin locations, voltage standards (LVCMOS33), and required I/O attributes.
Build/Flash/Run Commands
The following assumes Linux paths. Windows equivalents are provided.
Recommended directory layout:
– basys3_vga_colorbars/
– src/rtl/*.v
– constraints/Basys-3-Master.xdc
– scripts/build.tcl
– scripts/program.tcl
scripts/build.tcl
# Vivado 2023.2 non-project batch flow for Basys 3 VGA color bars.
set PART "xc7a35tcpg236-1"
set TOP "top_basys3_vga"
set BASE_DIR [file normalize [file join [pwd] ".."]]
# If this script is invoked from scripts/, BASE_DIR is project root.
# Create a project in build/ for logs/artifacts
file mkdir $BASE_DIR/build
cd $BASE_DIR/build
create_project basys3_vga_colorbars . -part $PART -force
# Add sources
add_files -fileset sources_1 [file normalize "$BASE_DIR/src/rtl/clock_divider.v"]
add_files -fileset sources_1 [file normalize "$BASE_DIR/src/rtl/vga_timing.v"]
add_files -fileset sources_1 [file normalize "$BASE_DIR/src/rtl/top_basys3_vga.v"]
# Add constraints (Digilent Master XDC, downloaded previously)
add_files -fileset constrs_1 [file normalize "$BASE_DIR/constraints/Basys-3-Master.xdc"]
# Set top
set_property top $TOP [current_fileset]
# Synthesis
launch_runs synth_1 -jobs 4
wait_on_run synth_1
# Implementation
launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1
# Copy bitstream to a predictable location
file copy -force [glob *.runs/impl_1/*.bit] "$BASE_DIR/build/basys3_vga_colorbars.bit"
puts "Build complete. Bitstream: $BASE_DIR/build/basys3_vga_colorbars.bit"
scripts/program.tcl
# File: scripts/program.tcl
# Program Basys 3 over JTAG using the generated bitstream.
set BASE_DIR [file normalize [file join [pwd] ".."]]
set BITFILE [file normalize "$BASE_DIR/build/basys3_vga_colorbars.bit"]
open_hw
connect_hw_server
open_hw_target
# Select the first Artix-7 35T device
set dev [lindex [get_hw_devices xc7a35t*] 0]
current_hw_device $dev
refresh_hw_device $dev
set_property PROGRAM.FILE $BITFILE $dev
program_hw_devices $dev
refresh_hw_device $dev
close_hw
puts "Programming done."
Build and Program (Linux)
# Verify Vivado version
/opt/Xilinx/Vivado/2023.2/bin/vivado -version
# From project root
cd basys3_vga_colorbars
# Build (synthesis, place/route, bitstream)
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source scripts/build.tcl
# Program the FPGA (Basys 3 must be plugged in and powered)
/opt/Xilinx/Vivado/2023.2/bin/vivado -mode batch -source scripts/program.tcl
Build and Program (Windows PowerShell)
# Verify Vivado version
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -version
# From project root
cd basys3_vga_colorbars
# Build
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source scripts\build.tcl
# Program
& "C:\Xilinx\Vivado\2023.2\bin\vivado.bat" -mode batch -source scripts\program.tcl
Step-by-step Validation
Follow these steps to confirm the design is producing a proper VGA signal with color bars:
1) Cable and Power
– Connect a VGA cable from Basys 3 to the monitor.
– Connect the USB Micro-B cable to the Basys 3 and your PC.
– Turn the Basys 3 power switch ON.
– Ensure the monitor input is set to VGA/Input 1, etc.
2) Constraints Setup
– Verify constraints/Basys-3-Master.xdc is present in the project.
– Open it in a text editor and ensure the lines for:
– CLK100MHZ
– VGA_HS, VGA_VS
– VGA_R[3:0], VGA_G[3:0], VGA_B[3:0]
– (Optionally CPU_RESETN)
are uncommented. Confirm the port names match the RTL exactly.
3) Build Success
– Run the build command. Confirm that:
– Synthesis completes without critical errors.
– Implementation completes without unrouted nets.
– Bitstream is generated at build/basys3_vga_colorbars.bit.
4) Program the FPGA
– Run the program command. The Vivado console should show “Programming done.”
– If multiple Xilinx devices are attached, disconnect others or adjust the program.tcl to pick the correct device.
5) Monitor Display
– Within a second or two, your monitor should show 8 vertical bars:
– Left to right: White, Yellow, Cyan, Green, Magenta, Red, Blue, Black.
– The image should be stable (no jitter) and centered or close to centered.
6) Monitor Info Readout
– Most monitors have an on-screen display (OSD) info function:
– Confirm resolution is 640×480.
– Refresh rate close to 60 Hz.
– Pixel clock likely reported near 25 MHz.
7) Reset Test (Optional)
– If you enabled CPU_RESETN and mapped it to the center pushbutton:
– Press and hold the button: the screen may briefly blank (depending on reset behavior).
– Release it: the pattern should reappear stable.
8) Visual Checks
– Stripe widths: each color bar is 80 pixels; on a 640-wide resolution, that’s 8 equal-width bars.
– Colors: Ensure full-bright white, yellow, cyan, green, magenta, red, blue, and black are rendered, corresponding to 4-bit channel depths.
Troubleshooting
- No video signal on monitor:
- Confirm the Basys 3 is powered on and the monitor is on the correct input.
- Verify the constraints file (Basys-3-Master.xdc) was added to the project and that the VGA lines are uncommented.
- Ensure the top-level ports match the constraint names exactly (case-sensitive).
-
Rebuild and reprogram.
-
Monitor reports “Out of Range” or no sync:
- Some monitors are strict about pixel clock. The 25.000 MHz approximation usually works, but a few require the exact 25.175 MHz. See Improvements to generate the exact pixel clock using an MMCM (Clocking Wizard) in Vivado.
-
Double-check negative sync polarity (hsync/vsync active low). Our code sets ~hsync_region and ~vsync_region properly.
-
Wrong colors or odd color gradients:
- Verify that all 12 VGA color bits are constrained and connected. Missing bits can cause dim or incorrect colors.
- Check that the XDC maps VGA_R[3:0], VGA_G[3:0], VGA_B[3:0] correctly to the Basys 3 pins.
-
If you modified port widths or names, ensure the XDC and RTL agree.
-
Implementation DRC or unrouted nets:
-
Often indicates missing or commented-out constraints. Re-open the Master XDC and ensure the appropriate lines are uncommented.
-
JTAG programming fails:
- Close any other Vivado sessions that might be using the hardware server.
- Use a different USB port or cable.
- Power-cycle the Basys 3.
-
Ensure udev rules/drivers are correct on Linux (Vivado installer can set these up). On Windows, ensure the Digilent USB Cable drivers are installed.
-
Reset stuck asserted:
- If CPU_RESETN is constrained and your top uses it directly, it’s active-low. Ensure you’re not tying it low in constraints, and confirm the pushbutton is not pressed. You can also bypass it by hardwiring resetn to 1’b1 in RTL for quick testing.
Improvements
These are options to refine accuracy, robustness, or functionality:
- Exact 25.175 MHz pixel clock:
- Use Vivado’s Clocking Wizard (MMCME2-based) to derive exactly 25.175 MHz from 100 MHz.
- In a Vivado project, create a Clocking Wizard IP with:
- Input: 100.000 MHz
- Output: 25.175 MHz
- Instantiate the generated IP instead of the simple divider. Replace the clock_divider with the IP’s output (locked signal can be used to gate resetn).
-
This tends to resolve any picky monitor issues.
-
On-screen diagnostics:
- Overlay the hcount/vcount region or draw a border to confirm alignment.
-
Add test patterns (checkerboard, grayscale ramps, RGB gradients) to verify DAC linearity and bit order.
-
Support multiple resolutions:
- Parameterize timing for 800×600@60 (40 MHz), 1024×768@60 (65 MHz) with an appropriate MMCM.
-
Add on-board switches to choose a mode and adjust bars accordingly.
-
Debounced reset and status LEDs:
- Debounce the reset button.
-
Drive LEDs to indicate active video or sync pulses for quick visual debugging.
-
Clean clock domain crossing:
- If adding UI from buttons/switches, synchronize inputs to the pixel clock domain to avoid metastability artifacts.
Final Checklist
- Materials and Setup
- Basys 3 (Xilinx Artix‑7 XC7A35T-1CPG236C) connected over micro‑USB
- VGA cable to a monitor set to VGA input
-
Vivado WebPACK 2023.2 installed and accessible from CLI
-
Project Files
- src/rtl/clock_divider.v present
- src/rtl/vga_timing.v present
- src/rtl/top_basys3_vga.v present
- constraints/Basys-3-Master.xdc downloaded and VGA/clock lines uncommented
-
scripts/build.tcl and scripts/program.tcl created
-
Build
- Run vivado -mode batch -source scripts/build.tcl without errors
-
Confirm build/basys3_vga_colorbars.bit exists
-
Program
-
Run vivado -mode batch -source scripts/program.tcl and see “Programming done.”
-
Validation
- Monitor shows 8 vertical color bars: White, Yellow, Cyan, Green, Magenta, Red, Blue, Black
- Monitor info/OSD indicates 640×480 @ about 60 Hz
-
Stable image without flicker or rolling
-
If Issues
- Re-check the Basys-3 Master XDC line uncomments and name matching
- Consider exact pixel clock via Clocking Wizard if sync fails
- Verify cables, power, and JTAG connectivity
With this setup and code, you have a working baseline VGA 640×480 color-bar generator on the Basys 3 (Xilinx Artix‑7), built and programmed end-to-end from the Vivado command line. This is a solid foundation for more advanced video projects (sprites, text, framebuffers, and higher resolutions).
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.



