Project

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.

Board1BitSquared iCEBreaker
Timeabout 4 hours
Languageverilog
Extra parts1BitSquared 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.

One horizontal line, not to scale: 640 visible clocks, 16 front porch, 96 sync low, 48 back porch.clk_pixhcount0..639640..655656..751752..7990dehsyncrgbpatternpattern
One horizontal line, not to scale: 640 visible clocks, 16 front porch, 96 sync low, 48 back porch.

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

PLL12 to 25.125 MHzvga_sync800 x 525 counterspatternbars, chequer, blockrgb registerblack when de is lowblock positionupdates on framePmod 1A and 1BR, G, B, HS, VSclk_pixx, y, deframe pulse12 bitshsync, vsyncBTN1freeze block
Blocks in the VGA test pattern design. All logic after the PLL runs on the 25.125 MHz pixel clock.

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] and y[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_pix and de on 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.