🏩

Zynq+FPGAでグラフィック表示回路設計

2023/05/05に公開

Zynq+FPGAでグラフィック表示回路をつくる

はじめに

https://amzn.to/40isQyG

本投稿では本書9章~を実施したメモを書きます

環境

FPGA : Xilinx Zybo Z7-20
OS : WSL2 Ubuntu20.04
開発環境 : Vivado ML edition 2022.1 Linux版

やりたいこと

VivadoでHDLを書いてグラフィックをHDMIから表示するIPを作る

下記の要件を満たすものにする

  • DDR3メモリ上の画像を表示する
  • 解像度VGA(後ほど拡張する)
  • RGB24bit
  • Zynqから制御可能なIPとして構築
  • 画素の表示アドレス設定はZynq経由でGPIOから制御する

グラフィック表示IPができたらVitis HLSで作成したビットブロック描画回路と組み合わせる
https://zenn.dev/ryo_tan/articles/e813da5c5faf7c

AXI bus制御方法

  • ステートマシンで制御
  • HALT: 1画面が始まるまでwait->Dispstart
  • SETADDR: ARチャネルでアドレス発行
  • READING: VRAM Read-> FIFO Write
  • WAITING: FIFOが満杯の間wait

実装

例によってソースコードはここから
https://www.shuwasystem.co.jp/support/7980html/6326.html

VivadoでProjectを作成してソースコードをimportするとこのヒエラルキーになる
Display.vの最上階層で各ブロックコードのIFをつなぎ合わせている
以下のソースコードの各ブロック動作について記述する

グラフィック表示回路用のV Blanking検出用コード

VGAディスプレイのVBLANK期間を検出するためのVerilogモジュールで、VBLANK期間中に画像データの更新が行われる

入力:

  • ACLK: クロック信号
  • ARST: 非同期リセット信号(アクティブハイ)
  • VGA_VS: VGA 垂直同期信号
  • CLRVBLNK: VBLANK 信号クリア用(アクティブハイ)

出力:

  • VBLANK: 垂直ブランク期間中を示す(アクティブハイ)

内部信号:

  • vblank_ff: 3ビット配列FF,VGA_VS信号エッジ検出用。
  • set_vblank: vblank_ff[2:1] が 2'b10 の場合、VGA_VS 立ち上がりエッジ検出。

主要処理:
クロック立ち上がりエッジで vblank_ff各ビットにVGA_VS信号値シフト
リセットアクティブ時、vblank_ff を3'b111にリセット

set_vblank:VGA_VS立ち上がりエッジ検出用
vblank_ff[2:1]が2'b10のとき、立ち上がりエッジを検出する
-> vblank_ffはARSTがLowの場合VGA_VSの値をvblank_ffの0bit目に値を格納し、それを毎クロックでシフトする。従って過去3サンプル分の論理を監視し、VGS_VSがアクティブハイを検出したらset_vblankフラグをアクティブハイにする。後は後述の通り

VBLANK信号は別ブロックで常時更新される。リセットアクティブ、またはCLRVBLNKアクティブ時、VBLANKクリア。set_vblankアクティブ時、VBLANKをセットする

module disp_flag
  (
    input               ACLK,
    input               ARST,
    input               VGA_VS,
    input               CLRVBLNK,
    output  reg         VBLANK
    );

reg [2:0]   vblank_ff;

always @( posedge ACLK ) begin
    if ( ARST )
        vblank_ff <= 3'b111;
    else begin
        vblank_ff[0] <= VGA_VS;
        vblank_ff[1] <= vblank_ff[0];
        vblank_ff[2] <= vblank_ff[1];
    end
end

