AXI でプロセッサとつながる IP コアを作る (3)

この記事は、AXI を使ってプロセッサと連携する回路の設計例について説明するコースの第3回です。前回は、AXI-Lite で制御や少量のデータのやりとりを行うインタフェース回路について説明しました。今回は、AXI-Stream でテストパターンの動画像を生成する IP コアを作成し、これらを既存の IP コアと組み合わせて、HDMI 接続のディスプレイにテストパターンを表示させるシステムを構築します。

今回示すシステムは HDMI を扱うため、残念ながらその動作を ACRi ルームの検証環境で試すことはできません。ただ、このレベルの設計ができるようになれば、そろそろ個人で Zynq のボードを購入して遊ぶことを検討してみてもいいかもしれません。

なお、第2回~第3回で使用する回路の SystemVerilog 記述は、GitHub からダウンロードできるようにしています。今回取り上げる回路記述に対応するファイルは、主にこのうちの sender/hdl/AXIS_send.sv です。

テストパターン生成 IP コアの設計

ビデオアプリケーションにおける AXI-Stream

いきなりですが、今回ははじめに構築するシステム全体のブロック図を示したいと思います。下図がそのブロック図です。

今回構築するシステムのブロック図。

図の右上の青枠で示したのが Zynq の PS (プロセッサ) 部です。Zynq ではメモリコントローラも PS に含まれるので、この部分がプロセッサ + DRAM に相当します。また、中央左の赤枠で示したのが、今回作成するテストパターン生成 IP コアです。

また、図中では IP コア同士の接続のうち、AXI を使用しているものを矢印で示しています。青矢印が AXI-Lite を用いた制御のための接続、緑矢印が DRAM 上のフレームバッファへの読み書きのための (フル機能の) AXI の接続です。そして赤矢印が、今回の主題である AXI-Stream の接続です。

ビデオアプリケーション向けの IP コアでは、AXI-Stream の使い方についてガイドラインが定められています。詳細は Xilinx 社ドキュメントの文書番号 UG1037 の第4章に記載されていますが、ここではその概要をまとめます。

動画像の転送では、フレームのサイズを幅 W x 高さ H 画素としたとき、各画素 (x, y) の情報を (0, 0), (1, 0), …, (W-1, 0), (0, 1), (1, 1), …, (W-1, H-1) の順番で転送します。フレームの最後の画素を転送したら、また (0, 0) 画素から次のフレームの転送を行います。また、各フレームの最初の画素 (0, 0) は SOF (Start of Frame)、各行の最後の画素 (W-1, *) は EOL (End of Line) という信号で、それぞれ示します。

以上をまとめたのが下図です。赤,緑で示した画素がそれぞれ SOF, EOL を表します。ビデオアプリケーションにおける AXI-Stream では、SOF, EOL の信号として、それぞれ TUSER, TLAST を使用します。

AXI-Stream での画素データの転送順。

各画素の情報は TDATA に乗せます。よく使われるのは赤・緑・青に各8ビットを割り当てた24ビットの RGB 形式ですが、この場合 TDATA は24ビット幅となり、上位ビットから順に赤、青、緑 の順でデータを配置します。RGB ではなく RBG であることに注意が必要です。

以上より、ビデオアプリケーションにおける AXI-Stream で必要な信号とその概要を下表にまとめます。方向の「→」は送信側から受信側へ、「←」は受信側から送信側へ送られる信号を表します。

信号名方向概要
TDATA画素データ。RGB 形式では上位ビットから RBG の順に並べる。
TUSER(0, 0) 画素を送信する時に ‘1’ にする。
TLAST(W-1, *) 画素を送信する時に ‘1’ にする。
TVALID画素データの準備ができたら ‘1’ にする。
TREADY受信側の準備ができているときに ‘1’ になる。

テストパターン生成 IP コアの全体構造

