An XVGA (more often called XGA) 1024x768 controller is a pair of counters and a few comparators. At 60 Hz with a 65 MHz pixel clock, a horizontal counter runs from 0 to 1343 and a vertical counter from 0 to 805; comparators on those counts switch the blanking signals and the active-low horizontal and vertical sync pulses at fixed positions. Anything that draws pixels then reads the counters and drives the RGB outputs during the visible 1024 by 768 area.
This post walks through the block diagram, the timing numbers, a corrected and simulated Verilog module and the 3-bit colour chart, with an example of what the screen shows.
Key takeaways
- 1024x768 at 60 Hz uses a 65 MHz pixel clock, 1344 clocks per line and 806 lines per frame, giving about 60.004 Hz.
- Each line is 1024 visible pixels, 24 front porch, 136 sync and 160 back porch. Each frame is 768 visible lines, 3 front porch, 6 sync and 29 back porch.
- Both sync pulses are active low for this mode.
- Register the sync, blank and RGB outputs on the same clock so they stay aligned at the connector.
- The original 2008 code had several compile errors; the corrected module below compiles and was checked in simulation.
How a VGA-style timing generator works
A VGA monitor (or a VGA-to-HDMI converter) expects analog colour signals plus two digital timing signals. The display scans each line left to right and each frame top to bottom. Between lines it needs a horizontal sync pulse to start the next line, and between frames a vertical sync pulse to return to the top. The intervals before and after each sync pulse, the front porch and back porch, are periods when the colour outputs must be black.
The controller in the block diagram has three parts:
- Horizontal counter (hcount): counts pixel clocks from 0 to 1343, then wraps.
- Vertical counter (vcount): counts lines from 0 to 805, advancing once each time hcount wraps.
- Comparators and flags: detect specific counts to turn blanking and sync on and off. Instead of decoding a range every cycle, the code sets and clears a flag at single count values, which keeps the logic small.
1024x768 at 60 Hz timing numbers
These are the VESA Display Monitor Timing (DMT) values for 1024x768 at 60 Hz.
| Parameter | Horizontal (pixels) | Vertical (lines) |
|---|---|---|
| Visible area | 1024 (counts 0 to 1023) | 768 (lines 0 to 767) |
| Front porch | 24 (1024 to 1047) | 3 (768 to 770) |
| Sync pulse | 136 (1048 to 1183) | 6 (771 to 776) |
| Back porch | 160 (1184 to 1343) | 29 (777 to 805) |
| Total | 1344 | 806 |
| Sync polarity | Negative (active low) | Negative (active low) |
Worked example: frame rate and line rate
- Line rate = 65,000,000 / 1344 = 48.363 kHz, so one line takes 20.68 us.
- Frame rate = 48,363 / 806 = 60.004 Hz.
- Clocks per frame = 1344 x 806 = 1,083,264, of which 1024 x 768 = 786,432 (about 73%) are visible.
- The original code comment mentions a 64.8 MHz clock, which is what some FPGA clock synthesisers produce when they cannot hit 65 MHz exactly. At 64.8 MHz the frame rate is 59.82 Hz, well within what monitors accept.
The corrected Verilog module
The listing below keeps the structure of the original post: the same counters, the same set and clear flags, the same horizontal positions and the same two pixel generators (a white border and colour bars). It is now a single module that compiles cleanly.
// 1024x768 at 60 Hz (VESA DMT) timing generator, 65 MHz pixel clock
module xvga (
input clk, // 65 MHz pixel clock
input pattern, // 0 = white border, 1 = colour bars
output reg [10:0] hcount, // pixel number on current line, 0 to 1343
output reg [9:0] vcount, // line number, 0 to 805
output reg hsync, // active low
output reg vsync, // active low
output reg blank, // high outside the 1024x768 area
output reg [2:0] rgb // {R,G,B}, one bit each
);
reg hblank, vblank;
// horizontal: 1024 visible + 24 front porch + 136 sync + 160 back porch = 1344
wire hblankon = (hcount == 11'd1023); // last visible pixel
wire hsyncon = (hcount == 11'd1047); // sync low from pixel 1048
wire hsyncoff = (hcount == 11'd1183); // sync high again from pixel 1184
wire hreset = (hcount == 11'd1343); // end of line
// vertical: 768 visible + 3 front porch + 6 sync + 29 back porch = 806
wire vblankon = hreset & (vcount == 10'd767);
wire vsyncon = hreset & (vcount == 10'd770); // sync low from line 771
wire vsyncoff = hreset & (vcount == 10'd776); // sync high again from line 777
wire vreset = hreset & (vcount == 10'd805); // end of frame
wire next_hb = hreset ? 1'b0 : hblankon ? 1'b1 : hblank;
wire next_vb = vreset ? 1'b0 : vblankon ? 1'b1 : vblank;
initial begin // simulation start-up values; on an FPGA use a reset or init values
hcount = 0; vcount = 0; hsync = 1; vsync = 1; hblank = 0; vblank = 0;
blank = 0; rgb = 0;
end
always @(posedge clk) begin
hcount <= hreset ? 11'd0 : hcount + 11'd1;
hblank <= next_hb;
hsync <= hsyncon ? 1'b0 : hsyncoff ? 1'b1 : hsync;
vcount <= hreset ? (vreset ? 10'd0 : vcount + 10'd1) : vcount;
vblank <= next_vb;
vsync <= vsyncon ? 1'b0 : vsyncoff ? 1'b1 : vsync;
blank <= next_vb | (next_hb & ~hreset);
end
// one pixel generator, registered, so rgb lines up with hsync and vsync
always @(posedge clk) begin
if (next_vb | (next_hb & ~hreset))
rgb <= 3'b000;
else if (pattern)
rgb <= hcount[9:7]; // eight vertical bars, 128 pixels each
else
rgb <= (hcount == 11'd1023 || hcount == 11'd0 ||
vcount == 10'd767 || vcount == 10'd0) ? 3'b111 : 3'b000;
end
endmodule
What was fixed in the 2008 code
- rgb was not in the port list and was declared only as an output wire, yet assigned in an always block. It is now a port declared as output reg.
- Two always blocks drove rgb. A reg may be assigned from only one always block in synthesisable code. The border and colour-bar generators are now merged into one block, selected by a new pattern input.
- The border used 640x480 values (hcount 639, vcount 479), left over from a smaller VGA design, so the border appeared in the wrong place. It now uses 1023 and 767.
- Vertical sync position: the original turned vsync on after line 776 and off after line 782, which is 6 lines wide but 9 lines of front porch instead of the VESA 3. Most monitors still lock, but the corrected code uses 770 and 776 so it matches the VESA table.
- Missing endmodule and the unused blank register have been fixed; blank is now an output, useful for DACs and HDMI encoders that need a data-enable signal.
- Start-up values: an initial block gives defined values in simulation. On an FPGA, use register initial values or a reset; in an ASIC, use a reset.
Colour chart
With one bit each for red, green and blue, the three bits give eight colours:
| RGB | Colour |
|---|---|
| 000 | Black |
| 001 | Blue |
| 010 | Green |
| 011 | Cyan |
| 100 | Red |
| 101 | Magenta |
| 110 | Yellow |
| 111 | White |
In the colour-bar pattern, rgb = hcount[9:7], so the colour changes every 128 pixels and the 1024-pixel line shows all eight colours from black on the left to white on the right. The original code used hcount[8:6], which gives 64-pixel bars that repeat twice across the screen; either works, and you can change the slice to taste.
Example pixel output
A self-checking testbench
This testbench runs one full frame and checks the totals: clocks per frame, number of hsync pulses, hsync width, vsync width in lines and the number of visible pixels.
`timescale 1ns/1ps
module xvga_tb;
reg clk = 0;
always #7.692 clk = ~clk; // about 65 MHz
wire [10:0] hcount; wire [9:0] vcount; wire hsync, vsync, blank; wire [2:0] rgb;
xvga dut (.clk(clk), .pattern(1'b1), .hcount(hcount), .vcount(vcount),
.hsync(hsync), .vsync(vsync), .blank(blank), .rgb(rgb));
integer clks = 0, hlow = 0, lines = 0, vlow_lines = 0, visible = 0, errors = 0;
integer hsync_starts = 0;
reg hsync_d = 1;
always @(posedge clk) begin
clks = clks + 1;
if (!hsync) hlow = hlow + 1;
if (!blank) visible = visible + 1;
if (hsync_d && !hsync) begin
hsync_starts = hsync_starts + 1;
if (!vsync) vlow_lines = vlow_lines + 1;
end
hsync_d = hsync;
end
initial begin
// run exactly one frame: 1344 x 806 clocks
repeat (1344*806) @(posedge clk);
#1;
$display("clocks per frame = %0d (expect 1083264)", clks);
$display("hsync pulses = %0d (expect 806)", hsync_starts);
$display("hsync low clocks = %0d per line (expect 136)", hlow / hsync_starts);
$display("lines with vsync low = %0d (expect 6)", vlow_lines);
$display("visible pixels = %0d (expect 786432)", visible);
$display("frame rate = %f Hz", 65.0e6 / clks);
if (clks != 1083264 || hsync_starts != 806 || hlow != 136*806 || vlow_lines != 6 || visible != 786432)
$display("FAIL"); else $display("PASS");
$finish;
end
endmodule
Output from Icarus Verilog 12.0:
clocks per frame = 1083264 (expect 1083264)
hsync pulses = 806 (expect 806)
hsync low clocks = 136 per line (expect 136)
lines with vsync low = 6 (expect 6)
visible pixels = 786432 (expect 786432)
frame rate = 60.003840 Hz
PASS
Hooking it up on an FPGA board
- Pixel clock: generate 65 MHz (or as close as you can) with a PLL or MMCM from the board oscillator.
- Colour outputs: with 1 bit per colour, drive each VGA colour pin through a resistor so a logic high gives about 0.7 V into the monitor's 75 ohm termination. Boards with resistor-ladder DACs give more bits per colour; widen rgb to match.
- Sync outputs: drive hsync and vsync directly as 3.3 V logic.
- Frame buffer: to show an image, use hcount and vcount as the address into a block RAM, and remember that the RAM read adds a clock of latency, which you must also add to hsync, vsync and blank.
Common mistakes
- Leaving colour outputs active during blanking. Many monitors use the porch level as their black reference, so the picture looks washed out or unstable.
- Mixing registered and unregistered outputs, which shifts RGB by a pixel relative to sync.
- Using the wrong sync polarity. Different modes use different polarities; 1024x768 at 60 Hz uses negative on both.
- Driving one reg from two always blocks, as in the original listing. Simulators may accept it with odd results; synthesis tools reject it or create conflicting drivers.
FAQ
Is it XVGA or XGA?
1024x768 is usually called XGA (Extended Graphics Array, from IBM in 1990). XVGA is an older, less common name for the same resolution, used in some course material.
Can I use a 50 MHz or 100 MHz board clock directly?
Not for this mode. 1024x768 at 60 Hz needs about 65 MHz. With a 50 MHz clock, 800x600 at 72 Hz is a standard mode that fits exactly.
Why count to 1343 and not 1344?
The counter starts at 0, so values 0 to 1343 are 1344 clocks.
Does this work through a VGA-to-HDMI adapter?
Usually yes, as long as the timing matches a standard mode. For a native HDMI or DVI output you also need a TMDS encoder, which uses blank as its data-enable input.



I really appreciate you for all the valuable information that you are providing us through your blog.
ReplyDelete