0. 概要
C/C++で作成したプログラムをFPGAで動作させるチュートリアル。
登場人物と彼らの役割は以下。
-
VivadoHLS
高位合成用のソフトウェアで、C/C++で作成したプログラムをFPGA用の言語であるVerilog/HDLに変換してくれる。 -
Vivado
Verilog/HDLをBitstreamというFPGA言語にコンパイルしてくれる。
一般的なCPUアーキテクチャでいうとアセンブラがVerilog/HDL(RTL: レジスタ転送レベル)に対応する。マシン語がBitsteramに対応。 -
Zynq
FPGAとCPUが1枚のボードに実装されている。このような形式をSoCといってARMのCPUからFPGAへアクセスできるのが利点である。これにより、CPU上でLinuxを動作させ、デバイスドライバを用いてFPGAへアクセスすることができる。
また、FPGAのメモリ空間をCPU(メモリ)にマッピングすることができるため、CPUの使用感でFPGAが使える。
C/C++で作成したプログラムをFPGAで動作させる全体の流れは以下。
- C/C++をVerilog/VHDL化(Vivado HLSを使用)
- Verilog/VHDLをBitstream化(Vivadoを使用)
- BitstremをFPGAに書き込む(Zynq上)
- FPGAをCPU(SoC)から使う(implementation in c on Zynq)
環境は以下。
- FPGA: Pynq for Zynq
- 母艦PC: Windows 7 64bit
1. C/C++→Verilog/VHDL化
まず、何らかのC/C++コードがあるものとする。今回はCNNとする。これを最終的にはFPGAで動作させる。
最上位関数及びインタフェースの設定
CNNのプログラムの仕様としては画像(input)を受け取り、その画像が何であるかを識別した結果(output)を返すものとする。
このため、FPGAへ搭載する最上位の関数は以下になる(私の場合)。
void top_function(T *_input, T *_output) {
// CNNの処理
}
因みに、上記のようにポインタでargsを渡すと大きさが未知なので、こんな感じのエラーが出る。
unsupported memory access on variable '_output' which is (or contains) an array with unknown size at compile time.
そのため、以下のように大きさを指定する。
この時、大きさを指定することによって、そのサイズ分を確保していそうであるものの、
Cと同様で配列はメモリコピーにならず、ポインタ渡しになる。
すなわち配列を超える大きさでも最初のオフセットからずらして参照すればアクセスできる(お勧めしない)。
void top_function(T _input[INPUT_NUM], T _output[OUTPUT_NUM])
次に、このinput
とoutput
をCPU及びFPGAで参照できるようにするため、
以下のようにインタフェースを設定する。
#pragma HLS INTERFACE s_axilite port=_input bundle=AXI4LS
#pragma HLS INTERFACE s_axilite port=_output bundle=AXI4LS
#pragma HLS INTERFACE s_axilite port=return bundle=AXI4LS
コーディングはこれでおしまい。次から高位合成を行う。
高位合成の準備
まずはターゲット(FPGA)の設定が適切かを見る。
Solution > Solution Settingsを選択。
Solution Settings > Synthesisを選択。
次に、この画面上でpartの横にある...を選択。
このFPGAの一覧から作成したプログラムを動作させたいFPGAを選択。
選択後はOKを選択し、画面を閉じる。
次に最上位関数の設定
Project > Project Settings
Project Settings > Synthesis > Top function > Browseから最上位関数を選択
私の場合はtop_function.c
高位合成
次に高位合成をする
Starting C synthesis ...
とConsoleに表示されて、コンパイルが始まる。
なお、Cシミュレータと異なり、高位合成時のコンパイラではmalloc、New、Vectorや各種STLが使えないこと、ポインタ渡しにも制約があるため、コンパイルに失敗することが多いだろう。
一先ず、コンパイル時にメモリの大きさが明確(だますことはできる)になっているように気を付けておけばよい。
コンパイルが成功すると以下のようなログとともに、Synthesis Reportが表示される。
INFO: [HLS 200-111] Finished generating all RTL models Time (s): cpu = 00:00:53 ; elapsed = 00:01:54 . Memory (MB): peak = 1176.785 ; gain = 1118.508
INFO: [SYSC 207-301] Generating SystemC RTL for top_function.
INFO: [VHDL 208-304] Generating VHDL RTL for top_function.
INFO: [VLOG 209-307] Generating Verilog RTL for top_function.
INFO: [HLS 200-112] Total elapsed time: 114.699 seconds; peak allocated memory: 1.034 GB.
Finished C synthesis.
まずは赤矢印の表を見て、自身の実装したプログラムをFPGAに搭載すると、どれくらいのロジックとメモリを使うかを見てみよう。これが100%を超えていれば、もちろんそのFPGAには乗らないので、またプログラムを変更しつつ、コンパイルして調整しよう。
Verilogを書き出す
では次に、コンパイルした結果を外部から読み取れるようにする。
そのために、Export RTL
を選択する。
そうすると、Starting export RTL ...
と表示され、Exportが始まる。
終わるとConsoleにFinished export RTL ...
と表示される。
また、左のExplorerにimpl > ipのフォルダが追加され、ここにコンパイル結果が格納される。
その中にxilinx_com_hls_xxxxxx_xxx.zipもある。
なお、CPUからFPGAを叩くために使うDriverもこの時生成されており、
impl > ip > drivers
の中に入っている。このフォルダの中のsrcフォルダを全てコピーしておけばよいだろう。
2. Verilog/VHDLをBitstream化(Vivadoを使用)
FPGAへ焼くために書き出されたVerilogをBitstream化する。
まずはVivadoを起動。
Projectの作成
Quick Start > Create Projectを選択
Next
テキトーなProject nameとProject locationを設定 > Next
HLSでRTLをエクスポートしたので、RTLプロジェクトを選択 > Next
IPの追加
PROJECT MANAGER > IP Catalogを選択
IP Catalog上で右クリック > IP settings
+ボタンで、先ほどHLSから書き出されたIPフォルダを追加
<YOUR_VIVADO_HLS_PATH>\solution1\impl\ip
こんな感じのファイルがあるフォルダ。
回路の作成
PROJECT MANAGER > IP INTEGRATOR > Create Block Designを選択
こんな画面が表示されるので、そのままOK
ADD IPを選択
Zynqと打ってエンター
Zynqが追加されたのをみつつ、Run Block Automation
デフォルトの設定でOK
私の場合はTop_function
追加されたのを確認しつつ、Run Connection Automationを選択し、自動配線。
後は気になるブロックなどをダブルクリックで選択して、詳細を確認すればよい。
次に、このブロック線図が正しいかValidationを行う。
ValidationはF6キーで行える。
Sources > 右クリック > Create HDL Wrapperを選択
Bitstream化する
ではVerilogから回路図が出来たので、いよいよBitstream化する。
Generate Bitstreamを選択
なおNumber of jobsでスレッド数を変えられる。
CPU数 - 1くらいの数にしておこう。CPU全て使うと、たまにVivadoが落ちたりする。
こんな具合に、SynthesisとImplementationがCompleteになっていれば成功
ではいよいよBitstreamを書き出す。
File > Export > Export Hardware
こんな感じのファイルパスのところにbitstreamが書き出される。
<YOUR_PATH>\testProject\testProject.runs\impl_1
こんな感じのファイル群
.bit
という拡張子がBitstreamのファイルになる。
これで必要なファイル達は殆ど揃った。
3. BitstreamをFPGAに書き込む(Zynq上)
では、いよいよFPGAに書き込む。まずFPGAが乗っているボードにSCPないしは、何らかの方法で先ほど生成したbitstreamを送る。
main.pyは以下のように書く。
#coding:utf-8
import os.path
BITSTREAM_FILE = "./design_1_wrapper.bit"
PARTIAL_BITSTREAM = "/sys/devices/soc0/amba/f8007000.devcfg/is_partial_bitstream"
FPGA_DEVICE = "/dev/xdevcfg"
if __name__ == '__main__':
# Compose bitfile name, open bitfile
with open(BITSTREAM_FILE, 'rb') as f:
buf = f.read()
# Set is_partial_bitfile device attribute to 0
with open(PARTIAL_BITSTREAM, 'w') as fd:
fd.write('0')
# Write bitfile to xdevcfg device
with open(FPGA_DEVICE, 'wb') as f:
f.write(buf)
以下で実行
sudo python3.x main.py
そうするとBitstreamがCPUからFPGAへに書き込まれる。
/dev/xdevcfg
このxdevcfgがFPGAとマッピングされており、
こちらにBitstreamを書き込めば、オーバレイできる仕組みとなっている。
そのため、こんなのでもよい。
$sudo bitstream > /dev/xdevcfg
4. FPGAをCPU(SoC)から使う(implementation in c on Zynq)
まず、CPUからFPGAを叩くために、デバイスドライバなどをIncludeしたC言語で実装しなければならない。
そのため、必要なファイルを揃える。
ビルドに必要なファイルを揃える
まずVIVADO HLSで生成したIPフォルダのDriverを開く。パスは以下。
<YOUR_PROJECT_PATH>\solution1\impl\ip\drivers\<YOUR_FUNCTION>\src\
ファイルは最上位関数の名前によって変わる。
では、Makefile以外をコピー。
次に私でいうところの、xtop_function.hを見てみると、以下のファイルがヘッダとして読み込まれていることが分かる。
#include "xil_types.h"
#include "xil_assert.h"
#include "xstatus.h"
#include "xil_io.h"
これらのファイルがありそうなところから、ごっそりインクルードファイルとして持ってくる。
C:\Xilinx\SDK\2017.3\data\embeddedsw\lib\bsp\standalone_v6_4\src\common
これらのファイルをコピー。
最終的にこんな構造になっていればよい。
testBench.cについてはまだないと思われるが、次項で説明する。
ビルド、そして実行
testBench.cは実際にFPGAをCPUから叩く際のアプリケーションとなる。
以下のような感じで書けばよい。
#include <stdio.h>
#include "xtop_function.h"
int main(){
XTop_function XMluti_ap;
XTop_function_Initialize(&XMluti_ap, "fabric") ;
float input[] = {...};
float output[] = {...} ;
XTop_function_Write_p_input_Bytes(&XMluti_ap, 0, (char *)input, (28 * 28 * 4));
while(!XTop_function_IsIdle(&XMluti_ap)) ;
XTop_function_Start(&XMluti_ap);
while(!XTop_function_IsDone(&XMluti_ap)) ;
for(int i=0; i<10; ++i){
XTop_function_Read_p_output_Bytes(&XMluti_ap, i * 4, (char *)&output[i], 4) ;
printf("%d : %f\n", i, output[i]) ;
}
return(0);
}
各関数は微妙に関数名が異なっていることもあるので、xtop_function.hを参考にすること。
因みに、これ
XTop_function_Initialize(&XMluti_ap, "fabric") ;
このfabric
全てのFPGAボードで共通というわけではない。ものすごい落とし穴である。
これはCPUからFPGAにアクセスする時のアドレスの識別子なのである。
これがデバドラXTop_function_Initialize
の中身である。
int XTop_function_Initialize(XTop_function *InstancePtr, const char* InstanceName) {
XTop_function_uio_info *InfoPtr = &uio_info;
struct dirent **namelist;
int i, n;
char* s;
char file[ MAX_UIO_PATH_SIZE ];
char name[ MAX_UIO_NAME_SIZE ];
int flag = 0;
assert(InstancePtr != NULL);
n = scandir("/sys/class/uio", &namelist, 0, alphasort);
if (n < 0) return XST_DEVICE_NOT_FOUND;
for (i = 0; i < n; i++) {
strcpy(file, "/sys/class/uio/");
strcat(file, namelist[i]->d_name);
strcat(file, "/name");
if ((line_from_file(file, name) == 0) && (strcmp(name, InstanceName) == 0)) {
flag = 1;
s = namelist[i]->d_name;
s += 3; // "uio"
InfoPtr->uio_num = atoi(s);
break;
}
}
if (flag == 0) return XST_DEVICE_NOT_FOUND;
uio_info_read_name(InfoPtr);
uio_info_read_version(InfoPtr);
for (n = 0; n < MAX_UIO_MAPS; ++n) {
uio_info_read_map_addr(InfoPtr, n);
uio_info_read_map_size(InfoPtr, n);
}
sprintf(file, "/dev/uio%d", InfoPtr->uio_num);
if ((InfoPtr->uio_fd = open(file, O_RDWR)) < 0) {
return XST_OPEN_DEVICE_FAILED;
}
// NOTE: slave interface 'Axi4ls' should be mapped to uioX/map0
InstancePtr->Axi4ls_BaseAddress = (u32)mmap(NULL, InfoPtr->maps[0].size, PROT_READ|PROT_WRITE, MAP_SHARED, InfoPtr->uio_fd, 0 * getpagesize());
assert(InstancePtr->Axi4ls_BaseAddress);
InstancePtr->IsReady = XIL_COMPONENT_IS_READY;
return XST_SUCCESS;
}
ようは、ここのパスにある者以外はじかれるということですね。
strcpy(file, "/sys/class/uio/");
strcat(file, namelist[i]->d_name);
strcat(file, "/name");
if ((line_from_file(file, name) == 0) && (strcmp(name, InstanceName) == 0)) {
なら以下から名前を持って来ればよし!ということです。
$ cd /sys/class/uio/uio0
$ cat name
因みに他のやり方としては、Socの中身を見る方法もある。
$ cd /sys/devices/soc0/amba@0
$ ls -a
これで表示された40000_xxxx
みたいな名前から始まるやつのxxxx
がだいたい識別子である。(zynqの場合)
. f8000000.ps7-slcr f8f00200.ps7-globaltimer
.. f8001000.ps7-ttc f8f00600.ps7-scutimer
43c40000.lap_filter_axim_hls f8003000.ps7-dma f8f00620.ps7-scuwdt
driver_override f8006000.ps7-ddrc f8f01000.ps7-scugic
e0001000.serial f8007000.ps7-dev-cfg f8f02000.ps7-pl310
e0002000.ps7-usb f8007100.ps7-xadc fc000000.ps7-qspi-linear
e000a000.ps7-gpio f8008000.ps7-afi modalias
e000b000.ps7-ethernet f8009000.ps7-afi power
e000d000.ps7-qspi f800a000.ps7-afi subsystem
e0100000.ps7-sdio f800b000.ps7-afi uevent
e0200000.ps7-iop-bus-config f800c000.ps7-ocmc
これでインタフェースの識別子/名前が分かったので、あとはインスタンスを作るだけ。その後は各関数を使って目的の機能を利用する。
関数の簡単な説明は以下。
XTop_function_Write_p_input_Bytes
バイト単位でFPGAの設定したインタフェースに値を送る。
この場合だとinputというインタフェースに値を送ることになっている。
XTop_function_IsIdle
アイドル状態かを確認
XTop_function_Start
FPGAを動作させる
!XTop_function_IsDone
処理が終了したか
XTop_function_Read_p_output_Bytes
バイト単位でFPGAの設定したインタフェースから値を読み込む
では既にBitstreamはFPGAに書き込まれているので以下でコンパイル。
g++ -Wall -I ./include/ -g *.c
以下で実行
sudo ./a.out