Kanade Electronicskanaelecom
📺

FPGA で VGA 映像出力

FPGA で 640x480 の VGA 信号を作る

FPGA で画面を出すと楽しいです。 LED が 1 個点滅するだけでも嬉しいですが、ディスプレイに自分で作った信号が映るとかなり感動します。

この記事では、VGA の 640x480 60 Hz 信号を FPGA で生成します。 HDMI ではなく VGA です。 アナログで、タイミングも素直で、オシロでも見やすい。 入門にちょうどいい化石です。

VGA 信号

VGA コネクタには、映像信号 3 本と同期信号 2 本があります。

PinFunction信号
1RedAnalog 0.7 V
2GreenAnalog 0.7 V
3BlueAnalog 0.7 V
5GNDGND
6/7/8R/G/B ReturnGND
10Sync GNDGND
13HSyncTTL
14VSyncTTL

RGB はアナログ信号です。 最大 0.7 V ぐらいを 75 Ω 終端へ出します。

HSync と VSync はデジタル信号です。 水平同期と垂直同期で、ディスプレイに「今どこを描いているか」を伝えます。

640x480 60 Hz

640x480 60 Hz の標準的なタイミングは次です。

項目水平 pixel垂直 line
表示領域640480
Front porch1610
Sync pulse962
Back porch4833
合計800525

ピクセルクロックは 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 色で持つと、

640 times 480 times 6 = 1843200 bit

約 1.8 Mbit です。 小さな FPGA の Block RAM には入りません。

そのため、入門では次のような方針を取ります。

  • タイル方式にする
  • 低解像度の内部画面を拡大する
  • 1 bit / 2 bit 色にする
  • 外部 SRAM / SDRAM を使う
  • 座標からその場で描画する

たとえば 160x120 の 6 bit 色なら、

160 times 120 times 6 = 115200 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

このモジュールから xyvisible を受け取り、別の描画モジュールで 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 は古い規格ですが、古いからこそ手で作れます。 ありがたい化石です。

参考