HomeFPGA from ScratchDay 23
DAY 23 · VIDEO

VGA / HDMI Video Output on FPGA

By EcrioniX · Updated Jun 11, 2026

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.

1. VGA timing — 640×480@60Hz

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.

2. Horizontal timing (per line)

RegionPixelsDescription
Active display640Visible pixels — RGB data valid
Front porch16Blank pixels before HSYNC pulse
HSYNC pulse96HSYNC = 0 (active low) for 96 pixel clocks
Back porch48Blank pixels after HSYNC, before next line
Total800800 pixel clocks per line at 25.175 MHz ≈ 31.77 µs

3. Vertical timing (per frame)

RegionLinesDescription
Active display480Visible lines
Front porch10Blank lines before VSYNC
VSYNC pulse2VSYNC = 0 (active low) for 2 lines
Back porch33Blank lines after VSYNC
Total525525 lines × 800 pixels = 420,000 pixel clocks/frame → 60 Hz

4. Port table — vga_sync

PortDirWidthDescription
clk_25IN125 MHz pixel clock (generate with PLL from Day 18)
rstIN1Synchronous reset
hsyncOUT1Horizontal sync (active low, 96 clocks wide)
vsyncOUT1Vertical sync (active low, 2 lines)
activeOUT1High during visible area. Drive RGB = 0 when low.
hposOUT10Current horizontal pixel position (0–639 during active)
vposOUT10Current vertical line position (0–479 during active)

5. vga_sync.v

vga_sync.v
// 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

6. Using vga_sync to draw pixels

vga_top.v (snippet)
// 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

7. Testbench — tb_vga_sync.v

tb_vga_sync.v
// 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

8. Expected output

PASS: hsync_cnt=1050 (expected ~1050)
PASS: vsync_cnt=2 (expected 2)

ALL TESTS PASSED (2/2)

Key Takeaways

Frequently Asked Questions

Why does VGA need a 25 MHz pixel clock?

640×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).

What are front porch and back porch?

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.

How do you display a pixel?

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.

← Previous
Day 22: I2C Controller