プロジェクト

iCEBreakerでVGAテストパターンを表示する

オープンソースフローでiCE40 UP5Kから640x480、60 Hzの映像信号を生成します。PLLで作るピクセルクロック、同期信号生成器、カラーバー、市松模様、跳ね回るブロックを12ビットVGA Pmodに出力します。

中級Latticeオープンソース更新 2026年11月13日
ボード1BitSquared iCEBreaker
所要時間約4時間
言語verilog
追加部品1BitSquared iCEBreaker、12ビットVGA Pmod(Digilent Pmod VGAまたは互換品)、VGAモニターとケーブル、USBケーブル

作るもの

FPGAが駆動できる出力の中で、モニターほど達成感のあるものはありません。そしてVGAは、そこへ至る最も簡単な道です。アナログの色信号3本と同期パルス2本、そのすべてをカウンタで生成できます。このプロジェクトでは、Pmod 1Aと1Bの両方にまたがる12ビットVGA Pmodを使い、iCEBreakerで640x480、60 Hzの信号を作ります。画面には8本のカラーバー、画面下4分の1の市松模様、そして画面内を跳ね回るオレンジ色のブロックを表示します。ボタン1を押すとブロックが止まります。

ボード上の12 MHz水晶からiCE40のPLLで25.175 MHzのピクセルクロックを導出し、テストベンチでタイミングを検証した同期信号生成器を書き、Yosys、nextpnr-ice40、icepack、iceprogのフローを実行します。再利用できるのは同期信号生成器です。以降のあらゆる映像プロジェクトはここから始まります。

必要なもの

  • iCEBreaker v1.0(Lattice iCE40UP5K、SG48パッケージ、5,280 LUT)とそのUSBケーブル。
  • 12ビットVGA Pmod。このプロジェクトでは、Digilent Pmod VGAのような、ダブル幅コネクタに各色4ビットの抵抗ラダー方式のPmodを前提としています。1BitSquaredの12ビットDVI Pmodは別のデバイスで、ピクセルクロックとデータイネーブルも必要になります。本プロジェクトが動いたあとの発展課題として適しています。
  • VGAモニター、または640x480を受け付けるVGA-HDMI変換器。ほとんどの製品が対応しています。
  • オープンソースのiCE40フロー: Yosys、nextpnr-ice40、icestorm(icepack、iceprog、icepll)。YosysHQのoss-cad-suiteバンドルにすべて含まれています。詳しくはTang Nano 9Kのプロジェクトを参照してください。
  • 任意: Icarus VerilogとVerilator。

設計

640x480のタイミング

VGAはラスター走査です。各ラインは640個の表示ピクセルに続いて、ビーム(最近のパネルではスケーラー)が左端に戻るためのブランキング期間があります。各フレームは480本の表示ラインに続いて垂直ブランキング期間があります。60 Hzでの業界標準の数値は次のとおりです。

パラメータ 水平(ピクセル) 垂直(ライン)
表示領域 640 480
フロントポーチ 16 10
同期パルス 96 2
バックポーチ 48 33
合計 800 525
同期極性 負 負

1ラインあたり800ピクセル、1フレームあたり525ライン、毎秒60フレームを掛けると25.2 Mピクセル/sになり、標準のピクセルクロックは25.175 MHzで、59.94 Hzが得られます。同期パルスはどちらもアクティブLowです。表示領域の外では色を黒にしなければなりません。モニターはブランキングレベルを基準として使うためです。

1本の水平ライン(縮尺は正確ではありません): 表示640クロック、フロントポーチ16、同期Low 96、バックポーチ48。clk_pixhcount0..639640..655656..751752..7990dehsyncrgbpatternpattern
1本の水平ライン(縮尺は正確ではありません): 表示640クロック、フロントポーチ16、同期Low 96、バックポーチ48。

ピクセルクロック

iCEBreakerの水晶は12 MHzです。UP5KのPLLは基準クロックを(DIVF + 1) / (DIVR + 1)倍して、533 MHzから1,066 MHzの範囲に収まる必要がある電圧制御発振器に入れ、それを2^DIVQで分周します。icestormのicepllツールがこの設定を探索してくれます。

icepll -i 12 -o 25.175

結果はDIVR = 0、DIVF = 66、DIVQ = 5、FILTER_RANGE = 1で、得られる出力は25.125 MHzです。これは12 MHz x 67 / 32にあたります。誤差は0.2%で、リフレッシュレートは59.94 Hzではなく59.8 Hzになります。モニターは絶対的なクロックではなく同期パルスにロックするので、1、2%以内なら問題なく受け付けられます。すぐ上の候補であるDIVF = 67では25.5 MHz、60.7 Hzになり、これも動作します。12 MHzからこのPLLでちょうど25.2 MHzを作ることはできません。

同期信号生成器とパターン

