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:
- Visible Area (640 px): Where we paint colors.
- Front Porch (16 px): A breather before the pulse.
- Sync Pulse (96 px): The actual H-Sync pulse.
- 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 = 4096colors.
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;
endmoduleHow 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;
endmoduleWhat 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.