Zynq+FPGAでグラフィック表示回路設計
Zynq+FPGAでグラフィック表示回路をつくる
はじめに
本投稿では本書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で作成したビットブロック描画回路と組み合わせる
AXI bus制御方法
- ステートマシンで制御
- HALT: 1画面が始まるまでwait->Dispstart
- SETADDR: ARチャネルでアドレス発行
- READING: VRAM Read-> FIFO Write
- WAITING: FIFOが満杯の間wait
実装
例によってソースコードはここから
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