この記事について
Vivado HLSを使用して、C言語だけで、3の倍数と3のつく数字の時だけアホになるIPを高位合成して作ります。
タイトルはネタですが、ハンズオンっぽく、Vivado HLSの使い方を習得できるような記事にしたつもりです。
全体の仕様
- CPU (Cアプリケーション)からナベアツIPのレジスタに数字を書く。
- ナベアツIPは、アホになるときにLEDを点灯する。また、アホかどうかをレジスタに書く。CPUはその値を確認できる。
ナベアツIPの仕様
- 名前: nabeatsu
- 入力ポート:
ap_uint<32> number
- 入力となる数字
- インターフェイス = AXI Lite スレーブ
- 出力ポート:
ap_uint<1> *ahoOut
- アホになるとき1を出力
- インターフェイス = ap_none (+ register)
- 出力ポート:
ap_uint<1> 戻り値
- アホになるとき1を出力
- インターフェイス = AXI Lite スレーブ
出力ポートが2つあるのは、CPUがAXI経由でレジスタからREADする用、と、LEDに直接出力する用です。
環境と事前知識
環境
- 開発用PC: Windows 10 64-bit
- Vivado 2017.4 WebPACKライセンス
- Vivado HLS 2017.4
- Xilinx SDK 2017.4
- ターゲットボード: ZYBO (Z7-20)
事前知識
VivadoとXilinx SDKの簡単な使い方は分かっているものとします。下記記事を参考にしてください。
- ZYBO (Zynq) 初心者ガイド
全体の流れ
まず、Vivado HLSでナベアツIPを作ります。Vivado HLSはあくまでIPを作るツールです。 そのため、Vivado HLSでIPを作った後に、Vivadoで全体のハードウェアを作ります。その中で先ほど作成したナベアツIPを使用します。Vivadoでハードウェア(hdf)を作り、エクスポートしたら、最後にXilinx SDKで制御プログラムを作成します。
事前準備
Vivado HLSでZYBOボードを参照できるようにします。C:\Xilinx\Vivado\2017.4\common\config\VivadoHls_boards.xml
に、以下2行を追加します。
<board name="Zybo_Z7_10" display_name="Zybo_Z7_10" family="zynq" part="xc7z010clg400-1" device="xc7z010" package="clg400" speedgrade="-1" vendor="digilentinc.com" />
<board name="Zybo_Z7_20" display_name="Zybo_Z7_20" family="zynq" part="xc7z020clg400-1" device="xc7z020" package="clg400" speedgrade="-1" vendor="digilentinc.com" />
Vivado HLSでナベアツIPを作る
プロジェクトを作る
Vivado HSLを起動して、Create New Projectします。プロジェクト名はnabeatsu
とします。ソースコードの追加などはひとまず何もせずにNextします。
最後に、ソリューションの設定をします。その時に、使用するデバイス設定が必要になりますので、今回はBoardsからZybo Z7 20 (先ほどの事前準備で追加した)を指定します。
C++ソースコードの実装と、Cシミュレーションの実行
作成するIPのソースコードを用意します。
メニューバー -> Project -> New Sourceと、New Test Benchで、ソースコードとテストベンチを作ります。今回はナベアツIPを作るので、それぞれ以下のようなファイルを作成します。
- Source: nabeatsu.cpp
- Test Bench: nabeatsu_tb.cpp
それぞれ、以下のように実装します。
アルゴリズムの説明は簡単なので省略しますが、以下がポイントになります。
- 型として、ビット幅が指定できる
ap_uint
を使用している - 出力ポートは、戻り値でもポインタ引数でもOK
- テストベンチでちゃんとテストコードを書く。(なくても害はないが)
#include <ap_int.h>
ap_uint<1> nabeatsu(ap_uint<32> number, ap_uint<1> *ahoOut)
{
if (number % 3 == 0) {
*ahoOut = 1;
return 1;
}
while (number > 0) {
if (number % 10 == 3) {
*ahoOut = 1;
return 1;
}
number /= 10;
}
*ahoOut = 0;
return 0;
}
#include <ap_int.h>
#include <stdio.h>
extern ap_uint<1> nabeatsu(ap_uint<32> number, ap_uint<1> *ahoOut);
int main()
{
printf("Hello \n");
ap_uint<1> ahoOut;
for (int i = 1; i < 100; i++) {
printf("%d: ", i);
if (nabeatsu(i, &ahoOut)) printf("aho");
printf("\n");
}
return 0;
}
実装が完了したら、メニューバー -> Project -> Run C Simulationします。これによってテストベンチ内のmain関数が実行されて、Cレベルでの動作確認ができます。
C 高位合成する
とりあえず合成してみる
まず、メニューバー -> Project -> Project Settingsで、Synthesisを選んで、Top Function
にnabeatsu
関数を設定しておきます。
その後、メニューバー -> Solution -> Run C Synthesis -> Active Solutionで、高位合成を行います。
完了したら、高位合成結果のレポートが表示されます。ここで性能(レイテンシ等)や回路規模を確認します。
インターフェースをAXIにする
レポート内の、Interfaceを見ていただきたいのですが、これを見ると、number_V
やahoOut_V
の他にもap_start
などがあります。これらは、このIPをコントロールするためのインターフェイスになります。このIPの接続先が、PL内の他のIPだけであればこれでも良いのですが、今回はCPUから制御する予定です。そのため、CPUから制御しやすいようにAXIインターフェースに変更します。
nabeatsu.cpp
を開いた状態で、右側のペインでDirective
タブを選びます。関数名や引数をダブルクリックすることで、どのようなインターフェースにするかを選択できます。なお、戻り値のインターフェースは関数名をダブルクリックして設定できます。インターフェイス仕様はこの記事の冒頭で記したとおりです。一点注意点は、ahoOutは直接LEDピン(M14)に接続します。そのため、余計な制御ピンは不要なので、ap_none
とします。しかし、それだけだとvalid以外のときに出力が不安定になってしまいます。そのため、オプションとしてregister
を指定します。これによって、出力状態が保たれます。
IP化する
Directive設定後、再度、Run C SynthesisしてRTLを出力します。その後、Export RTLをクリックして、IP化します。設定はデフォルトのままでOKです。
メモ: 最適化 / Directiveについて
今回はやりませんが、Directiveでは、上述したインターフェイスの他にも、ループをパイプライン化するとか展開するといった指定も可能です。回路規模や性能のトレードオフとなるところです。色々な設定を試して、レポートやRTLシミュレーション結果を見て、プロジェクトに最適な設定を探します。
このように、ソースコードは同じだけど、Directiveを色々変えて試したい、という状況が発生します。そのため、Vivado HLSではソリューションという概念を導入しています。ソースコードを共通としながら、ソリューション毎に別々のDirectiveを指定することが出来ます。これによって、後で比較したりするのが楽になります。
これも今回はやりませんが、RTLシミュレーションをやるには、Run C SynthesisでRTLを出力した後に、Run C/RTL Cosimulationします。シミュレーション設定ウィンドウが開くので、Dump Traceにportかallを設定して、OKします。完了後、Open Wave Viewerをクリックすることで、新規に開かれるVivadoウィンドウ上で信号波形を確認することができます。
Vivado (IP Integrator)で全体のハードウェア(hdf)を作る
プロジェクトを作る
Vivadoで、Zybo Z7-20ボードを指定して、新しいプロジェクトを作ります。IP Integratorで新規Block Designを作成して、PSを配置しておきます。(https://qiita.com/take-iwiw/items/24a8a94741fdbb80f62a )を参考にしてください。
ナベアツIPを組込む
Vivado HLSで作成したIPを取り込むために、IPレポジトリの追加を行う必要があります。
左側のFlow Navigator -> PROJECT MANAGER -> Settingsを開き、IP -> Repository から、先ほど作成IP化したナベアツIPの場所を指定します。IPのパスは、HSLプロジェクトのパス/高位合成したソリューション名/impl/ip
になります。
再度Block Designに戻ると、Add IPからNabeatsu
が指定できるようになっているはずです。これを追加して、Run Connection Automationで自動配線します。
配置されたnabeatsu_0
のahoOut_V[0:0]
で右クリック -> Make External
をクリックして、外部ポートを作成しておきます。これは、後でLED(M14)に接続します。
IP間の接続が完了したら、Generate Output Products, Create HDL Wrapperします。
アホ出力をLEDに接続する
左側のFlow Navigator -> RTL ANALYSIS -> Open Elaborated Designを開きます。
下部に表示されるI/O Portsタブで、ahoOut_V_0[0]
にM14 (LVCMOS33)を割り当てて、適当な名前でXDCファイルとして保存します。
ハードウェアを完成させる
Generate Bitstreamをクリックして、ビットストリームを作成します。その後、Export Hardwareでビットストリーム付きのhdfを作成します。
Xilinx SDKで制御プログラムを書く
VivadoのメニューバーからLaunch SDKでXilinx SDKを起動します。
standaloneプラットフォームのアプリケーションプロジェクトを作成します。
プログラム全体は以下のようになります。ナベアツIPへのアクセスは、自動生成されたbspプロジェクト内のxnabeatsu.h
を使用します。
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xnabeatsu.h"
int main()
{
init_platform();
print("Hello World\n\r");
XNabeatsu instNabe;
u32 aho;
XNabeatsu_Initialize(&instNabe, XPAR_NABEATSU_0_DEVICE_ID);
for (int i = 1; i < 100; i++) {
printf("%d: ", i);
XNabeatsu_Set_number_V(&instNabe, i);
XNabeatsu_Start(&instNabe);
while (XNabeatsu_IsDone(&instNabe) == 0);
aho = XNabeatsu_Get_return(&instNabe);
if (aho == 1) {
printf("aho\n");
} else {
printf("\n");
}
getchar();
}
cleanup_platform();
return 0;
}
実行する
Program FPGAでビットストリームファイルを書き込んだ後に、ビルドしたCアプリケーションをRunします。
すると、入力数字が3の倍数か3を含むときだけアホになっていることが分かると思います。また、アホになっているときはLEDが点灯します。
Hello World
1:
2:
3: aho
4:
5:
6: aho
7:
8:
9: aho
10:
11:
12: aho
13: aho
14:
15: aho
16:
17:
18: aho
19:
20:
21: aho
22:
23: aho
メモ: HLSで高位合成されたIPへのアクセス
自分でVerilogを書いた自作IPの場合は、レジスタの何番地が何用のパラメータ、といった情報を自分で分かっていると思います。HLSで高位合成されたIPでも、bsp内のxnabeatsu_hw.h
に、アドレスと用途がまとまっています。しかし、処理開始/終了のためのstart/doneフラグなどを自分で管理するのは面倒です。
そのため、今回使用したように、自動生成されたライブラリ(xnabeatsu.c/h)を使用するのが楽です。
便利なことに、LinuxでUIOとしてアクセスするためのライブラリ(xnabeatsu_linux.c)も自動生成されます。しかもインターフェイス(xnabeatsu.h)は、共通で使用することが出来ます。そのため、IPをHLSで作った後に、まずはXSDKのstandaloneプラットフォームで軽く動作確認をして、その後、Linux側で本格的にアプリケーションで使用する、といったワークフローでも移行が簡単にできます。
参考資料
https://www.slideshare.net/marsee101/vivado-hls1
https://forums.xilinx.com/t5/Vivado-High-Level-Synthesis-HLS/Zybo-Board-Files-for-HLS/td-p/748198