VGA test pattern on the iCEBreaker
Generate 640x480 at 60 Hz from an iCE40 UP5K with the open-source flow, with a PLL-derived pixel clock, a sync generator, colour bars, a chequerboard and a bouncing block on a 12-bit VGA Pmod.
| Board | 1BitSquared iCEBreaker |
|---|---|
| Time | about 4 hours |
| Language | verilog |
| Extra parts | 1BitSquared iCEBreaker, 12-bit VGA Pmod (Digilent Pmod VGA or compatible), VGA monitor and cable, USB cable |
What you will build
A monitor is the most satisfying output an FPGA can drive, and VGA is the easiest way to get there: three analogue colour lines and two sync pulses, all generated by counters. This project builds a 640x480 at 60 Hz signal on the iCEBreaker with a 12-bit VGA Pmod spanning both Pmod 1A and 1B. The picture is eight colour bars, a chequerboard across the bottom quarter, and an orange block that bounces around the screen. Button 1 freezes the block.
You will derive the 25.175 MHz pixel clock from the board’s 12 MHz crystal with the iCE40 PLL, write a sync generator whose timing is checked by a testbench, and run the Yosys, nextpnr-ice40, icepack and iceprog flow. The sync generator is the reusable part: every later video project starts from it.
What you need
- An iCEBreaker v1.0 (Lattice iCE40UP5K, SG48 package, 5,280 LUTs) with its USB cable.
- A 12-bit VGA Pmod. This project assumes a resistor-ladder Pmod with 4 bits per colour on a double-width connector, such as the Digilent Pmod VGA. The 1BitSquared 12-bit DVI Pmod is a different device that needs the pixel clock and data-enable as well, and is a good extension once this works.
- A VGA monitor, or a VGA-to-HDMI converter that accepts 640x480. Most do.
- The open-source iCE40 flow: Yosys, nextpnr-ice40, icestorm (
icepack,iceprog,icepll). The YosysHQ oss-cad-suite bundle contains all of them, as described in the Tang Nano 9K project. - Optional: Icarus Verilog and Verilator.
Design
The 640x480 timing
VGA is a raster. Each line is 640 visible pixels followed by a blanking interval in which the beam (or, on a modern panel, the scaler) returns to the left. Each frame is 480 visible lines followed by a vertical blanking interval. The industry-standard numbers at 60 Hz are:
| Parameter | Horizontal (pixels) | Vertical (lines) |
|---|---|---|
| Visible | 640 | 480 |
| Front porch | 16 | 10 |
| Sync pulse | 96 | 2 |
| Back porch | 48 | 33 |
| Total | 800 | 525 |
| Sync polarity | negative | negative |
800 pixels per line times 525 lines per frame times 60 frames per second is 25.2 Mpixel/s, and the standard pixel clock is 25.175 MHz, giving 59.94 Hz. Both sync pulses are active low. Colour must be driven to black outside the visible area, because the monitor uses the blanking level as its reference.
The pixel clock
The iCEBreaker’s crystal is 12 MHz. The UP5K PLL multiplies a reference by (DIVF + 1) / (DIVR + 1) into a voltage-controlled oscillator that must sit between 533 MHz and 1,066 MHz, then divides by 2^DIVQ. The icepll tool from icestorm searches those settings:
icepll -i 12 -o 25.175
It reports DIVR = 0, DIVF = 66, DIVQ = 5, FILTER_RANGE = 1, and an achieved output of 25.125 MHz. That is 12 MHz x 67 / 32. The error is 0.2 %, which gives a refresh rate of 59.8 Hz instead of 59.94 Hz. Monitors lock to the sync pulses rather than to an absolute clock, and anything within a percent or two is accepted without complaint. The nearest alternative above, DIVF = 67, gives 25.5 MHz and 60.7 Hz, which also works. 25.2 MHz exactly is not reachable from 12 MHz with this PLL.
Sync generator and pattern
vga_sync is two counters, 0 to 799 and 0 to 524, with the sync, data-enable and coordinate outputs registered to keep the output clean. The pattern logic reads x and y and chooses a colour. The output rgb register is forced to black whenever de is low. The block position is a pair of counters that update once per frame on the frame pulse, which happens at the first clock of vertical blanking so the block never changes mid-picture.
The design is held in reset until the PLL reports lock, then released a few clocks later. This avoids a counter starting on a clock that is still settling.
Code
Sync generator
// 640x480 at 60 Hz sync generator. Expects a pixel clock close to 25.175 MHz.
module vga_sync (
input wire clk, // pixel clock
input wire rst,
output reg hsync, // active low
output reg vsync, // active low
output reg de, // high inside the 640x480 visible area
output reg [9:0] x, // 0..639 while de is high
output reg [9:0] y, // 0..479 while de is high
output reg frame // one-cycle pulse at the start of each frame's blanking
);
// Horizontal: 640 visible, 16 front porch, 96 sync, 48 back porch = 800 clocks.
localparam [9:0] H_VISIBLE = 10'd640;
localparam [9:0] H_FP = 10'd16;
localparam [9:0] H_SYNC = 10'd96;
localparam [9:0] H_TOTAL = 10'd800;
// Vertical: 480 visible, 10 front porch, 2 sync, 33 back porch = 525 lines.
localparam [9:0] V_VISIBLE = 10'd480;
localparam [9:0] V_FP = 10'd10;
localparam [9:0] V_SYNC = 10'd2;
localparam [9:0] V_TOTAL = 10'd525;
reg [9:0] hcount;
reg [9:0] vcount;
always @(posedge clk) begin
if (rst) begin
hcount <= 10'd0;
vcount <= 10'd0;
end else if (hcount == H_TOTAL - 10'd1) begin
hcount <= 10'd0;
vcount <= (vcount == V_TOTAL - 10'd1) ? 10'd0 : vcount + 10'd1;
end else begin
hcount <= hcount + 10'd1;
end
end
// Registered outputs: one clock of latency, which the pattern generator does not care about.
always @(posedge clk) begin
hsync <= ~((hcount >= H_VISIBLE + H_FP) && (hcount < H_VISIBLE + H_FP + H_SYNC));
vsync <= ~((vcount >= V_VISIBLE + V_FP) && (vcount < V_VISIBLE + V_FP + V_SYNC));
de <= (hcount < H_VISIBLE) && (vcount < V_VISIBLE);
x <= hcount;
y <= vcount;
frame <= (hcount == 10'd0) && (vcount == V_VISIBLE);
end
endmodule
PLL wrapper
// 12 MHz in, 25.125 MHz out. Values from: icepll -i 12 -o 25.175
module pll_25mhz (
input wire clk_12m,
output wire clk_pix,
output wire locked
);
SB_PLL40_PAD #(
.FEEDBACK_PATH("SIMPLE"),
.DIVR(4'b0000), // reference divider: 12 MHz / 1
.DIVF(7'b1000010), // feedback divider 66 + 1 = 67, VCO = 804 MHz
.DIVQ(3'b101), // output divider 2^5 = 32, 804 / 32 = 25.125 MHz
.FILTER_RANGE(3'b001)
) u_pll (
.PACKAGEPIN(clk_12m),
.PLLOUTGLOBAL(clk_pix),
.LOCK(locked),
.RESETB(1'b1),
.BYPASS(1'b0)
);
endmodule
Top level and pattern
// VGA test pattern on the iCEBreaker: colour bars, a chequerboard and a bouncing block.
module top (
input wire clk, // 12 MHz crystal, pin 35
input wire btn1, // hold to freeze the block (active high on the iCEBreaker)
output wire [3:0] vga_r,
output wire [3:0] vga_g,
output wire [3:0] vga_b,
output wire vga_hs,
output wire vga_vs,
output wire [1:0] led
);
wire clk_pix;
wire locked;
pll_25mhz u_pll (.clk_12m(clk), .clk_pix(clk_pix), .locked(locked));
// Hold the design in reset until the PLL has locked, then release synchronously.
reg [3:0] rst_cnt;
reg rst;
always @(posedge clk_pix) begin
if (!locked) begin
rst_cnt <= 4'd0;
rst <= 1'b1;
end else if (rst_cnt != 4'hF) begin
rst_cnt <= rst_cnt + 4'd1;
rst <= 1'b1;
end else begin
rst <= 1'b0;
end
end
wire de, frame;
wire [9:0] x, y;
vga_sync u_sync (
.clk(clk_pix), .rst(rst), .hsync(vga_hs), .vsync(vga_vs),
.de(de), .x(x), .y(y), .frame(frame)
);
// Bouncing 32x32 block, moved once per frame.
reg [9:0] bx, by;
reg dx, dy; // 1 = moving right / down
reg [1:0] btn_sync;
always @(posedge clk_pix) btn_sync <= {btn_sync[0], btn1};
always @(posedge clk_pix) begin
if (rst) begin
bx <= 10'd100; by <= 10'd100;
dx <= 1'b1; dy <= 1'b1;
end else if (frame && !btn_sync[1]) begin
bx <= dx ? bx + 10'd1 : bx - 10'd1;
by <= dy ? by + 10'd1 : by - 10'd1;
if (bx == 10'd0) dx <= 1'b1;
if (bx == 10'd607) dx <= 1'b0; // 640 - 32 - 1
if (by == 10'd0) dy <= 1'b1;
if (by == 10'd447) dy <= 1'b0; // 480 - 32 - 1
end
end
wire in_block = (x >= bx) && (x < bx + 10'd32) && (y >= by) && (y < by + 10'd32);
// Eight 80 px colour bars in the top three quarters, a 16 px chequerboard below.
reg [11:0] bar;
always @(*) begin
case (x[9:4] / 6'd5) // 640 / 80 = 8 bars, 80 = 5 * 16
6'd0: bar = 12'hFFF; // white
6'd1: bar = 12'hFF0; // yellow
6'd2: bar = 12'h0FF; // cyan
6'd3: bar = 12'h0F0; // green
6'd4: bar = 12'hF0F; // magenta
6'd5: bar = 12'hF00; // red
6'd6: bar = 12'h00F; // blue
default: bar = 12'h000; // black
endcase
end
wire chequer = x[4] ^ y[4];
wire [11:0] lower = chequer ? 12'hAAA : 12'h333;
wire [11:0] pattern = (y < 10'd360) ? bar : lower;
reg [11:0] rgb;
always @(posedge clk_pix) begin
if (!de) rgb <= 12'h000; // must be black outside the visible area
else if (in_block) rgb <= 12'hF80; // orange block
else rgb <= pattern;
end
assign {vga_r, vga_g, vga_b} = rgb;
// LEDs: PLL lock, and the frame pulse stretched so it is visible as a 60 Hz flicker.
assign led = {locked, vga_vs};
endmodule
Testbench
The testbench measures the sync generator rather than the whole top level, so it does not need a PLL model. It counts clocks and lines and compares against the table above.
`timescale 1ns / 1ps
// Measures the sync generator against the 640x480 at 60 Hz numbers.
module tb_vga;
reg clk = 1'b0;
reg rst = 1'b1;
always #19.9 clk = ~clk; // about 25.125 MHz
wire hsync, vsync, de, frame;
wire [9:0] x, y;
vga_sync dut (.clk(clk), .rst(rst), .hsync(hsync), .vsync(vsync), .de(de), .x(x), .y(y), .frame(frame));
integer hs_low = 0, line_len = 0, de_per_line = 0, lines = 0, vs_low_lines = 0, de_lines = 0;
integer errors = 0;
task check(input [255:0] name, input integer got, input integer want);
begin
if (got !== want) begin
errors = errors + 1;
$display("FAIL: %0s = %0d, expected %0d", name, got, want);
end else begin
$display("ok: %0s = %0d", name, got);
end
end
endtask
initial begin
repeat (4) @(posedge clk);
rst = 1'b0;
// Sample on the falling clock edge so registered outputs have settled.
// Measure one line: hsync low width and line length in clocks.
@(negedge hsync); @(negedge clk);
hs_low = 0; line_len = 0;
while (hsync == 1'b0) begin hs_low = hs_low + 1; @(negedge clk); end
line_len = hs_low;
while (hsync == 1'b1) begin line_len = line_len + 1; @(negedge clk); end
check("hsync low clocks", hs_low, 96);
check("line length clocks", line_len, 800);
// Count de-high clocks on a visible line.
@(posedge de); @(negedge clk);
de_per_line = 0;
while (de == 1'b1) begin de_per_line = de_per_line + 1; @(negedge clk); end
check("de clocks per line", de_per_line, 640);
// Count hsync falling edges between two vsync falling edges.
@(negedge vsync);
lines = 0; vs_low_lines = 0; de_lines = 0;
fork
begin : line_counter
forever begin
@(negedge hsync); @(negedge clk);
lines = lines + 1;
if (vsync == 1'b0) vs_low_lines = vs_low_lines + 1;
if (y < 10'd480) de_lines = de_lines + 1;
end
end
begin
@(negedge vsync);
disable line_counter;
end
join
check("lines per frame", lines, 525);
check("vsync low lines", vs_low_lines, 2);
check("visible lines", de_lines, 480);
if (errors == 0) $display("PASS");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule
To lint the full top level with Verilator, a stub for the Lattice primitive is needed. Keep it in a sim directory so it never reaches synthesis.
// Simulation-only stand-in for the iCE40 PLL: passes the input clock straight through.
/* verilator lint_off DECLFILENAME */
/* verilator lint_off UNUSEDPARAM */
module SB_PLL40_PAD #(
parameter FEEDBACK_PATH = "SIMPLE",
parameter [3:0] DIVR = 4'b0000,
parameter [6:0] DIVF = 7'b0000000,
parameter [2:0] DIVQ = 3'b000,
parameter [2:0] FILTER_RANGE = 3'b000
) (
input wire PACKAGEPIN,
output wire PLLOUTGLOBAL,
output wire LOCK,
input wire RESETB,
input wire BYPASS
);
assign PLLOUTGLOBAL = PACKAGEPIN;
assign LOCK = RESETB & ~BYPASS;
endmodule
Pin constraints
The iCEBreaker Pmod 1A header pins 1 to 4 are FPGA pins 4, 2, 47, 45 and pins 7 to 10 are 3, 48, 46, 44. Pmod 1B pins 1 to 4 are 43, 38, 34, 31 and pins 7 to 10 are 42, 36, 32, 28. The PCF below assumes the Digilent Pmod VGA layout, with red and blue on the first connector and green plus syncs on the second. Map these to your Pmod’s pinout if it differs.
# iCEBreaker v1.0 pins for the VGA test pattern.
# Map the vga_* names to your Pmod's pinout. The numbers below assume a
# Digilent-style 12-bit VGA Pmod: red and blue on Pmod 1A, green and syncs on Pmod 1B.
set_io clk 35 # 12 MHz crystal
set_io btn1 20 # user button 1
set_io led[0] 11 # red LED
set_io led[1] 37 # green LED
# Pmod 1A: header pins 1,2,3,4 = 4,2,47,45 and 7,8,9,10 = 3,48,46,44
set_io vga_r[0] 4
set_io vga_r[1] 2
set_io vga_r[2] 47
set_io vga_r[3] 45
set_io vga_b[0] 3
set_io vga_b[1] 48
set_io vga_b[2] 46
set_io vga_b[3] 44
# Pmod 1B: header pins 1,2,3,4 = 43,38,34,31 and 7,8,9,10 = 42,36,32,28
set_io vga_g[0] 43
set_io vga_g[1] 38
set_io vga_g[2] 34
set_io vga_g[3] 31
set_io vga_hs 42
set_io vga_vs 36
Makefile
# Open-source flow for the iCEBreaker (iCE40UP5K, SG48 package)
TOP = top
SRC = top.v vga_sync.v pll_25mhz.v
all: $(TOP).bin
$(TOP).json: $(SRC)
yosys -q -p "synth_ice40 -top $(TOP) -json $(TOP).json" $(SRC)
$(TOP).asc: $(TOP).json icebreaker.pcf
nextpnr-ice40 --up5k --package sg48 --freq 25.125 \
--json $(TOP).json --pcf icebreaker.pcf --asc $(TOP).asc
$(TOP).bin: $(TOP).asc
icepack $(TOP).asc $(TOP).bin
prog: $(TOP).bin
iceprog $(TOP).bin
sim:
iverilog -g2012 -o sim.vvp tb_vga.v vga_sync.v && vvp sim.vvp
lint:
verilator --lint-only -Wall $(SRC) sim/sb_pll40_pad_stub.v --top-module top
clean:
rm -f $(TOP).json $(TOP).asc $(TOP).bin sim.vvp
.PHONY: all prog sim lint clean
Build and program
make sim # testbench: six ok lines and PASS
make lint # Verilator, no warnings
make # yosys, nextpnr-ice40, icepack
make prog # iceprog writes top.bin to the on-board flash
The equivalent commands by hand:
yosys -q -p "synth_ice40 -top top -json top.json" top.v vga_sync.v pll_25mhz.v
nextpnr-ice40 --up5k --package sg48 --freq 25.125 --json top.json --pcf icebreaker.pcf --asc top.asc
icepack top.asc top.bin
iceprog top.bin
nextpnr prints a timing report. The pixel-clock domain should close at 60 MHz or more, far above the 25.125 MHz asked for. The design uses around 150 LUTs, so there is a lot of room left for whatever you draw next.
iceprog writes to the SPI flash, so the design comes back on every power-up. If iceprog cannot open the device on Linux, install the udev rule from the icestorm repository.
Test
Plug in the Pmod before programming, then connect the monitor. Within a second of programming the monitor should report 640x480 at 60 Hz (some show 59 Hz, which is the PLL error) and display, from the left: white, yellow, cyan, green, magenta, red, blue and black bars, each 80 pixels wide, over the top three quarters of the screen. The bottom quarter is a grey chequerboard of 16-pixel squares. An orange 32-pixel square drifts diagonally and bounces off the edges. Hold BTN1 and it stops.
The green LED is driven straight from vsync, so it appears steadily lit. The red LED shows PLL lock. If the monitor says “no signal”, check the sync pins on the PCF first. If the picture is present but the colours are wrong or dim, the colour bits are on the wrong pins or in the wrong order. Swapping the Pmod around or checking its silkscreen against the PCF usually finds it.
Things to try
- Add a text overlay: an 8x8 font in block RAM and a character buffer, indexed by
x[9:3]andy[9:3]. The memories lesson covers the ROM. - Make the block a sprite: a 32x32 image in a ROM with a transparent colour.
- Draw a 1-pixel border around the visible area and confirm all four edges are on screen, which is a good check of the porch values.
- Switch to 800x600 at 60 Hz, which needs a 40 MHz pixel clock and positive sync polarity. Work out the PLL settings with
icepll -i 12 -o 40. - Replace the VGA Pmod with the 1BitSquared DVI Pmod, which needs
clk_pixanddeon extra pins and gives a digital picture.
Summary
- 640x480 at 60 Hz is 800 clocks per line and 525 lines per frame at 25.175 MHz, with 96-clock and 2-line negative sync pulses.
- The iCE40 PLL gets you 25.125 MHz from 12 MHz, and the 0.2 % error is harmless.
- A sync generator is two counters with registered outputs, and a testbench can check it exactly.
- Colour must be black during blanking.
- The whole flow is four commands, and the result lives in flash.