Generating a stable video signal from an FPGA is one of the most satisfying projects in digital design — pixels drawn by your own logic on a real screen. This lesson builds a complete 640×480@60Hz VGA sync generator, explains the full timing specification, and gives you the pixel coordinate outputs needed to render any image or pattern.
VGA was designed for CRT monitors whose electron beam scanned one row at a time. The timing signals — HSYNC (horizontal sync) and VSYNC (vertical sync) — tell the monitor when to start a new line and a new frame. Modern LCD monitors still accept these signals and use them to lock their internal timing.
| Region | Pixels | Description |
|---|---|---|
| Active display | 640 | Visible pixels — RGB data valid |
| Front porch | 16 | Blank pixels before HSYNC pulse |
| HSYNC pulse | 96 | HSYNC = 0 (active low) for 96 pixel clocks |
| Back porch | 48 | Blank pixels after HSYNC, before next line |
| Total | 800 | 800 pixel clocks per line at 25.175 MHz ≈ 31.77 µs |
| Region | Lines | Description |
|---|---|---|
| Active display | 480 | Visible lines |
| Front porch | 10 | Blank lines before VSYNC |
| VSYNC pulse | 2 | VSYNC = 0 (active low) for 2 lines |
| Back porch | 33 | Blank lines after VSYNC |
| Total | 525 | 525 lines × 800 pixels = 420,000 pixel clocks/frame → 60 Hz |
| Port | Dir | Width | Description |
|---|---|---|---|
| clk_25 | IN | 1 | 25 MHz pixel clock (generate with PLL from Day 18) |
| rst | IN | 1 | Synchronous reset |
| hsync | OUT | 1 | Horizontal sync (active low, 96 clocks wide) |
| vsync | OUT | 1 | Vertical sync (active low, 2 lines) |
| active | OUT | 1 | High during visible area. Drive RGB = 0 when low. |
| hpos | OUT | 10 | Current horizontal pixel position (0–639 during active) |
| vpos | OUT | 10 | Current vertical line position (0–479 during active) |
// vga_sync.v — 640×480 @ 60Hz VGA sync generator
// Pixel clock: 25.175 MHz (use 25 MHz — within monitor tolerance)
// Connect clk_25 to a PLL output (Day 18: Fvco=1000 MHz, O=40)
module vga_sync (
input wire clk_25,
input wire rst,
output reg hsync,
output reg vsync,
output wire active,
output wire [9:0] hpos,
output wire [9:0] vpos
);
// ---- Horizontal timing constants ----
localparam H_ACTIVE = 640;
localparam H_FP = 16; // front porch
localparam H_SYNC = 96; // sync pulse width
localparam H_BP = 48; // back porch
localparam H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP; // 800
// ---- Vertical timing constants ----
localparam V_ACTIVE = 480;
localparam V_FP = 10;
localparam V_SYNC = 2;
localparam V_BP = 33;
localparam V_TOTAL = V_ACTIVE + V_FP + V_SYNC + V_BP; // 525
// ---- Counters ----
reg [9:0] h_cnt = 0; // horizontal pixel counter 0..799
reg [9:0] v_cnt = 0; // vertical line counter 0..524
// Increment horizontal counter every pixel clock
always @(posedge clk_25) begin
if (rst) begin
h_cnt <= 0;
v_cnt <= 0;
end else begin
if (h_cnt == H_TOTAL - 1) begin
h_cnt <= 0;
if (v_cnt == V_TOTAL - 1)
v_cnt <= 0;
else
v_cnt <= v_cnt + 1;
end else begin
h_cnt <= h_cnt + 1;
end
end
end
// ---- Generate HSYNC ----
// HSYNC pulse: active (low) from H_ACTIVE+H_FP to H_ACTIVE+H_FP+H_SYNC
always @(posedge clk_25) begin
if (rst)
hsync <= 1;
else
hsync <= ~((h_cnt >= H_ACTIVE + H_FP) &&
(h_cnt < H_ACTIVE + H_FP + H_SYNC));
end
// ---- Generate VSYNC ----
always @(posedge clk_25) begin
if (rst)
vsync <= 1;
else
vsync <= ~((v_cnt >= V_ACTIVE + V_FP) &&
(v_cnt < V_ACTIVE + V_FP + V_SYNC));
end
// ---- Active area and pixel coordinates ----
wire h_active = (h_cnt < H_ACTIVE);
wire v_active = (v_cnt < V_ACTIVE);
assign active = h_active && v_active;
assign hpos = h_active ? h_cnt : 10'd0;
assign vpos = v_active ? v_cnt : 10'd0;
endmodule
// Draw a coloured checkerboard pattern
// rgb_r, rgb_g, rgb_b connect to the VGA DAC resistors (4-bit each on Basys3)
module vga_top (
input wire clk_25,
input wire rst,
output wire hsync,
output wire vsync,
output wire [3:0] rgb_r,
output wire [3:0] rgb_g,
output wire [3:0] rgb_b
);
wire active;
wire [9:0] hpos, vpos;
vga_sync sync_gen (
.clk_25(clk_25), .rst(rst),
.hsync(hsync), .vsync(vsync),
.active(active), .hpos(hpos), .vpos(vpos)
);
// Checkerboard: alternate colour every 32 pixels
wire cell = hpos[5] ^ vpos[5]; // bit 5 = divide by 32
assign rgb_r = active ? (cell ? 4'hF : 4'h0) : 4'h0;
assign rgb_g = active ? (cell ? 4'h0 : 4'hF) : 4'h0;
assign rgb_b = active ? 4'h0 : 4'h0;
endmodule
// tb_vga_sync.v — count hsync pulses and verify timing
`timescale 1ns/1ps
module tb_vga_sync;
reg clk_25 = 0;
reg rst = 1;
wire hsync, vsync, active;
wire [9:0] hpos, vpos;
vga_sync dut(.clk_25(clk_25),.rst(rst),.hsync(hsync),.vsync(vsync),
.active(active),.hpos(hpos),.vpos(vpos));
always #20 clk_25 = ~clk_25; // 25 MHz = 40 ns period
integer hsync_cnt = 0;
integer vsync_cnt = 0;
integer pass_cnt = 0;
integer fail_cnt = 0;
reg hsync_prev = 1, vsync_prev = 1;
// Count sync pulse falling edges
always @(posedge clk_25) begin
hsync_prev <= hsync;
vsync_prev <= vsync;
if (hsync_prev && !hsync) hsync_cnt = hsync_cnt + 1;
if (vsync_prev && !vsync) vsync_cnt = vsync_cnt + 1;
end
initial begin
$dumpfile("tb_vga_sync.vcd");
$dumpvars(0, tb_vga_sync);
repeat(4) @(posedge clk_25);
rst = 0;
// Wait for 2 full frames (2 × 525 × 800 = 840,000 pixel clocks)
// At 40 ns/pixel: 840,000 × 40 ns = 33.6 ms
#34_000_000; // 34 ms simulation time
// Each frame = 525 lines = 525 hsync pulses
// 2 frames = 1050 hsync pulses expected
if (hsync_cnt >= 1048 && hsync_cnt <= 1052) begin
$display("PASS: hsync_cnt=%0d (expected ~1050)", hsync_cnt);
pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: hsync_cnt=%0d (expected ~1050)", hsync_cnt);
fail_cnt = fail_cnt + 1;
end
// 2 vsync pulses expected (1 per frame)
if (vsync_cnt == 2) begin
$display("PASS: vsync_cnt=%0d (expected 2)", vsync_cnt);
pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: vsync_cnt=%0d (expected 2)", vsync_cnt);
fail_cnt = fail_cnt + 1;
end
if (fail_cnt == 0)
$display("\nALL TESTS PASSED (%0d/%0d)", pass_cnt, pass_cnt+fail_cnt);
else
$display("\nFAILED: %0d passed, %0d failed", pass_cnt, fail_cnt);
$finish;
end
initial #40_000_000 begin $display("TIMEOUT"); $finish; end
endmodule
PASS: hsync_cnt=1050 (expected ~1050) PASS: vsync_cnt=2 (expected 2) ALL TESTS PASSED (2/2)
active flag is the gate for your pixel data — only drive colour when it is high640×480@60Hz requires 800 total pixels × 525 total lines × 60 fps = 25.2 MHz. A 25 MHz pixel clock is close enough for all monitors. Generate it with a PLL from the 100 MHz board clock (M=10, D=1, O=40).
After the active pixels, there is a blanking period. The front porch is blank pixels before the sync pulse; the back porch is blank pixels after it. Together they give the monitor time to retrace the electron beam. RGB must be zero during all blanking.
When active is high, drive the RGB DAC with your pixel colour for coordinates hpos/vpos. When active is low, drive RGB to zero. The VGA DAC converts digital values to analogue voltages on the 15-pin connector.