LoginSignup
6
6

More than 1 year has passed since last update.

FPGA(Pynq/ZYNQ)でプログラム(C/C++)を動作させるまでの手順

Last updated at Posted at 2022-04-06

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])

次に、このinputoutputを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)の設定が適切かを見る。
image.png
Solution > Solution Settingsを選択。

image.png
Solution Settings > Synthesisを選択。
次に、この画面上でpartの横にある...を選択。

image.png
このFPGAの一覧から作成したプログラムを動作させたいFPGAを選択。
選択後はOKを選択し、画面を閉じる。

次に最上位関数の設定
image.png
Project > Project Settings

image.png
Project Settings > Synthesis > Top function > Browseから最上位関数を選択
私の場合はtop_function.c

高位合成

次に高位合成をする

image.png
赤矢印の再生ボタンをクリック

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.

image.png

まずは赤矢印の表を見て、自身の実装したプログラムをFPGAに搭載すると、どれくらいのロジックとメモリを使うかを見てみよう。これが100%を超えていれば、もちろんそのFPGAには乗らないので、またプログラムを変更しつつ、コンパイルして調整しよう。

Verilogを書き出す

では次に、コンパイルした結果を外部から読み取れるようにする。
そのために、Export RTLを選択する。

image.png
ポチっと押すとExport RTL画面が表示される。

image.png
デフォルト設定のまま、OKを押す。

そうすると、Starting export RTL ...と表示され、Exportが始まる。
image.png

終わると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を選択
image.png

Next

image.png

テキトーなProject nameとProject locationを設定 > Next
image.png

HLSでRTLをエクスポートしたので、RTLプロジェクトを選択 > Next

image.png
何も設定せずNext

image.png
制約も何も設定せずNext

image.png
HLSと同じターゲットに設定 > Next

image.png
Finish

IPの追加

image.png
PROJECT MANAGER > IP Catalogを選択

image.png
IP Catalog上で右クリック > IP settings

image.png
IP > Repositoryを選択

+ボタンで、先ほどHLSから書き出されたIPフォルダを追加
<YOUR_VIVADO_HLS_PATH>\solution1\impl\ip
image.png
こんな感じのファイルがあるフォルダ。

image.png
追加したらOK > OKを選択して閉じる。

回路の作成

PROJECT MANAGER > IP INTEGRATOR > Create Block Designを選択

image.png
こんな画面が表示されるので、そのままOK
image.png
ADD IPを選択
image.png
Zynqと打ってエンター

image.png
Zynqが追加されたのをみつつ、Run Block Automation
image.png
デフォルトの設定でOK

次に、またADD IPから、自身の最上位関数を登録。
image.png

私の場合はTop_function
image.png
追加されたのを確認しつつ、Run Connection Automationを選択し、自動配線。

image.png
デフォルト設定でOK

image.png
自動配線された。

後は気になるブロックなどをダブルクリックで選択して、詳細を確認すればよい。
次に、このブロック線図が正しいかValidationを行う。

ValidationはF6キーで行える。

image.png
こんな画面が表示されれば検証終了

image.png
Sources > 右クリック > Create HDL Wrapperを選択

image.png
デフォルトでOK

image.png
これでラッパーが作成される。

Bitstream化する

ではVerilogから回路図が出来たので、いよいよBitstream化する。
image.png
Generate Bitstreamを選択

image.png
YES

image.png
OKでコンパイル開始。

なおNumber of jobsでスレッド数を変えられる。
CPU数 - 1くらいの数にしておこう。CPU全て使うと、たまにVivadoが落ちたりする。

image.png
終わるとこんな感じの画面が出るので、キャンセル

image.png
次にProject Summaryを押す

image.png
こんな具合に、SynthesisとImplementationがCompleteになっていれば成功

image.png
ではいよいよBitstreamを書き出す。
File > Export > Export Hardware

image.png
Include bitstreamにチェックを入れてOK

こんな感じのファイルパスのところにbitstreamが書き出される。
<YOUR_PATH>\testProject\testProject.runs\impl_1
image.png
こんな感じのファイル群
.bitという拡張子がBitstreamのファイルになる。

これで必要なファイル達は殆ど揃った。

3. BitstreamをFPGAに書き込む(Zynq上)

では、いよいよFPGAに書き込む。まずFPGAが乗っているボードにSCPないしは、何らかの方法で先ほど生成したbitstreamを送る。

image.png
こんな感じのファイル構造になっていればよい。

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\
image.png
ファイルは最上位関数の名前によって変わる。
では、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

image.png

これらのファイルをコピー。

image.png
image.png
最終的にこんな構造になっていればよい。
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

実行結果
image.png
入力画像に対して、何のクラスであるかの確率が出力された。

6
6
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6