下図に、今回作成するテストパターン生成 IP コアの全体構造を示します。このうち AXI_ctrl は、前回説明した AXI-Lite でプロセッサからの制御を受ける回路です。

テストパターン生成 IP コアの概要。

今回新たに追加するパターン生成回路 AXIS_send は、おおよそ 1/60 秒に1フレームのペースで、特定のカラーパターンを生成し、AXI-Stream で出力する回路とします。フレームのサイズは、入力 GO が最初に ‘1’ になったときのスイッチ入力 SW が ‘0’ であれば 800 x 480、’1′ であればその1.5倍の 1200 x 720 とします。また、回路が動作中であるかどうかは、出力 RUN で示すこととします。

テストパターン生成回路の SystemVerilog 記述

ここからは、テストパターン生成回路の主要部分の SystemVerilog 記述を示しつつ、その動作を解説します。まず、回路の入出力 (外見) は4行目以降で定義されています。

module AXIS_send (
    input                AXIS_VID_ACLK,
    input                AXIS_VID_ARESETN,
    output logic [23: 0] AXIS_VID_TDATA,
    output logic         AXIS_VID_TLAST,
    output logic [ 0: 0] AXIS_VID_TUSER,
    output logic         AXIS_VID_TVALID,
    input  logic         AXIS_VID_TREADY,
    input  logic         SW,
    input  logic         GO,
    output logic         RUN);

先に示した回路図と、ビデオアプリケーションにおける AXI-Stream で必要な信号の定義に沿った記述になっています。TDATA, TUSER は仕様上は任意のビット幅を取れるものですが、ここでビット幅がそれぞれ24ビット、1ビットであることを宣言しています。

AXI-Stream の信号出力は、FIFO に一旦溜め込んで、受信側の準備が出来たタイミングで出力しています。画素データだけではなく SOF (TUSER), EOL (TLAST) も一緒に格納しますので、FIFO のデータ幅は26ビットです。これらに関連する信号は fifo_ というプレフィクスがついており、その定義は58行目以降の assign 文でなされています。

