featured image

How a 25 MHz Clock Becomes 307,200 Pixels on a Screen

A deep dive into the `simple_480p` VGA timing generator, revealing how two counters and a handful of comparators produce the exact sequence of signals required to drive a 640×480 display at 60 Hz — all from first principles in Verilog.

Published

Fri Aug 08 2025

Technologies Used

FPGA Verilog
Beginner 16 minutes

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:

QuantityCalculationResult
Pixels per line640 active + 16 front porch + 96 sync + 48 back porch800
Lines per frame480 active + 10 front porch + 2 sync + 33 back porch525
Clocks per frame800 × 525420,000
Frame rate25,000,000 Hz ÷ 420,00059.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.

We respect your privacy.

← View All Tutorials

Related Projects

    Ask me anything!