iCEBreakerでVGAテストパターンを表示する
オープンソースフローでiCE40 UP5Kから640x480、60 Hzの映像信号を生成します。PLLで作るピクセルクロック、同期信号生成器、カラーバー、市松模様、跳ね回るブロックを12ビットVGA Pmodに出力します。
| ボード | 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です。表示領域の外では色を黒にしなければなりません。モニターはブランキングレベルを基準として使うためです。
ピクセルクロック
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を作ることはできません。
同期信号生成器とパターン
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つで、結果はフラッシュに残ります。