assign set_vblank = (vblank_ff[2:1] == 2'b10);

always @( posedge ACLK ) begin
    if ( ARST )
        VBLANK <= 1'b0;
    else if ( CLRVBLNK )
        VBLANK <= 1'b0;
    else if ( set_vblank )
        VBLANK <= 1'b1;
end

endmodule

ディスプレイコントローラステートマシン

  • ARST Activeでcur(current)=HALT状態にセット
  • それ以外はcurの値がnxt(next)にセットされる
  • dispstart ActiveでSETADDRに遷移
  • SETADDR状態でARREADY ActiveでREADINGに遷移
  • READING状態でRLAST/RVALID/RREADYを監視し、All Activeになると各条件に応じて遷移
    • dispend ->Active : HALT
    • FIFOREADY ->NonActive : WAITING
    • その他 : SETADDR
  • WAITING状態からはFIFOREADYがActiveになるまで遷移しない
always @( posedge ACLK ) begin
    if ( ARST )
        cur <= HALT;
    else
        cur <= nxt;
end

always @* begin
    case ( cur )
        HALT:       if ( dispstart )
                        nxt = SETADDR;
                    else
                        nxt = HALT;
        SETADDR:    if ( ARREADY )
                        nxt = READING;
                    else
                        nxt = SETADDR;
        READING:    if ( RLAST & RVALID & RREADY ) begin
                        if ( dispend )
                            nxt = HALT;
                        else if ( !FIFOREADY )
                            nxt = WAITING;
                        else
                            nxt = SETADDR;
                    end
                    else
                        nxt = READING;
        WAITING:    if ( FIFOREADY )
                        nxt = SETADDR;
                    else
                        nxt = WAITING;
        default:    nxt = HALT;
    endcase
end

VRAMリード

  • axistart_ff : AXISTART監視用。毎クロックデータをシフトさせて過去3CLK分を監視する
  • DISPON(input)がHighかつaxistart_ff=01になったらdispstartがActiveになる
reg [2:0]   axistart_ff;

always @( posedge ACLK ) begin
    if ( ARST )
        axistart_ff <= 3'b000;
    else begin
        axistart_ff[0] <= AXISTART;
        axistart_ff[1] <= axistart_ff[0];
        axistart_ff[2] <= axistart_ff[1];
    end
end

wire dispstart = DISPON & (axistart_ff[2:1] == 2'b01);
  • 1.ARST Active 2.ステート:HALTかつdispstart=Active時 のいずれかでアドレスリセット
  • ARVALIDとARREADYがActive時にaddrcntに0x80(=bin10000000)を足し合わせる
    これにより次の読み出しアドレスを指定する。今回は8Byte単位で読み出している
always @( posedge ACLK ) begin
    if ( ARST )
        addrcnt <= 30'b0;
    else if ( cur==HALT && dispstart )
        addrcnt <= 30'b0;
    else if ( ARVALID & ARREADY )
        addrcnt <= addrcnt + 30'h80;
end
  • 今回はVGA解像度なので640×480、1画素は24bitだが32bit最上位8bitを未使用として4Byteで1画素としている。
     従って640×480×4Byteでディスプレイの最大Pixel数VGA_MAXを定義している
  • VGA_MAXとaddrcntが一致した際にdispendをActiveにすることでステートマシンを次の状態に遷移させる(今回はHALT)
localparam integer VGA_MAX = 30'd640 * 30'd480 * 30'd4;
assign dispend = (addrcnt == VGA_MAX);

FIFOリードタイミング制御ブロック

VRAMからデータを読み出し->FIFOにデータを格納した後、ディスプレイに表示するためのブロック
ここではReadが行われるタイミングをFIFORD信号で制御している

  • rdstart: FIFOからReadするタイミングの開始点を指定する信号
     HFP+HWidth+HBPから3を引いている理由はディスプレイ側のタイミング制約と予想

  • rdend: FIFO Readの終了点 こちらも同様に3を引いている

  • Vertival Sync期間でのRead禁止を入れている(VFP+VSA+VBPの間FIFORD=0)

  • DISPON信号がActiveかつHCNT=rdstart(=HBlank期間終了)でActive

  • FIFORDに対して1CLK分遅らせてDisp_enable信号としている

wire [9:0] rdstart = HFRONT + HWIDTH + HBACK - 10'd3;
wire [9:0] rdend   = HPERIOD - 10'd3;

always @( posedge PCK ) begin
    if ( PRST )
        FIFORD <= 1'b0;
    else if ( VCNT < VFRONT + VWIDTH + VBACK )
        FIFORD <= 1'b0;
    else if ( (HCNT==rdstart) & DISPON )
        FIFORD <= 1'b1;
    else if ( HCNT==rdend )
        FIFORD <= 1'b0;
end

reg disp_enable;

always @( posedge PCK ) begin
    if ( PRST )
        disp_enable  <= 1'b0;
    else
        disp_enable  <= FIFORD;
end

FIFOリード

  • disp_enableがActiveの時にFIFOデータをReadしていく
  • VGA_R/G/Bはそれぞれ8bitの信号
  • 画素フォーマットに従ってFIFOOUTから読み出した32bitのデータを下位ビットから順番に入力する
  • 1画素分のデータがVGA_R/G/Bに格納された
always @( posedge PCK ) begin
    if ( PRST )
        {VGA_R, VGA_G, VGA_B} <= 24'h0;
    else if ( disp_enable )
        {VGA_R, VGA_G, VGA_B} <= FIFOOUT;
    else
        {VGA_R, VGA_G, VGA_B} <= 24'h0;
end

パッケージしてIP化

動作が分かったのでIPにしていく

  • Synthesis
  • Create and Package New IP -> Review and Package -> Package IP
  • Display部分のIPができた

グラフィック表示回路のデザイン作成

  • 別のプロジェクトを作成する
  • Tools -> IP -> Repository ->先ほどパッケージしたIPディレクトリを指定
  • HDMItoVGA IPも必要なので同様にimport
  • Block designを作成
  • Zynq PSの追加
    • GPIO接続用のHP 0をEnable
    • Clock Configuration -> PL Fabric Clocks ->FCLK_CLK0 100MHzに変更
    • Run Connection Automation
  • Display IPを追加
    • Run Connection Automation -> AXI Interconnect経由でM_AXIをZynqと接続
  • 画素表示アドレス制御用GPIOの追加
    • 30bitのデータと1bitの制御用Outputに分ける
    • GPIOを二つ用意し、両方ともDual Channelで使用
    • S_AXIポートを指定してRun Connection Automation

こんなかんじ

  • Display IPの出力にHDMItoVGA IPを接続
  • External Portを作成してPort name修正
  • Validate Design
  • Create HDL Wrapper

Block Designが完成

  • 制約ファイルのインポート
  • Generate Bitstream

実機検証

  • Export Hardware

  • Write TCL (Gitで管理用) ->Save

  • Launch Vitis IDE

  • Create Application Project

  • Export Hardwareで作成したxsaファイルをplatformとして指定

  • Empty Application -> Finish

  • Diplay_Circuit_System Applicationのsrcにdisp_test.cをインポート

  • Build Project

テストプログラム動作検証

  • 各変数の設定は省略
  • 言語はC

VBlank Wait

  • VBlankをクリアするためのCLRVBLANKをセット
  • CLRBLNKをクリア
  • これによりHardware側でVBlankがActiveになるまでwaitされる
void wait_vblank(void) {
    XGpio_DiscreteWrite(&GpioBlank, CLRVBLNK, 1);
    XGpio_DiscreteWrite(&GpioBlank, CLRVBLNK, 0);
    while (XGpio_DiscreteRead(&GpioBlank, VBLANK)==0);
}

矩形描画処理

  • 矩形を描画するためのファンクション
  • 最初のfor文で上辺と下辺部の線を書いている
  • 2番目のfor文で左右の辺を描画
void drawbox( int xpos, int ypos, int width, int height, int col ) {
    int x, y;

    for ( x=xpos; x<xpos+width; x++ ) {
        VRAM[ ypos*XSIZE + x ] = col;
        VRAM[ (ypos+height-1)*XSIZE + x ] = col;
    }
    for ( y=ypos; y<ypos+height; y++ ) {
        VRAM[ y*XSIZE + xpos ] = col;
        VRAM[ y*XSIZE + xpos + width -1 ] = col;
    }
}

後はmainファンクション
ざっくり内容

  • 表示アドレス、表示ON/OFFのイニシャライズ
  • VBlank信号のイニシャライズ
  • wait_vblankを実行
  • Dispaddr:0x1000000を指定
  • DiSPON信号をActive
  • VRAMの値に全て0を入力(Write)
  • L1/L2キャッシュをクリアしてDDRメモリに値を反映
  • drawbox関数の値をDRAMに入力する
  • 終了する時はwait_vblankを再度実行
  • DISPON信号をInavtiveにする

FPGAに転送

  • ZyboをPCに接続してUSBportをWSL側にマウントする(毎回毎回めんどくさい)
     毎回コレをPowershellから打ってますが、いい方法あったら教えてください
usbipd wsl list
usbipd wsl attach --busid <busid>
  • Programming FPGA
  • Debug As ->Break pointを設定してtestプログラムの挙動を確認

    - 意図通りの動作を確認
    例によってFPGA側からの出力をスクショするのが面倒なので画像は割愛します

おわりに

めちゃくちゃ長かった・・・・・・・・
今回の記事でビットブロック回路までの合成ができなかったので、次回合成します

Discussion