fpga-vga-hdmi-generacion-video

VGA and HDMI Video Generation with FPGA

  • 6 min

A VGA controller is a circuit that generates pixel coordinates and synchronization pulses following specific video timings.

If you try to generate a 640x480 pixel VGA video signal at 60Hz with an Arduino Uno, the poor chip will have to dedicate 100% of its CPU just to spit bits to the pins, and even then it will probably fail.

With an FPGA, we can generate video while other blocks work in parallel, as long as there are enough resources and bandwidth.

Today we are going to learn how to command the electron beam of a monitor. We are going to design a VGA Controller.

The Image Scan

Even if you use a modern LCD monitor or an HDMI projector, the video transmission standard is still based on how old cathode-ray tube (CRT) televisions worked.

Imagine a light brush drawing the screen:

It starts at the top left.

It draws a horizontal line to the right.

When it reaches the end, it turns off, quickly returns to the left, and moves down a little.

It repeats this until it reaches the very bottom.

It returns to the top to start the next image (Frame).

For the monitor to know when to “return to the left” or “return to the top,” we need two sacred synchronization signals:

  • H-Sync (Horizontal Sync): “End of line! Return to the left.”
  • V-Sync (Vertical Sync): “End of image! Return to the top.”

Blanking Timings (Porches)

Here is the trick. A resolution of 640x480 is not 640x480. The monitor needs extra time “off-screen” to stabilize the magnetic beam before starting to draw again.

These dead zones are called Porches. A complete horizontal line is divided into:

  1. Visible Area (640 px): Where we paint colors.
  2. Front Porch (16 px): A breather before the pulse.
  3. Sync Pulse (96 px): The actual H-Sync pulse.
  4. Back Porch (48 px): A breather after the pulse to stabilize.

Total: 640 + 16 + 96 + 48 = 800 pixel clock cycles per line. The same applies vertically (480 visible lines vs. 525 total lines).

The Pixel Clock:

Each frame contains 800 × 525 = 420,000 pixel clock cycles. The historic 640×480 mode uses a clock of 25.175 MHz, which yields approximately 59.94 frames per second. Using 25 MHz would give us about 59.52 Hz;

Many monitors accept this, although you shouldn’t take it for granted.

Analog Color Outputs

VGA is analog. We need voltages between 0V (Black) and 0.7V (Maximum Color). Since the FPGA only outputs 0V or 3.3V, we use a Resistor DAC on the VGA connector.

We typically have 3 channels: Red, Green, and Blue.

  • If we have 1 bit per color (3 pins): Only 8 possible colors.
  • If we have 4 bits per color (12 pins): 2^12 = 4096 colors.

A VGA input usually presents a 75 Ω termination. Do not connect a 3.3V output directly: use the resistor network calculated for your number of bits and check your board’s schematic. It is not always a simple R-2R ladder.

Verilog Implementation

Let’s create a controller for 640x480 @ 60Hz. We need two counters: one for X (0 to 799) and one for Y (0 to 524).

module vga_driver (
    input wire clk_25mhz,   // Exact pixel clock
    input wire rst,
    output wire hsync,      // VGA H-Sync pin
    output wire vsync,      // VGA V-Sync pin
    output wire video_on,   // 1 = We are in the visible area
    output wire [9:0] x,    // Current X coordinate (for other modules to paint)
    output wire [9:0] y     // Current Y coordinate
);

    // Standard VGA 640x480 60Hz parameters
    localparam H_VISIBLE = 640;
    localparam H_FRONT   = 16;
    localparam H_SYNC    = 96;
    localparam H_BACK    = 48;
    localparam H_TOTAL   = 800; // 640+16+96+48

    localparam V_VISIBLE = 480;
    localparam V_FRONT   = 10;
    localparam V_SYNC    = 2;
    localparam V_BACK    = 33;
    localparam V_TOTAL   = 525; // 480+10+2+33

    // Counters
    reg [9:0] h_count = 0;
    reg [9:0] v_count = 0;

    // 1. Horizontal and Vertical Counter
    always @(posedge clk_25mhz) begin
        if (rst) begin
            h_count <= 0;
            v_count <= 0;
        end else begin
            // Horizontal advance
            if (h_count == H_TOTAL - 1) begin
                h_count <= 0;
                // Vertical advance (at the end of a line)
                if (v_count == V_TOTAL - 1) begin
                    v_count <= 0;
                end else begin
                    v_count <= v_count + 1;
                end
            end else begin
                h_count <= h_count + 1;
            end
        end
    end

    // 2. Sync pulses active low and aligned with counters
    assign hsync = ~((h_count >= H_VISIBLE + H_FRONT) &&
                     (h_count <  H_VISIBLE + H_FRONT + H_SYNC));
    assign vsync = ~((v_count >= V_VISIBLE + V_FRONT) &&
                     (v_count <  V_VISIBLE + V_FRONT + V_SYNC));

    // 3. Video ON signal (Only paint when in the visible area)
    assign video_on = (h_count < H_VISIBLE) && (v_count < V_VISIBLE);

    // 4. Export coordinates for the drawing logic
    assign x = h_count;
    assign y = v_count;

endmodule
Copied!

How to Paint Each Pixel

Now that we have the controller telling us WHERE we are (x, y) and WHEN to paint (video_on), we only need combinational logic to decide WHAT color to put.

module test_patron (
    input wire clk,         // 12 MHz input (if using external PLL) or 25 MHz
    output wire hsync,
    output wire vsync,
    output wire [2:0] rgb   // 3 pins: R, G, B (1 bit each)
);

    wire video_on;
    wire [9:0] x, y;

    // Instance of the VGA driver (Assuming 'clk' is already 25 MHz)
    vga_driver mi_vga (
        .clk_25mhz(clk),
        .rst(1'b0),
        .hsync(hsync),
        .vsync(vsync),
        .video_on(video_on),
        .x(x),
        .y(y)
    );

    // PAINT LOGIC
    // If video_on is 0, it is MANDATORY to send BLACK (000).
    // Otherwise, the monitor will lose the black level and display poorly.
    
    reg [2:0] color_pintar;
    
    always @(*) begin
        if (!video_on) begin
            color_pintar = 3'b000;
        end else begin
            // Example: Vertical tricolor flag
            if (x < 213)      color_pintar = 3'b100; // Red
            else if (x < 426) color_pintar = 3'b010; // Green
            else              color_pintar = 3'b001; // Blue
            
            // Example 2: Chessboard pattern
            // color_pintar = (x[4] ^ y[4]) ? 3'b111 : 3'b000;
        end
    end

    assign rgb = color_pintar;

endmodule
Copied!

What about HDMI / DVI?

VGA is analog and “old.” Can we output pure digital HDMI? Yes, but it is much more difficult.

HDMI (and DVI) use an encoding called TMDS (Transition Minimized Differential Signaling). Each channel encodes 8 video bits into 10-bit TMDS symbols, thus transmitting at ten times the pixel frequency. For 640×480, each channel runs around 251.75 Mbit/s.

On basic FPGAs, this speed may be outside the pin specifications or require very device-dependent techniques. Other families include dedicated serializers and differential resources that facilitate DVI/HDMI output.

The good news is that the timing logic (h_count, v_count, video_on) is exactly the same. Only the final physical output stage changes.