PLL12→25.125 MHzvga_sync800 x 525カウンタパターンバー、市松、ブロックrgbレジスタdeがLowなら黒ブロック位置フレームごとに更新Pmod 1Aと1BR, G, B, HS, VSclk_pixx, y, deframeパルス12ビットhsync, vsyncBTN1ブロック停止
VGAテストパターン設計のブロック。PLL以降のロジックはすべて25.125 MHzのピクセルクロックで動作します。

vga_syncは0から799までと0から524までの2つのカウンタで、同期、データイネーブル、座標の各出力をレジスタで受けて出力をきれいに保っています。パターンロジックはxとyを読んで色を選びます。出力のrgbレジスタはdeがLowの間は強制的に黒になります。ブロック位置は1組のカウンタで、frameパルスのたびに1フレームに1回更新されます。このパルスは垂直ブランキングの最初のクロックで発生するので、ブロックが画面の途中で動くことはありません。

設計はPLLがロックを報告するまでリセット状態に保たれ、その数クロック後に解除されます。これにより、まだ安定していないクロックでカウンタが動き始めるのを防ぎます。

コード

同期信号生成器

// 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ラッパー

// 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

トップレベルとパターン

// 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

テストベンチ

このテストベンチはトップレベル全体ではなく同期信号生成器を測定するので、PLLのモデルは不要です。クロックとラインを数え、上の表と比較します。

`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

Verilatorでトップレベル全体をリントするには、Latticeプリミティブのスタブが必要です。合成に混入しないよう、simディレクトリに置いてください。

// 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

ピン制約

iCEBreakerのPmod 1Aヘッダーのピン1から4はFPGAピン4、2、47、45、ピン7から10は3、48、46、44です。Pmod 1Bのピン1から4は43、38、34、31、ピン7から10は42、36、32、28です。以下のPCFはDigilent Pmod VGAの配置を前提としており、赤と青が1つ目のコネクタ、緑と同期信号が2つ目のコネクタにあります。お使いのPmodのピン配置が異なる場合は、それに合わせて対応付けてください。

# 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

ビルドと書き込み

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

手作業で実行する場合の同等のコマンドは次のとおりです。

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はタイミングレポートを出力します。ピクセルクロック領域は60 MHz以上で収束するはずで、要求した25.125 MHzをはるかに上回ります。この設計が使うのは約150 LUTなので、次に描きたいものに使える余裕はたっぷりあります。

iceprogはSPIフラッシュに書き込むので、設計は電源を入れるたびに復帰します。Linuxでiceprogがデバイスを開けない場合は、icestormリポジトリのudevルールをインストールしてください。

テスト

書き込みの前にPmodを装着し、モニターを接続します。書き込みから1秒以内にモニターが640x480、60 Hz(PLLの誤差により59 Hzと表示されるものもあります)を報告し、画面上部4分の3に左から白、黄、シアン、緑、マゼンタ、赤、青、黒の各80ピクセル幅のバーが表示されるはずです。下4分の1は16ピクセル角のグレーの市松模様です。オレンジ色の32ピクセル角のブロックが斜めに移動し、端で跳ね返ります。BTN1を押している間は止まります。

緑のLEDはvsyncで直接駆動されているので、常時点灯しているように見えます。赤のLEDはPLLのロックを示します。モニターが「信号なし」と表示する場合は、まずPCFの同期信号ピンを確認してください。画像は出るが色がおかしい、または暗い場合は、色のビットが別のピンに出ているか、順序が違っています。Pmodを差し替えてみるか、シルク印刷をPCFと照合すれば、たいてい原因が見つかります。

試してみること

  • テキストオーバーレイを追加する: ブロックRAMに8x8フォントと文字バッファを置き、x[9:3]とy[9:3]で参照します。メモリのレッスンでROMを扱っています。
  • ブロックをスプライトにする: 透明色を持つ32x32の画像をROMに入れます。
  • 表示領域の周囲に1ピクセルの枠を描き、4辺すべてが画面内に入っていることを確認します。ポーチの値の良い検証になります。
  • 800x600、60 Hzに切り替える: 40 MHzのピクセルクロックと正の同期極性が必要です。icepll -i 12 -o 40でPLLの設定を求めてください。
  • VGA Pmodを1BitSquaredのDVI Pmodに置き換える: 追加のピンにclk_pixとdeが必要で、デジタルの映像が得られます。

まとめ

  • 640x480、60 Hzは25.175 MHzで1ライン800クロック、1フレーム525ラインで、負極性の同期パルスは96クロックと2ラインです。
  • iCE40のPLLで12 MHzから25.125 MHzが得られ、0.2%の誤差は問題になりません。
  • 同期信号生成器はレジスタ出力を持つ2つのカウンタで、テストベンチで厳密に検証できます。
  • ブランキング中は色を黒にしなければなりません。
  • フロー全体はコマンド4つで、結果はフラッシュに残ります。