FPGA で画面を出すと楽しいです。 LED が 1 個点滅するだけでも嬉しいですが、ディスプレイに自分で作った信号が映るとかなり感動します。
この記事では、VGA の 640x480 60 Hz 信号を FPGA で生成します。 HDMI ではなく VGA です。 アナログで、タイミングも素直で、オシロでも見やすい。 入門にちょうどいい化石です。
VGA 信号
VGA コネクタには、映像信号 3 本と同期信号 2 本があります。
| Pin | Function | 信号 |
|---|---|---|
| 1 | Red | Analog 0.7 V |
| 2 | Green | Analog 0.7 V |
| 3 | Blue | Analog 0.7 V |
| 5 | GND | GND |
| 6/7/8 | R/G/B Return | GND |
| 10 | Sync GND | GND |
| 13 | HSync | TTL |
| 14 | VSync | TTL |
RGB はアナログ信号です。 最大 0.7 V ぐらいを 75 Ω 終端へ出します。
HSync と VSync はデジタル信号です。 水平同期と垂直同期で、ディスプレイに「今どこを描いているか」を伝えます。
640x480 60 Hz
640x480 60 Hz の標準的なタイミングは次です。
| 項目 | 水平 pixel | 垂直 line |
|---|---|---|
| 表示領域 | 640 | 480 |
| Front porch | 16 | 10 |
| Sync pulse | 96 | 2 |
| Back porch | 48 | 33 |
| 合計 | 800 | 525 |
ピクセルクロックは 25.175 MHz です。 ただし、FPGA 工作では 25 MHz でも多くのディスプレイは映ります。 厳密な機器ではダメかもしれませんが、机の上の実験ならだいたいいけます。
水平同期と垂直同期は負論理です。 Sync pulse の期間だけ Low にします。
カウンタで座標を作る
VGA 出力は、水平カウンタと垂直カウンタで作れます。
localparam H_VISIBLE = 640;
localparam H_FRONT = 16;
localparam H_SYNC = 96;
localparam H_BACK = 48;
localparam H_TOTAL = 800;
localparam V_VISIBLE = 480;
localparam V_FRONT = 10;
localparam V_SYNC = 2;
localparam V_BACK = 33;
localparam V_TOTAL = 525;
reg [9:0] h_count = 0;
reg [9:0] v_count = 0;
always @(posedge pix_clk) begin
if (h_count == H_TOTAL - 1) begin
h_count <= 0;
if (v_count == V_TOTAL - 1) begin
v_count <= 0;
end else begin
v_count <= v_count + 1'b1;
end
end else begin
h_count <= h_count + 1'b1;
end
end
h_count が 0 から 799 まで回ります。
1 行が終わるたびに v_count が 1 増えます。
v_count は 0 から 524 まで回ります。
これで、画面全体の走査位置が作れます。
表示領域
実際に色を出すのは表示領域の中だけです。
wire visible = (h_count < H_VISIBLE) && (v_count < V_VISIBLE);
wire [9:0] x = h_count;
wire [9:0] y = v_count;
表示領域外では RGB を 0 にします。
assign red = visible ? pixel_red : 2'b00;
assign green = visible ? pixel_green : 2'b00;
assign blue = visible ? pixel_blue : 2'b00;
最初は座標で色を決めると分かりやすいです。
wire [1:0] pixel_red = x[7:6];
wire [1:0] pixel_green = y[7:6];
wire [1:0] pixel_blue = x[5:4] ^ y[5:4];
これだけで謎のグラデーションっぽい画面が出ます。 ちゃんと映るとだいぶ嬉しい。
同期信号
HSync は、表示領域と front porch のあとに Low にします。
wire hsync_start = H_VISIBLE + H_FRONT;
wire hsync_end = H_VISIBLE + H_FRONT + H_SYNC;
assign hsync = ~((h_count >= hsync_start) && (h_count < hsync_end));
VSync も同じです。
wire vsync_start = V_VISIBLE + V_FRONT;
wire vsync_end = V_VISIBLE + V_FRONT + V_SYNC;
assign vsync = ~((v_count >= vsync_start) && (v_count < vsync_end));
同期信号の極性を間違えると映らないことがあります。 映っても位置が変だったりします。 まずは 640x480 60 Hz の負論理で出します。
RGB DAC
VGA の RGB はアナログ 0.7 V です。 FPGA の出力ピンは 3.3 V のデジタルです。 そのままディスプレイへ突っ込むのはよくないです。
簡単には抵抗 DAC を作ります。 各色 2 bit にして、64 色表示にします。
FPGA bit1 --- R1 ---+
+---- VGA R
FPGA bit0 --- R2 ---+
|
75 Ω (monitor)
|
GND
VGA 入力側には 75 Ω 終端がある前提です。 FPGA 側の抵抗と合わせて、最大電圧が 0.7 V 付近になるようにします。
2 bit の重み付き DAC なら、抵抗比をだいたい 1
にします。 たとえば上位 bit に 470 Ω、下位 bit に 1 kΩ などです。 正確な色が欲しいなら計算しますが、まず映すだけなら多少雑でも見えます。ただし、FPGA ピンの電流制限は見ます。 全色全 bit を High にしたときの電流が、I/O バンクの制限を超えないようにします。
ピクセルクロック
640x480 60 Hz のピクセルクロックは 25.175 MHz です。 FPGA ボードのクロックが 12 MHz や 27 MHz の場合、そのままでは合いません。
PLL を使って 25 MHz 付近を作ります。
iCE40 なら icepll、Gowin なら Gowin PLL のプリミティブや IP 相当を使います。
実験では 25 MHz でも映ることが多いです。 Tang Nano 9K の 27 MHz をそのまま使うと、全体が少し速くなります。 ディスプレイによっては同期してくれますが、安定させるなら PLL を使います。
ここで「映ったから正しい!」と思うと、別のモニタで映らなくなります。 映像信号は相手が許してくれているだけのことが多いです。
フレームバッファ
座標から直接色を作るだけなら、メモリは要りません。 しかし、画像やゲーム画面を出すならフレームバッファが欲しくなります。
640x480 を 6 bit 色で持つと、
約 1.8 Mbit です。 小さな FPGA の Block RAM には入りません。
そのため、入門では次のような方針を取ります。
- タイル方式にする
- 低解像度の内部画面を拡大する
- 1 bit / 2 bit 色にする
- 外部 SRAM / SDRAM を使う
- 座標からその場で描画する
たとえば 160x120 の 6 bit 色なら、
かなり現実的になります。 4 倍に拡大して 640x480 に出せば、レトロゲームっぽい画面になります。
最小モジュール
まとめると、VGA コントローラはこんな形になります。
module vga_640x480(
input wire pix_clk,
output wire hsync,
output wire vsync,
output wire visible,
output wire [9:0] x,
output wire [9:0] y
);
localparam H_VISIBLE = 640;
localparam H_FRONT = 16;
localparam H_SYNC = 96;
localparam H_BACK = 48;
localparam H_TOTAL = 800;
localparam V_VISIBLE = 480;
localparam V_FRONT = 10;
localparam V_SYNC = 2;
localparam V_BACK = 33;
localparam V_TOTAL = 525;
reg [9:0] h_count = 0;
reg [9:0] v_count = 0;
always @(posedge pix_clk) begin
if (h_count == H_TOTAL - 1) begin
h_count <= 0;
if (v_count == V_TOTAL - 1) begin
v_count <= 0;
end else begin
v_count <= v_count + 1'b1;
end
end else begin
h_count <= h_count + 1'b1;
end
end
assign visible = (h_count < H_VISIBLE) && (v_count < V_VISIBLE);
assign x = h_count;
assign y = v_count;
assign hsync = ~((h_count >= H_VISIBLE + H_FRONT) &&
(h_count < H_VISIBLE + H_FRONT + H_SYNC));
assign vsync = ~((v_count >= V_VISIBLE + V_FRONT) &&
(v_count < V_VISIBLE + V_FRONT + V_SYNC));
endmodule
このモジュールから x、y、visible を受け取り、別の描画モジュールで RGB を作る構成にすると見通しがよいです。
assign rgb = visible ? draw_rgb : 6'b000000;
描画とタイミング生成を分けると、あとでゲームや文字表示へ拡張しやすいです。
デバッグ
映らないときは、順番に見ます。
- ピクセルクロックが出ているか
- HSync / VSync の周波数が合っているか
- 同期信号の極性が合っているか
- RGB が表示領域外で 0 になっているか
- DAC 抵抗値が極端におかしくないか
- GND がちゃんとつながっているか
- コネクタのピン番号を間違えていないか
HSync はだいたい 31.5 kHz、VSync はだいたい 60 Hz です。 オシロやロジアナでまずここを見ます。
同期が正しければ、真っ黒でもモニタ側は信号を認識することが多いです。 「信号なし」なら同期から疑う。 「黒画面」なら RGB や表示領域を疑う。
まとめ
VGA 出力は、FPGA の入門課題としてかなり良いです。
- 水平・垂直カウンタで座標を作る
- 表示領域だけ RGB を出す
- HSync / VSync は決まった期間だけ Low にする
- RGB は抵抗 DAC で 0.7 V 付近へ落とす
- フルフレームバッファは小 FPGA には重い
LED 点滅の次に、画面が出ると一気に「回路を作っている」感が出ます。 VGA は古い規格ですが、古いからこそ手で作れます。 ありがたい化石です。