assign fifo_in[25] = (disp_x == 11'd0 && disp_y == 10'd0);
assign fifo_in[24] = (disp_x == 11'd799);
assign fifo_in[23:16] = (col_x[8]) ? col_y : 8'h00;
assign fifo_in[15: 8] = (col_x[6]) ? col_y : 8'h00;
assign fifo_in[ 7: 0] = (col_x[7]) ? col_y : 8'h00;
assign fifo_we = (disp_x <= 11'd799 && disp_y <= 10'd479 && wait_cnt == 2'd0);
assign fifo_re = scale_tvalid & scale_tready;
assign fifo_rst = GO & ~ RUN;

fifo_in の25ビット目は SOF、24ビット目は EOL を示し、23~16ビット目が赤、15~8ビット目が青、7~0ビット目が緑の画素値をそれぞれ表します。SOF の画素を基準として、現在出力しようといている画素の x, y 座標は disp_x, disp_y に格納されているので、これらを使って SOF, EOL の値や FIFO への書き込みが必要か (fifo_we) を定めています。

また、画素値は col_x, col_y で計算していますが、これらの計算には disp_x, disp_y のほか、今までに送信したフレーム数を表す frame_cnt も使われています。そのため、カラーパターンは一定方向に動きながら表示されることになります。

disp_x, disp_y のインクリメントは、71行目以降の always_ff 文で行われています。その核となる部分は89~98行目です。

end else if (wait_cnt != 2'd0) begin
    wait_cnt  <= wait_cnt - 1'b1;
end else if (~ fifo_full) begin
    disp_x    <= (disp_x == 11'd1055) ? 11'd0 : disp_x + 1'd1;
    disp_y    <= (disp_x != 11'd1055) ? disp_y :
                 (disp_y == 10'd524)  ? 10'd0 : disp_y + 1'd1;
    frame_cnt <= (disp_x == 11'd1055 && disp_y == 10'd524) ?
                 frame_cnt + 1'd1 : frame_cnt;
    wait_cnt  <= 2'd2;
end

ここでは、disp_x は 0~1055 の範囲で、disp_y は 0~524 の範囲でインクリメントしています。またこのインクリメントは、サイクルごとに 2~0 の範囲でデクリメントされる wait_cnt が 0 のときに行われます。以上より、1フレームの送信 (disp_y の1巡) にかかるサイクル数は、3 x 1056 x 525 = 1,663,200 となります。クロック入力 AXIS_VID_ACLK の周波数が 100 MHz であれば、時間に直すと 16.632 ミリ秒と、おおむね 1/60 秒と一致します。

スケーリング回路の概要

テストパターン生成 IP では、スイッチ入力 SW によってフレームのサイズ (解像度) を切り替えます。大きいサイズ向けに別途パターンを用意しても良いのですが、今回は上述のテストパターン生成回路で作成した 800 x 480 の動画像を1.5倍に拡大して 1200 x 720 の動画像を生成することとしました。ここで使われているのがスケーリング回路 (ソースコードは sender/hdl/scaler.sv) です。

この回路は、AXI-Stream のビデオ信号を入力として、拡大された AXI-Stream のビデオ信号を出力する回路となっています。このような回路を間に挟むことでフィルタ処理なども実現できるのが、AXI-Stream を使う強みの1つです。スケーリング回路の詳細は本記事では省きますが、ここではその方針を簡単に示します。

補完の方針を下図に示します。今回は1.5倍の拡大なので、拡大前の動画像の x, y 座標がともに偶数の画素 (図中の青で示す画素) はそのまま使用できますが、それ以外 (赤で示す画素) は拡大前の複数の画素からの補完が必要です。各画素が入力される順番を考慮して、適宜データをレジスタや FIFO に保存しながら補完を行っていきます。補完方法は、画質よりもハードウェアの簡単さを重視して、5:3 ないし 3:5 の線形補間としました。隣接する画素値に対して 5:3 または 3:5 の重み付き平均を取ることで、中間の画素値を計算します。

スケーリング回路における画素の補完の方針。

今回はこの回路を SystemVerilog で記述してしまいましたが、この手の少し複雑な回路は Vivado HLS を使った C/C++ 言語からの高位合成を用いた方が、楽に記述できるかもしれません。Vivado HLS の入門にあたる「高位合成で加速するアクセラレータ開発」のコースが、本コースと並行して連載されています。ぜひ併せてご参照ください。

テストパターン生成回路のシミュレーション

テストパターン生成回路について、念のためシミュレーションで動作を確認しておきましょう。スイッチ入力 SW に ‘0’ を与えた回路と ‘1’ を与えた回路とを両方用意したテストベンチ (ソースコードは sender/testbench/sender_test.sv) を作成し、Vivado でシミュレーションします。シミュレーションは1フレーム弱にあたる16ミリ秒の間行います。

シミュレーション結果の全体図を下図に示します……って、細かすぎてわかりませんね。拡大します。

テストパターン生成回路のシミュレーション全体。

シミュレーション結果の一部を拡大したものが下図です。末尾に2とついていない方が SW を ‘0’ にした場合、ついている方が SW を ‘1’ にした場合の AXI-Stream の出力を示します。また、これまでに送信された画素数を count, count2 でカウントしています。

テストパターン生成回路のシミュレーションの一部。

TVALID 出力に注目します。SW を ‘0’ にした場合には、3サイクルに1回ずつ規則的に画素データが送信されています。一方、SW を ‘1’ にした場合は画素データが届いてから補完処理を行うため、それよりはやや不規則なタイミングで送信がなされています。

テストパターン生成回路のシミュレーションの終了時点の様子。

上図に示すシミュレーションの終了時点では、1フレーム分のパターン生成が完了しています。このときに送信された画素数は SW が ‘0’ の場合で 384,000、’1′ の場合で 864,000 となっており、1フレームの画素数 (800 x 480 = 384,000、1,200 x 720 = 864,000) と一致しています。

テストパターン表示システムの構築

各種 IP コアの設定

それでは、作成した回路記述一式と IP パッケージャを使ってテストパターン生成 IP コアを作成し、これをもとにテストパターン表示システムのブロック図を IP インテグレータで作成していきましょう。IP パッケージャの使い方は、「IP の世界からこんにちは」の第2回、IP インテグレータの基本的な使い方は、同コースの第4回を参照してください。

なお、今回のシステムは Digilent 社の Arty Z7-20 向けの HDMI 出力デモシステムの設計を参考に、不要な機能を省くことで構築しました。何かやりたいことがあり、既存の設計を参考にできる場合には、まずはそれを解析・改造するところから始めるのが王道です。

PYNQ-Z1 または Arty Z7-20 を対象にプロジェクトを作成し、IP Repository の設定を行います。今回は Vivado にあらかじめ用意された Xilinx 社の IP コアのほか、Digilent 社のライブラリのうち ip/rgb2dvi と if/tmds_v1_0 を使用します。これらをテストパターン生成 IP コアのフォルダが置かれたディレクトリにフォルダごとコピーし、このファイルのあるディレクトリを Vivado のプロジェクト設定で IP Repository に指定します。下図に示す2つの IP コアと1つのインタフェース定義が IP カタログから見えていることを確認します。

IP リポジトリが正しく設定されているときの IP カタログ。

Create Block Design でブロック図を新規作成します。その後、まずは Zynq の PS 部 (ZYNQ7 Processing System) を追加して、Run Block Automation を行います (この辺りは MicroBlaze のときと同じです)。

次に、PS 部の設定を変更します。PS をダブルクリックすると設定画面が表示されます。そのうち下図に示す PS-PL Configuration のページで、S AXI HP0 interface にチェックをし、PS-PL 間の HP ポートを1つ有効化します。

PS-PL 間のインタフェースに HP0 ポートを追加する。

つづいて、必要な IP コアを追加し、必要な設定をしていきます。今回必要な IP コアは以下のとおりです。名前の長い IP コアは、以下ではカッコに示す通り省略して表記します。

  • sender_top_v1_0: 作成したテストパターン生成 IP コアです。
  • Clocking Wizard: ビデオ出力のクロック信号の生成に使います。
  • AXI Video Direct Memory Access (AXI VDMA): DRAM 上のフレームバッファへのデータの送受信を担当します。
  • Video Timing Controller (VTC): ビデオ出力の同期信号の生成に使います。
  • AXI4-Stream to Video Out (Video Out): ディジタル RGB 形式のビデオ出力を生成します。
  • RGB to DVI Video Encoder (rgb2dvi): ビデオ出力を HDMI (DVI) に変換します。上述した Digilent 社の IP コアです。

Clocking Wizard の設定項目を下図に示します。今回は、720p の HDMI 出力を行いますが、そのピクセルクロック周波数は 74.25 MHz と定められています。また、その5倍の 371.25 MHz のクロックも rgb2dvi で必要です。これらを 100 MHz のクロック入力から生成するように設定します。

Clocking Wizard の設定。

AXI VDMA の設定項目を下図に示します。フレームバッファに画素データを書き込むときのビット幅は自動設定されますが、画素データを読み出すときのビット幅は手動設定が必要です。今回は1画素あたり24ビットなので、Read Channel の Stream Data Width を24に設定します。

AXI VDMA の設定。

VTC の設定項目を下図に示します。今回はプロセッサ側からの制御を行わず (AXI-Lite のインタフェースをもたず)、ビデオ入力がないので同期信号の検出が不要です。そのため、「Include AXI4-Lite Interface」と「Enable Detection」の2つのチェックを外します。

VTC の設定。

Video Out の設定項目を下図に示します。今回はピクセルクロックは PL (FPGA) 側のメインのクロックとは別になっています。そのため、Clock Mode を Independent に設定します。

Video Out の設定。

rgb2dvi の設定項目を下図に示します。今回は、リセット信号の極性を負論理に設定し、また内部で必要な5倍周波数 (371.25 MHz) のクロックは Clocking Wizard に生成させることとします。そのため、「Reset active high」と「Generate SerialClk internally from pixel clock」のチェックを外します。

rgb2dvi の設定。

IP コア同士の接続

以上の設定ができたら、次に IP コア同士を接続します。フル機能の AXI や AXI-Lite の信号は、アドレスマップを自動設定してもらうため、Run Connection Automation で設定します。AXI VDMA に読み出しと書き込みで AXI のインタフェースが2系統存在するためか、2回に分けて設定することになります。それぞれの設定で自動設定させる項目を下図に示します。

AXI 関連の自動接続の様子。

残りの配線は間違いが起きないように手動で行います。まずは、ビデオ信号とその周辺の配線を下表の通り行います。外部入出力へ接続するには、端子を make external して、外部ポートの端子名を変更します。

接続元端子名接続先端子名
sender_top_0SW(外部入力)SW
sender_top_0AXIS_VIDAXI VDMAS_AXIS_S2MM
AXI VDMAM_AXIS_MM2SVideo Outvideo_in
VTCvtiming_outVideo Outvtiming_in
Video Outvid_io_outrgb2dviRGB
rgb2dviTMDS(外部出力)TMDS
Video Outvtg_ceVTCgen_clken

最後の接続は忘れやすいので注意が必要です。これを忘れると同期信号がうまく生成されず、映像が表示されません。

次に、Zynq の PS 部からのクロック・リセット信号を下表の通り配線します。ただし Zynq PS は PS 部 (ZYNQ7 Processing System)、PS Reset は先の Connection Automation で生成された Processor System Reset を表します。

接続元端子名接続先端子名
Zynq PSFCLK_CLK0Clocking Wizardclk_in1
Zynq PSFCLK_CLK0AXI VDMAm_axi_mm2s_aclk
Zynq PSFCLK_CLK0AXI VDMAm_axi_s2mm_aclk
Zynq PSFCLK_CLK0Video Outaclk
PS Resetperipheral_resetClocking Wizardreset
PS Resetperipheral_aresetnVideo Outaresetn

最後に、Clocking Wizard の出力クロックをビデオ出力に関する IP コアへと供給します。下表に示す通りに接続します。接続元はすべて Clocking Wizard です。

接続元端子名接続先端子名
clk_out1VTCclk
clk_out1Video Outvid_io_out_clk
clk_out1rgb2dviPixelClk
clk_out2rgb2dviSerialClk
lockedVTCresetn
lockedVideo Outvid_io_out_ce
lockedrgb2dviaRst_n

以上の配線を行った後のブロック図の例を下図に示します。

最終的なシステムのブロック図。

論理合成と Vitis による動作確認

ブロック図が完成したら、論理合成までの手順はこれまでと変わりません。ブロック図を Validate し、各種のファイルを Generate し、制約ファイル (XDC) をプロジェクトに追加し、HDL Wrapper を作成し、Generate Bitstream で論理合成以降の一連の処理を行います。

制約ファイルは PYNQ-Z1, Arty Z7-20 ともに以下のファイル (Arty_Pynq.xdc) を使用します。ブロック図上で Make external した入出力に対するピン番号と仕様を定義しています。

set_property -dict { PACKAGE_PIN M20   IOSTANDARD LVCMOS33 } [get_ports { SW }];

set_property -dict { PACKAGE_PIN L17   IOSTANDARD TMDS_33  } [get_ports { TMDS_clk_n }];
set_property -dict { PACKAGE_PIN L16   IOSTANDARD TMDS_33  } [get_ports { TMDS_clk_p }];
set_property -dict { PACKAGE_PIN K18   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_n[0] }];
set_property -dict { PACKAGE_PIN K17   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_p[0] }];
set_property -dict { PACKAGE_PIN J19   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_n[1] }];
set_property -dict { PACKAGE_PIN K19   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_p[1] }];
set_property -dict { PACKAGE_PIN H18   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_n[2] }];
set_property -dict { PACKAGE_PIN J18   IOSTANDARD TMDS_33  } [get_ports { TMDS_data_p[2] }];

その後、Export Hardware でハードウェア部分をエクスポートしてから、Vitis を起動し、ソフトウェア部分の記述を行います。具体的には、フレームバッファと AXI VDMA を初期化してから、テストパターン生成 IP コアを起動するソフトウェアを記述します。Vitis の基本的な使用方法は、「IP の世界からこんにちは」の第5回を参照してください。

AXI VDMA の初期化以降のソフトウェアのソースコードを以下に示します (全文は init.c にあります)。

// VDMA を初期化
x = (VWIDTH - pwidth) / 2;
y = (VHEIGHT - pheight) / 2;
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x30, 0x8B);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xAC, (unsigned int) &fbuf[0][y][x * 3]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xB0, (unsigned int) &fbuf[1][y][x * 3]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xB4, (unsigned int) &fbuf[2][y][x * 3]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xA8, VWIDTH * 3);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xA4, pwidth * 3);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0xA0, pheight);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x00, 0x8B);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x5C, (unsigned int) &fbuf[0][0][0]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x60, (unsigned int) &fbuf[1][0][0]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x64, (unsigned int) &fbuf[2][0][0]);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x58, VWIDTH * 3);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x54, VWIDTH * 3);
Xil_Out32(XPAR_AXI_VDMA_0_BASEADDR + 0x50, VHEIGHT);

