Objective and use case
What you’ll build: This project involves creating a VGA sprite engine on the Digilent Basys 3 FPGA, enabling the rendering of multiple animated sprites at a resolution of 800×600.
Why it matters / Use cases
- Developing real-time graphics applications for educational tools, enhancing learning through visual feedback.
- Creating interactive video games on FPGA platforms, allowing for custom sprite animations and backgrounds.
- Implementing hardware-based graphics processing for embedded systems, showcasing the capabilities of FPGAs in multimedia applications.
- Exploring the fundamentals of VGA timing and sprite management, crucial for understanding digital video systems.
Expected outcome
- Achieve a stable 40 MHz pixel clock with less than 5% jitter, ensuring smooth rendering of sprites.
- Render at least 10 moving sprites simultaneously with frame updates occurring every 16.67 ms.
- Maintain latency under 10 ms for sprite position updates, providing a responsive user experience.
- Utilize less than 50% of the FPGA’s logic resources, demonstrating efficient use of hardware.
Audience: FPGA developers; Level: Intermediate
Architecture/flow: The design flow includes Verilog coding for the VGA timing generator and sprite compositor, with a focus on synthesizable constructs and command-line operations in Vivado.
Advanced FPGA Practical: VGA Sprite Engine at 800×600 on Digilent Basys 3
This hands-on lab guides you through building a hardware sprite engine that renders multiple moving sprites over a procedurally generated background at 800×600 resolution on a Digilent Basys 3 FPGA board. You will implement a full VGA timing generator, a 40 MHz pixel clock using a Xilinx MMCM (Clocking Wizard), a latency-aware sprite compositor, and a simple animation loop that updates sprite positions at frame boundaries.
The flow uses Verilog (synthesizable, concise subset) and Vivado WebPACK, operated from the command line via Tcl for reproducibility.
Prerequisites
- Background:
- Comfortable with Verilog-2001 synthesizable constructs.
- Familiar with Xilinx Vivado IP integrator basics (Clocking Wizard), but we will generate it via CLI.
-
Understanding of raster scanning and VGA timings.
-
Software:
- Xilinx Vivado WebPACK 2023.2 (or later). This guide uses 2023.2.
-
Vivado board files for Basys 3 are optional but not required because we constrain pins via XDC.
-
OS:
-
Linux or Windows (commands shown are platform-agnostic Tcl scripts run via Vivado).
-
Display:
- A monitor that supports 800×600@60 Hz VGA input.
Materials
- FPGA board: Digilent Basys 3 (Model: Basys 3; FPGA: Xilinx Artix-7 XC7A35T-1CPG236C).
- Cables:
- Micro-USB cable (for JTAG/programming).
- VGA cable (DB15, male–male).
- Display:
- VGA monitor capable of 800×600@60 Hz.
- Host PC with Vivado WebPACK 2023.2.
Setup / Connection
- Connect the Basys 3 to the host PC over USB (Micro-USB).
- Connect a VGA cable from the Basys 3 VGA port to the monitor. No external breadboard wiring is required; the board’s resistor DAC drives the VGA connector.
- Power the monitor; set it to use the VGA input. Most monitors auto-detect; otherwise, manually select VGA.
VGA Mode Target and Timing
We target 800×600 at approximately 60 Hz (“SVGA 60”). The standard uses a 40.000 MHz pixel clock and positive polarity HSync/VSync. The detailed timing is:
| Parameter | Horizontal (pixels) | Vertical (lines) |
|---|---|---|
| Active | 800 | 600 |
| Front porch | 40 | 1 |
| Sync pulse | 128 | 4 |
| Back porch | 88 | 23 |
| Total | 1056 | 628 |
| Polarity | + (positive) | + (positive) |
| Derived | H freq ≈ 37.879 kHz | V freq ≈ 60.317 Hz |
Notes:
– Pixel clock = 40.000 MHz.
– H frequency = 40e6 / 1056 ≈ 37.8788 kHz.
– V frequency = H frequency / 628 ≈ 60.317 Hz.
Full Code (Verilog)
Place the following files under project directory:
- src/hdl/top_basys3_vga_sprites.v
- src/hdl/vga_timing_800x600.v
- src/hdl/sprite_engine.v
- src/hdl/sprite_bitmap.v
The clock generator IP (clk_wiz_40m) will be created by Vivado; we just instantiate it.
// File: src/hdl/vga_timing_800x600.v
// Generates 800x600@~60Hz timing with positive HS/VS, 40 MHz pixel clock.
module vga_timing_800x600 (
input wire pix_clk,
input wire resetn,
output reg [10:0] hcount, // 0..1055
output reg [10:0] vcount, // 0..627
output reg hsync, // positive polarity
output reg vsync, // positive polarity
output reg active_video
);
localparam H_ACTIVE = 800;
localparam H_FP = 40;
localparam H_SYNC = 128;
localparam H_BP = 88;
localparam H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP; // 1056
localparam V_ACTIVE = 600;
localparam V_FP = 1;
localparam V_SYNC = 4;
localparam V_BP = 23;
localparam V_TOTAL = V_ACTIVE + V_FP + V_SYNC + V_BP; // 628
wire hsync_on = (hcount >= (H_ACTIVE + H_FP)) && (hcount < (H_ACTIVE + H_FP + H_SYNC));
wire vsync_on = (vcount >= (V_ACTIVE + V_FP)) && (vcount < (V_ACTIVE + V_FP + V_SYNC));
wire active_on = (hcount < H_ACTIVE) && (vcount < V_ACTIVE);
always @(posedge pix_clk) begin
if (!resetn) begin
hcount <= 0;
vcount <= 0;
hsync <= 1'b0;
vsync <= 1'b0;
active_video <= 1'b0;
end else begin
// Counters
if (hcount == H_TOTAL-1) begin
hcount <= 0;
if (vcount == V_TOTAL-1)
vcount <= 0;
else
vcount <= vcount + 1'b1;
end else begin
hcount <= hcount + 1'b1;
end
// Polarity: positive = assert '1' during sync interval
hsync <= hsync_on;
vsync <= vsync_on;
active_video <= active_on;
end
end
endmodule
// File: src/hdl/sprite_bitmap.v
// Procedural 32x32 sprite generator with 1-cycle latency, RGB444 output.
// Transparent color key: 12'h000 treated as transparent.
module sprite_bitmap #(
parameter SPR_W = 32,
parameter SPR_H = 32,
parameter STYLE = 0 // 0 = circle gradient, 1 = diamond checker
)(
input wire clk,
input wire en, // read enable
input wire [9:0] px, // 0..(SPR_W-1)
input wire [9:0] py, // 0..(SPR_H-1)
output reg [11:0] rgb_out // 4:4:4
);
// 1-cycle latency pipeline
reg [9:0] px_d, py_d;
reg en_d;
always @(posedge clk) begin
px_d <= px;
py_d <= py;
en_d <= en;
if (en_d) begin
rgb_out <= pixel_func(px_d, py_d);
end else begin
rgb_out <= 12'h000;
end
end
function [11:0] pixel_func;
input [9:0] x;
input [9:0] y;
integer cx, cy, dx, dy, r2;
reg [3:0] r, g, b;
begin
if (STYLE == 0) begin
// Circle with radial gradient
cx = SPR_W/2; cy = SPR_H/2;
dx = x - cx;
dy = y - cy;
r2 = dx*dx + dy*dy;
if (r2 <= ((SPR_W/2-2)*(SPR_W/2-2))) begin
// Map radius to color
r = (x[4:1]); // 0..15 gradient
g = (y[4:1]);
b = (~(x[4:1] ^ y[4:1]));
pixel_func = {r, g, b};
end else begin
pixel_func = 12'h000; // transparent
end
end else begin
// Diamond + checker
dx = (x > SPR_W/2) ? (x - SPR_W/2) : (SPR_W/2 - x);
dy = (y > SPR_H/2) ? (y - SPR_H/2) : (SPR_H/2 - y);
if ((dx + dy) < (SPR_W/2)) begin
r = {x[4:2], 1'b1};
g = {y[4:2], 1'b1};
b = {x[4]^y[4], x[3]^y[3], x[2]^y[2], 1'b1};
// Simple checker for variation
if ((x[2]^y[2]) == 1'b1) begin
r = r >> 1;
g = g >> 1;
b = b >> 1;
end
pixel_func = {r, g, b};
end else begin
pixel_func = 12'h000; // transparent
end
end
end
endfunction
endmodule
// File: src/hdl/sprite_engine.v
// Composites two 32x32 sprites with colorkey transparency over a procedural background.
// Accounts for 1-cycle bitmap latency by pipelining coordinates.
module sprite_engine #(
parameter SPR_W = 32,
parameter SPR_H = 32
)(
input wire pix_clk,
input wire resetn,
// Timing
input wire [10:0] hcount,
input wire [10:0] vcount,
input wire active_video,
// Sprite positions (top-left in screen coordinates)
input wire [10:0] spr0_x,
input wire [10:0] spr0_y,
input wire [10:0] spr1_x,
input wire [10:0] spr1_y,
output reg [3:0] vga_r,
output reg [3:0] vga_g,
output reg [3:0] vga_b
);
// Pipeline stage 0: compute local sprite coords / in-bounds flags
reg act0;
reg [10:0] h0, v0;
reg in0_0, in1_0;
reg [9:0] sx0_0, sy0_0, sx1_0, sy1_0;
// Sprite bitmap outputs (1-cycle latency)
wire [11:0] spr0_px, spr1_px;
// Instances of procedural bitmaps
sprite_bitmap #(.SPR_W(SPR_W), .SPR_H(SPR_H), .STYLE(0)) SPR0 (
.clk(pix_clk),
.en(in0_0),
.px(sx0_0),
.py(sy0_0),
.rgb_out(spr0_px)
);
sprite_bitmap #(.SPR_W(SPR_W), .SPR_H(SPR_H), .STYLE(1)) SPR1 (
.clk(pix_clk),
.en(in1_0),
.px(sx1_0),
.py(sy1_0),
.rgb_out(spr1_px)
);
// Background color function (procedural), use h/v from stage 1 (delayed)
reg act1;
reg [10:0] h1, v1;
reg [11:0] bg_rgb;
// Stage 2: compose with returned sprite pixels
reg [11:0] spr0_rgb_d, spr1_rgb_d;
// Stage 0: compute sprite-local coordinates for this pixel
always @(posedge pix_clk) begin
if (!resetn) begin
act0 <= 1'b0;
h0 <= 11'd0;
v0 <= 11'd0;
in0_0 <= 1'b0;
in1_0 <= 1'b0;
sx0_0 <= 10'd0; sy0_0 <= 10'd0;
sx1_0 <= 10'd0; sy1_0 <= 10'd0;
end else begin
act0 <= active_video;
h0 <= hcount;
v0 <= vcount;
in0_0 <= active_video &&
(hcount >= spr0_x) && (hcount < spr0_x + SPR_W) &&
(vcount >= spr0_y) && (vcount < spr0_y + SPR_H);
in1_0 <= active_video &&
(hcount >= spr1_x) && (hcount < spr1_x + SPR_W) &&
(vcount >= spr1_y) && (vcount < spr1_y + SPR_H);
sx0_0 <= hcount - spr0_x;
sy0_0 <= vcount - spr0_y;
sx1_0 <= hcount - spr1_x;
sy1_0 <= vcount - spr1_y;
end
end
// Stage 1: delay timing to match 1-cycle bitmap latency and compute background
always @(posedge pix_clk) begin
if (!resetn) begin
act1 <= 1'b0;
h1 <= 11'd0;
v1 <= 11'd0;
bg_rgb <= 12'h000;
spr0_rgb_d <= 12'h000;
spr1_rgb_d <= 12'h000;
end else begin
act1 <= act0;
h1 <= h0;
v1 <= v0;
// Background: gradient + XOR stripes
// Scale 0..799 and 0..599 into 4-bit ranges
// h1[7:4] approximates coarse gradient; combine with v1 for variation
bg_rgb[11:8] <= {h1[7:5], 1'b1}; // R
bg_rgb[7:4] <= {v1[7:5], 1'b1}; // G
bg_rgb[3:0] <= {h1[7]^v1[7], h1[6]^v1[6], h1[5]^v1[5], 1'b1}; // B
spr0_rgb_d <= spr0_px;
spr1_rgb_d <= spr1_px;
end
end
// Stage 2: final composition (priority: spr1 over spr0 over background)
always @(posedge pix_clk) begin
if (!resetn) begin
{vga_r, vga_g, vga_b} <= 12'h000;
end else begin
if (act1) begin
if (spr1_rgb_d != 12'h000) begin
{vga_r, vga_g, vga_b} <= spr1_rgb_d;
end else if (spr0_rgb_d != 12'h000) begin
{vga_r, vga_g, vga_b} <= spr0_rgb_d;
end else begin
{vga_r, vga_g, vga_b} <= bg_rgb;
end
end else begin
{vga_r, vga_g, vga_b} <= 12'h000;
end
end
end
endmodule
// File: src/hdl/top_basys3_vga_sprites.v
// Top-level for Digilent Basys 3 (XC7A35T). Generates 40 MHz pixel clock, timing, sprites.
module top_basys3_vga_sprites (
input wire clk100mhz, // 100 MHz onboard oscillator
input wire [15:0] sw, // switches (optional controls)
input wire btnC, btnU, btnL, btnR, btnD, // buttons (optional)
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,
output wire [15:0] led // status/debug
);
// Clock generation: 40 MHz pixel clock
wire pix_clk;
wire mmcm_locked;
wire resetn = mmcm_locked; // hold logic in reset until clock stable
// Instantiate Clocking Wizard IP (generated via Tcl as clk_wiz_40m)
clk_wiz_40m clkgen_i (
.clk_in1(clk100mhz),
.clk_out1(pix_clk),
.reset(1'b0),
.locked(mmcm_locked)
);
// VGA timing
wire [10:0] hcount, vcount;
wire active_video;
wire hsync, vsync;
vga_timing_800x600 timing_i (
.pix_clk(pix_clk),
.resetn(resetn),
.hcount(hcount),
.vcount(vcount),
.hsync(hsync),
.vsync(vsync),
.active_video(active_video)
);
// Sprite engine
reg [10:0] spr0_x = 11'd64;
reg [10:0] spr0_y = 11'd64;
reg [10:0] spr1_x = 11'd320;
reg [10:0] spr1_y = 11'd200;
// Velocity in pixels per frame
reg signed [10:0] dx0 = 11'sd2, dy0 = 11'sd1;
reg signed [10:0] dx1 = -11'sd3, dy1 = 11'sd2;
// Detect frame end (last pixel of last line)
wire end_of_frame = (hcount == 11'd1055) && (vcount == 11'd627);
// Animation: update positions on each frame
always @(posedge pix_clk) begin
if (!resetn) begin
spr0_x <= 11'd64; spr0_y <= 11'd64;
spr1_x <= 11'd320; spr1_y <= 11'd200;
dx0 <= 11'sd2; dy0 <= 11'sd1;
dx1 <= -11'sd3; dy1 <= 11'sd2;
end else if (end_of_frame) begin
// Move
spr0_x <= spr0_x + dx0;
spr0_y <= spr0_y + dy0;
spr1_x <= spr1_x + dx1;
spr1_y <= spr1_y + dy1;
// Bounce at edges (screen 800x600, sprite 32x32)
if (spr0_x <= 0 || spr0_x >= (800-32)) dx0 <= -dx0;
if (spr0_y <= 0 || spr0_y >= (600-32)) dy0 <= -dy0;
if (spr1_x <= 0 || spr1_x >= (800-32)) dx1 <= -dx1;
if (spr1_y <= 0 || spr1_y >= (600-32)) dy1 <= -dy1;
// Optional: control with switches to invert directions
if (sw[0]) dx0 <= -dx0;
if (sw[1]) dy0 <= -dy0;
if (sw[2]) dx1 <= -dx1;
if (sw[3]) dy1 <= -dy1;
end
end
sprite_engine SPRENG (
.pix_clk(pix_clk),
.resetn(resetn),
.hcount(hcount),
.vcount(vcount),
.active_video(active_video),
.spr0_x(spr0_x),
.spr0_y(spr0_y),
.spr1_x(spr1_x),
.spr1_y(spr1_y),
.vga_r(vga_r),
.vga_g(vga_g),
.vga_b(vga_b)
);
// Drive HSync/VSync directly (positive polarity)
assign vga_hs = hsync;
assign vga_vs = vsync;
// LEDs for quick diagnostics
// led[15] = mmcm_locked
// led[14] = active_video
// led[13] = end_of_frame pulse (stretched)
// led[12:0] = frame counter low bits
reg [23:0] frame_ctr = 24'd0;
reg [3:0] eof_stretch = 4'd0;
always @(posedge pix_clk) begin
if (!resetn) begin
frame_ctr <= 24'd0;
eof_stretch <= 4'd0;
end else begin
if (end_of_frame) begin
frame_ctr <= frame_ctr + 1;
eof_stretch <= 4'hF;
end else if (eof_stretch != 4'd0) begin
eof_stretch <= eof_stretch - 1;
end
end
end
assign led[15] = mmcm_locked;
assign led[14] = active_video;
assign led[13] = |eof_stretch;
assign led[12:0] = frame_ctr[12:0];
endmodule
Notes:
– The sprite bitmaps are generated procedurally (no external memory files required).
– The pipeline accounts for the 1-cycle latency of sprite_bitmap, ensuring colors align with the correct pixel.
Constraints and Board Connections
Use the official Digilent Basys-3 Master XDC so pin mappings are correct. Download the XDC at a fixed revision (example, commit 1a8bdcd from Digilent’s repository; replace with current stable if needed):
- URL: https://github.com/Digilent/Basys-3-Resources/blob/master/Constraints/Basys3_Master.xdc
Place it in: constraints/Basys3_Master.xdc
Inside that file, uncomment and adapt only the lines for:
– 100 MHz clock pin (clk100mhz): “W5” on Basys 3.
– VGA: vga_r[3:0], vga_g[3:0], vga_b[3:0], vga_hs, vga_vs.
Ensure the get_ports names exactly match the top-level ports in top_basys3_vga_sprites.v:
– clk100mhz
– vga_r[3:0], vga_g[3:0], vga_b[3:0]
– vga_hs, vga_vs
You should not change the PACKAGE_PIN assignments from the Digilent file. Just ensure the port names match.
If you prefer a minimal XDC file, copy only the relevant blocks (Clock and VGA) from the Digilent master XDC into a new file, constraints/basys3_vga_800x600.xdc, preserving all pin assignments and IOSTANDARD LVCMOS33.
Build / Flash / Run (CLI)
We will create the project, add sources, generate the 40 MHz clock IP via the Clocking Wizard, synthesize, implement, and program the device from Tcl. The directory layout is:
- project/
- src/hdl/*.v
- constraints/Basys3_Master.xdc
- scripts/build.tcl
- scripts/program.tcl
- ip/ (Vivado will place the clk_wiz here)
Create scripts/build.tcl with:
# Vivado WebPACK 2023.2 non-project batch flow for Basys 3 sprite engine
set TOOL_VERSION [version -short]
puts "Vivado version: $TOOL_VERSION"
# Clean work
file delete -force build
file mkdir build
cd build
# Create project
create_project sprite800x600 .. -part xc7a35tcpg236-1 -force
# Add HDL sources
add_files -fileset sources_1 [glob ../src/hdl/*.v]
# Add constraints (use Digilent Master XDC with VGA and clock uncommented)
add_files -fileset constrs_1 ../constraints/Basys3_Master.xdc
# Create Clocking Wizard IP for 40.0 MHz from 100 MHz
create_ip -name clk_wiz -vendor xilinx.com -library ip -module_name clk_wiz_40m
set_property -dict [list \
CONFIG.PRIM_SOURCE {MMCM} \
CONFIG.CLK_IN1_BOARD_INTERFACE {} \
CONFIG.PRIM_IN_FREQ {100.000} \
CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {40.000} \
CONFIG.MMCM_CLKFBOUT_MULT_F {8.0} \
CONFIG.MMCM_CLKOUT0_DIVIDE_F {20.000} \
CONFIG.MMCM_CLKIN1_PERIOD {10.000} \
] [get_ips clk_wiz_40m]
generate_target all [get_ips clk_wiz_40m]
synth_ip [get_ips clk_wiz_40m]
export_ip_user_files -of_objects [get_ips clk_wiz_40m] -no_script -sync -force -quiet
# Add generated IP to project (for out-of-context)
update_ip_catalog
# Elaborate/synth/impl/bitstream
update_compile_order -fileset sources_1
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
# Copy bitstream to top-level build dir
file copy -force [get_property PROGRESS_WRITES [get_runs impl_1]] .
set BITFILE [get_property TOP [current_fileset]] ; # or directly point
# Better: get real file path
set BITPATH [get_property BITSTREAM.FILE [get_runs impl_1]]
puts "Bitstream: $BITPATH"
Create scripts/program.tcl with:
# File: scripts/program.tcl
# Program Basys 3 over JTAG using Vivado 2023.2
# Locate the most recent bitstream produced by build.tcl
set build_dir [file normalize "./build"]
set bitfiles [glob -nocomplain -types f -directory $build_dir -tails -regexp {.*\.bit}]
if {[llength $bitfiles] == 0} {
puts "ERROR: No .bit found in $build_dir"; exit 1
}
# Assume only one bit or choose the latest by modification time
set bit [lindex $bitfiles 0]
set bitpath [file join $build_dir $bit]
puts "Programming bitstream: $bitpath"
open_hw
connect_hw_server
open_hw_target
# Pick the Basys 3 device (Artix-7 xc7a35t)
set devs [get_hw_devices *xc7a35t*]
current_hw_device [lindex $devs 0]
refresh_hw_device [current_hw_device]
set_property PROGRAM.FILE $bitpath [current_hw_device]
program_hw_devices [current_hw_device]
puts "Done."
Run the flow from a shell:
# Verify Vivado
vivado -version
# Build (synth+impl+bitstream)
vivado -mode batch -source scripts/build.tcl
# Program the FPGA
vivado -mode batch -source scripts/program.tcl
Expected output:
– Synthesis and implementation complete without critical warnings.
– Bitstream generated.
– Programming successful.
Step-by-step Validation
- Monitor OSD check:
- After programming, the monitor should wake and report “800×600” or “60 Hz (approx 60.3 Hz)” in its on-screen display.
-
If the monitor says “Out of Range,” see Troubleshooting.
-
Visual test:
-
You should see a moving colorful background with two 32×32 sprites bouncing within the 800×600 frame. One sprite is circular/gradient; the other is a diamond/checker pattern. Transparency should be evident where the shapes do not cover the background.
-
Polarity and timing:
- The image must be stable, centered, without tearing or shimmering. If edges look unstable, confirm we are using positive polarity HS/VS (as coded).
-
LED[15] should be ON indicating the MMCM is locked. LED[13] should pulse at ~60 Hz. LED[14] glows when in active video.
-
Switch interaction:
-
Flip SW0–SW3 during runtime to invert velocities for each axis of each sprite. Motion should respond immediately after the next frame tick.
-
Frequency alignment (optional measurements):
- HSync frequency should be about 37.879 kHz; VSync about 60.317 Hz. If you have a scope:
- Probe vga_hs to see a pulse at ~37.9 kHz.
- Probe vga_vs to see ~60 Hz pulses.
-
Pixel clock is 40.000 MHz, generated by the Clocking Wizard. If you have the necessary probing gear, you can observe edges indirectly through sync timing.
-
Frame bounds:
- Confirm sprites never draw outside the 800×600 viewport. They should bounce at edges. There should be no visible wrap-around of sprites at the right and bottom edges.
Troubleshooting
- Monitor shows “Out of Range” or blank:
- Ensure the pixel clock is exactly 40 MHz and HS/VS polarity is positive.
- Verify you uncommented and kept the correct VGA pin assignments from the Digilent Master XDC.
-
Some monitors are strict; power-cycle the monitor after programming to force re-detect.
-
Image shifted or unstable:
- Recheck timing constants in vga_timing_800x600.v; confirm H/V porch sizes and sync widths and that totals are 1056 and 628.
-
Ensure the Clocking Wizard is set to 100 MHz input and 40 MHz output exactly (no fractional misconfig).
-
No LEDs, no video:
- Confirm MMCM lock (LED[15]): if off, the clock IP may not be configured or the reset is stuck.
- Verify the 100 MHz clock pin constraint (clk100mhz) is correct and routed to W5 (via Digilent Master XDC).
-
Check power and JTAG programming succeeded (Vivado console).
-
Colors or brightness incorrect:
-
Ensure the VGA color pins are mapped to the 4:4:4 outputs and that all bits are constrained to the correct Basys 3 VGA pins. Wrong order will produce odd colors or banding.
-
Synthesis/implementation errors for clk_wiz_40m:
- Delete the build directory and rerun build.tcl to regenerate IP cleanly.
-
If using a different Vivado version, the IP version may differ. The create_ip command without -version selects the latest available; that is fine.
-
Flickering at sprite edges:
- Mismatch between bitmap latency and compositor pipeline can cause off-by-one artifacts. The provided code pipelines by one cycle to match the sprite_bitmap latency. If you refactor for BRAM-based $readmemh sprites, ensure read latency is accounted for similarly.
Improvements and Extensions
- More sprites:
-
Extend sprite_engine to support N sprites with a small attribute RAM and a priority encoder. For N up to ~32 at 40 MHz, a straightforward loop with registered stages can still meet timing on Artix-7.
-
Larger sprites or scaling:
-
Add scaling factors (nearest neighbor) to enlarge sprites when close to the camera. This requires adjusting pixel fetch coordinates and may necessitate dual-port BRAMs to keep pace.
-
Hardware blending:
-
Add one-bit alpha or simple weighted blend to avoid hard edges. With 4-bit channels, you can implement fast 1/2 or 1/4 blends with shifts.
-
Tile-based background:
-
Replace procedural background with a BRAM tile map to emulate retro consoles. A tile fetcher can index a 2D map and a tile ROM.
-
DMA/framebuffer:
-
Basys 3 has limited on-chip BRAM. For full-frame buffers, consider external memory on PMOD (advanced) or compress background using parametric generators.
-
Collision detection:
-
Track sprite bounding boxes and overlap; publish events on LED or UART for gameplay logic.
-
On-screen debugging:
-
Draw bounding boxes or text overlay by adding another compositor stage.
-
Timing margins:
- Add constraints on the 40 MHz domain and analyze slack. If you add more complex pipelines, turn on retiming and explore placement options.
Final Checklist
- Prerequisites:
- Vivado WebPACK 2023.2 installed and accessible via PATH.
-
Basys 3 connected via USB; VGA monitor and cable ready.
-
Materials:
- Digilent Basys 3 board (XC7A35T-1CPG236C).
-
VGA cable and monitor supporting 800×600@60 Hz.
-
Setup/Connection:
-
USB and VGA connected. Monitor set to VGA input.
-
Constraints:
- constraints/Basys3_Master.xdc present.
- Correct port names: clk100mhz, vga_{r,g,b}[3:0], vga_hs, vga_vs.
-
VGA and clock pin constraints uncommented and left at Digilent-specified PACKAGE_PIN assignments.
-
Code:
- src/hdl/vga_timing_800x600.v with H/V constants: 800/40/128/88 and 600/1/4/23, positive HS/VS.
- src/hdl/sprite_bitmap.v providing procedural 32×32 sprites with 1-cycle latency and colorkey=0.
- src/hdl/sprite_engine.v that pipelines and composites sprites over background.
-
src/hdl/top_basys3_vga_sprites.v instantiating clk_wiz_40m, timing, and sprite engine; animates sprites per frame.
-
Build:
- scripts/build.tcl creates clk_wiz_40m at 40 MHz, builds bitstream without errors.
-
Confirm vivado -mode batch -source scripts/build.tcl completes.
-
Program:
-
scripts/program.tcl programs the Basys 3 successfully.
-
Validation:
- Monitor OSD shows ~800×600 @ ~60 Hz.
- Two moving sprites visible; background gradient visible; transparency correct.
-
LED[15] = lock, LED[13] pulses per frame.
-
Troubleshooting resolved (if any):
- Sync polarity, porches, and pixel clock verified.
By following these steps, you have implemented a hardware-accelerated VGA sprite engine at 800×600 on a Digilent Basys 3, including timing generation, pixel clock synthesis via MMCM, and a compositing pipeline that is robust to memory latency. The structure is modular to support further scaling, additional sprite features, and more sophisticated backgrounds.
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.


