On this page
- Why Your Monitor Expects Exactly 800 Pixels Per Line (Even Though It Only Shows 640)
- What You Need to Know Before Starting
- A Typewriter Drawing an Invisible Grid
- Step 1: Define the Timing Boundaries
- Step 2: Generate Sync Signals Combinationally
- Step 3: Advance the Counters
- The Math Behind the Refresh Rate
- When the Counters Lie: Blank Screens and Clock Violations
Why Your Monitor Expects Exactly 800 Pixels Per Line (Even Though It Only Shows 640)
Every VGA monitor in the world obeys an invisible contract. The display doesn’t simply receive pixel data — it requires a precisely timed sequence of active pixels, blank intervals, and synchronization pulses, repeated line by line, frame by frame. If a single pixel clock cycle is out of place, the screen tears, rolls, or goes black entirely.
This tutorial walks through the simple_480p module in this Wordle project — a compact Verilog module that generates the exact timing signals a 640×480 VGA display demands. By the end, you’ll understand how two ten-bit counters and a handful of comparators produce a stable sixty-frame-per-second video signal. No external IP cores, no vendor primitives — just counter arithmetic.
VGA timing is governed by the VESA DMT (Display Monitor Timing) standard. The 640×480 @ 60 Hz mode has been supported by every monitor manufactured since 1987. Understanding it is foundational to any display-adjacent hardware design.
What You Need to Know Before Starting
You need to understand binary counters (a register that increments on each clock edge), the difference between combinational and sequential logic, and what a clock signal is and what “posedge” means in Verilog.
Tools: Verilog HDL (IEEE 1364-2001) and any simulator (Verilator, Icarus Verilog, or Vivado). File under discussion: modules/display/simple_480p.v (72 lines).
A Typewriter Drawing an Invisible Grid
Think of a VGA display as a typewriter. A carriage sweeps left to right across the page, laying down characters. At the end of each line, the carriage returns and the paper advances one line. When the bottom of the page is reached, the paper feeds back to the top.
A VGA signal works the same way. A horizontal counter (sx) sweeps from pixel 0 to pixel 799 on each line. When it wraps, a vertical counter (sy) advances from line 0 to line 524. At every position, the display checks whether it’s inside the visible area, inside a blanking interval, or inside a sync pulse.
The entire timing generator is just these two counters and a few comparators.
Step 1: Define the Timing Boundaries
The VGA 640×480 standard specifies four regions per horizontal line. These constants encode them as pixel-clock cycle counts, zero-indexed:
// Horizontal timing — 800 pixel clocks per line
parameter HA_END = 639; // last visible pixel
parameter HS_STA = HA_END + 16; // = 655: sync pulse begins after 16-clock front porch
parameter HS_END = HS_STA + 96; // = 751: sync pulse lasts 96 clocks
parameter LINE = 799; // last clock in the line (48-clock back porch follows sync)
The same structure repeats vertically, counted in lines rather than pixels:
// Vertical timing — 525 lines per frame
parameter VA_END = 479; // last visible line
parameter VS_STA = VA_END + 10; // = 489: sync pulse begins after 10-line front porch
parameter VS_END = VS_STA + 2; // = 491: sync pulse lasts 2 lines
parameter SCREEN = 524; // last line in the frame (33-line back porch follows)
Why these exact numbers? At a 25 MHz pixel clock, 800 pixels per line produces a horizontal frequency of 31.25 kHz. Dividing by 525 lines gives 59.52 Hz — the standard VGA refresh rate. Change any one of these parameters and the monitor will reject the signal.
Step 2: Generate Sync Signals Combinationally
Sync signals tell the monitor when to start a new line (hsync) or a new frame (vsync). VGA 640×480 uses negative-polarity syncs — the signal is LOW during the pulse:
always @* begin
hsync = ~(sx >= HS_STA && sx < HS_END); // LOW during pixels 655–750
vsync = ~(sy >= VS_STA && sy < VS_END); // LOW during lines 489–490
de = (sx <= HA_END && sy <= VA_END); // HIGH only in the 640×480 visible rectangle
end
The de (data enable) signal is what downstream modules use to decide whether to output a pixel color. When de is low, the display ignores whatever color data it receives — that interval is for sync pulses and blanking porches.
Step 3: Advance the Counters
The horizontal counter increments every pixel clock. When it reaches the end of a line (799), it wraps to zero and advances the vertical counter. When the vertical counter reaches the end of a frame (524), it also wraps to zero:
always @(posedge clk_pix) begin
if (sx == LINE) begin
sx <= 0;
sy <= (sy == SCREEN) ? 0 : sy + 1;
end else begin
sx <= sx + 1;
end
if (rst_pix) begin
sx <= 0;
sy <= 0;
end
end
The reset check is placed after the counter logic inside the same always block. Because it executes last, it overrides the counter — ensuring a clean reset regardless of the current counter state. Placing it before the counter logic would produce identical behavior in synthesis, but placing it inside an if-else with the counter logic would create a priority that might confuse some tools.
The Math Behind the Refresh Rate
The module has no concept of “60 Hz” — the refresh rate is an emergent property of the pixel clock and the timing constants:
| Quantity | Calculation | Result |
|---|---|---|
| Pixels per line | 640 active + 16 front porch + 96 sync + 48 back porch | 800 |
| Lines per frame | 480 active + 10 front porch + 2 sync + 33 back porch | 525 |
| Clocks per frame | 800 × 525 | 420,000 |
| Frame rate | 25,000,000 Hz ÷ 420,000 | 59.52 Hz |
The pixel clock is the single input that determines all timing. In the FPGA design, 100 MHz is divided by four using a two-bit counter to produce exactly 25 MHz. In simulation, the same division happens identically in Verilog.
Every downstream module — the board background renderer, the letter sprite engine, the color logic — simply reads sx, sy, and de from this generator. They never need to know about blanking intervals or sync pulses. This separation is what makes the display pipeline composable.
When the Counters Lie: Blank Screens and Clock Violations
Wrong pixel clock. If you supply 50 MHz instead of 25 MHz, every timing interval halves. The monitor detects an unsupported horizontal frequency (62.5 kHz instead of 31.25 kHz) and displays nothing — or a rolling, distorted image. There’s no error message. The screen just goes dark.
Counter overflow. The sx register is ten bits wide (range 0–1023), but it only counts to 799. If a reset glitch allowed sx to reach 800+ without wrapping, de would stay low permanently, producing a frame with no visible pixels. The synchronous reset guard prevents this.
Metastability. The pixel clock and the system clock are in different domains. In this design, the pixel clock is derived from the system clock via a simple divider, guaranteeing a fixed phase relationship. If the clocks were independent — from separate oscillators, for example — a clock-domain crossing synchronizer would be required on every signal crossing the boundary. The divider approach eliminates this entire class of bugs.
The skill you’ve acquired isn’t specific to 640×480. Every VESA display mode — 800×600, 1024×768, 1920×1080 — follows the same structure: active region, front porch, sync pulse, back porch, repeated horizontally and vertically. Change the six parameters and the pixel clock, and this same module generates any standard VGA signal. Video timing is arithmetic, not protocol.