// パターン送信 IP コアを初期化
Xil_Out32(XPAR_SENDER_TOP_0_BASEADDR + 0x00, 1);

今回は、3枚分のフレームバッファを用意して、読み書きするバッファを自動的に切り替える (トリプルバッファ) モードで AXI VDMA を設定します。といっても、Xilinx 社の公式チュートリアルで説明されている手順28~32の通りに設定するだけですが。

チュートリアルと少し異なる点は、書き込み (テストパターン生成 IP コアが生成) と読み出し (HDMI 出力) とでフレームのサイズが異なることです。DMA 転送の開始位置などを適切に設定 (41~46行目) することで、フレームバッファの中央にだけ書き込みを行い、残りの部分は初期化のときに設定したグレーと黒のチェック模様が表示されるようにしています。

あとはプロジェクトをビルドし、FPGA にビットストリームを書き込み、プログラムを転送します。適当な HDMI 接続のディスプレイとボードを接続し、テストパターンが正しく表示され、右下へとスクロールしていれば成功です!

テストパターン表示システムの動作例。

まとめ

今回は想像以上に長い記事になりました。テストパターンを生成する IP コアの作成から、それをシステムに組み込んでの動作確認まで、「IP の世界からこんにちは」で説明してきたことを総動員する内容でもありました。今回の要点は以下のとおりです。

  • AXI-Stream では、用途ごとに信号の使い方のガイドラインが定められている。例えばビデオアプリケーションでは、TUSER をフレームの先頭に、TLAST を行の末尾を示すために用いる。
  • フィルタ処理などを実現する際は、AXI-Stream で入出力する回路を設計し、回路の間に挟むとよい。
  • IP コアを用いた設計では、やりたいことに対して参考にできる既存の設計があるならば、その解析・改造から始めるとよい。

次回からはフル機能の AXI について説明します。第4回では AXI をより扱いやすいインタフェースに変換する回路について解説し、第5回でそれを使った専用計算回路の設計例について取り上げたいと思います。

愛知⼯業⼤学 藤枝直輝

タイトルとURLをコピーしました