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
- The Raster Scanner: Two Counters Drawing an Invisible Grid
- Building the Timing Generator, Line by Line
- Step 1: Define the timing boundaries
- Step 2: Generate sync signals combinationally
- Step 3: Advance the counters
- The Math Behind the Magic: Refresh Rate From First Principles
- When the Counters Lie: Timing Violations and Blank-Screen Failures
- You Can Now Build Any VGA Resolution From Two Counters
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 does not 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 will 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.
🔵 Deep Dive: 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
Concepts you should understand:
- Binary counters (a register that increments on each clock edge)
- The difference between combinational and sequential logic
- What a clock signal is and what “posedge” means in Verilog
Tools referenced:
- Verilog HDL (IEEE 1364-2001)
- Any simulator (Verilator, Icarus Verilog, or Vivado)
File under discussion:
modules/display/simple_480p.v(72 lines)
The Raster Scanner: Two Counters 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 is 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:
stateDiagram-v2
direction LR
state "Horizontal Counter (sx)" as HZ {
[*] --> ActivePixels: sx 0–639
ActivePixels --> FrontPorch: sx 640–655
FrontPorch --> SyncPulse: sx 656–751
SyncPulse --> BackPorch: sx 752–799
BackPorch --> [*]: sx wraps to 0
}
state "Vertical Counter (sy)" as VT {
[*] --> ActiveLines: sy 0–479
ActiveLines --> VFrontPorch: sy 480–489
VFrontPorch --> VSyncPulse: sy 490–491
VSyncPulse --> VBackPorch: sy 492–524
VBackPorch --> [*]: sy wraps to 0
}
Building the Timing Generator, Line by Line
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, but 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)
🔵 Deep Dive: 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 used for the 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 // reached pixel 799?
sx <= 0; // reset horizontal
sy <= (sy == SCREEN) ? 0 // if also at line 524, reset vertical
: sy + 1; // otherwise advance to next line
end else begin
sx <= sx + 1; // normal horizontal increment
end
if (rst_pix) begin // synchronous reset takes priority
sx <= 0;
sy <= 0;
end
end
🔴 Danger: 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 Magic: Refresh Rate From First Principles
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 (pixCounter[1]) 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 of concerns is what makes the display pipeline composable.
When the Counters Lie: Timing Violations and Blank-Screen Failures
What happens if the pixel clock is wrong? 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 worse, a rolling, distorted image. There is no error message; the screen simply goes dark.
What happens if a counter overflows? 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, the de signal would remain low, producing a frame with no visible pixels. The synchronous reset guard prevents this by forcing both counters to zero on any reset assertion.
What about 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, which guarantees a fixed phase relationship. If the clocks were independent (for example, from separate oscillators), a clock-domain crossing synchronizer would be required on every signal that crosses the boundary. The divider approach eliminates this entire class of bugs.
You Can Now Build Any VGA Resolution From Two Counters
The skill you have acquired is not 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.
The deeper lesson is that video timing is not a protocol to be decoded — it is an arithmetic identity. The refresh rate is the pixel clock divided by the product of the total horizontal and vertical periods. Every other detail — sync polarity, blanking intervals, active regions — is just a comparator on a counter.