高位合成を使って FPGA 上に回路実装をしていくお話の第四弾になります.
これがついに最終回の予定です!
他の回は以下にあるのでご覧ください.
1. 今回行うこと
全体の流れは以下のようになっています.
- C++ を使って Vitis HLS 用に実行したいコードを書く
- Vitis HLS を使って,シミュレーションをしてデバッグする
- Vitis HLS を使って,Verilog にコンパイルする
- コンパイルしてできた IP を使って,Vivado で回路を組む (これが難しい…)
- Vivado を使ってできた回路から bitstream を作成する (いわゆるジェネビ)
- 生成された bitstream を PYNQ にコピーする
- PYNQ で回路にデータを流して結果を得る
今回は 6. 生成された bitstream を PYNQ にコピーする
,7. PYNQ で回路にデータを流して結果を得る
の手順を解説していきたいと思います.
ソースコードは以下に公開しているので適宜参照してください.
2. PYNQ の準備
まずは,PYNQ を準備しないといけないということで,SD カードに PYNQ を書き込んで,FPGA 上で起動し,PC からアクセスできるか確かめていきましょう.
おさらいですが,PYNQ というのは FPGA 上で動く Linux の一種で,回路にデータを送り込んだり,時間を計測したりするのに使用します.
具体的な方法は公式の ZCU104 Setup Guide や PYNQ を使って Python で手軽に FPGA を活用 (2) なども参考にしてみてください.
2.1. PYNQ イメージをダウンロード
今回使用する PYNQ のバージョンは PYNQ v2.6 (ZCU 104 用)
で,開発ボード向けのダウンロードページ からダウンロード可能です.
別の FPGA を使っている場合はそれに合わせてダウンロードしてください.
今回は zcu104_v2.6.0.zip
というファイルがダウンロードされると思うので,解凍して zcu104_v2.6.0.img
を手に入れましょう.
ファイルサイズは 7.2 GB 程度のようなので,16 GB 程度のマイクロ SD カードを用意しておくといいでしょう.
2.2. PYNQ イメージをマイクロ SD カードに書き込む
今使っているコンピュータの OS が Mac や Linux の人は dd コマンドなどを使って,Windows の人は例えば Rufus などを使って,zcu104_v2.6.0.img
をマイクロ SD カードに書き込んでください.
OS やデータが入っているディスクを上書きしてしまわないように十分に気をつけて作業を行ってください.
2.3. ボードのセットアップ
以下の手順でボードのセットアップを行ってください.
基本的にはステップ 4 は行わずに,イーサネットを繋いでネットワーク経由で PC にアクセスすることをお勧めします.
理由は,ネットワークに繋がるので,アップデートしたり新しいパッケージをインストールしたりするのが容易で,使いたい時に毎回 PC と繋げる必要がなくて楽だからです.
- マイクロ SD カードからブートするモードに設定する
- Dip switch 1 (モード 0): オン (写真の向きで下側)
- Dip switch 2 (モード 1): オフ (同上側)
- Dip switch 3 (モード 2): オフ (同上側)
- Dip switch 4 (モード 3): オフ (同上側)
- 12V の電源ケーブルを繋ぐ
- 上下があるので,間違えないように注意
- マイクロ SD カードをボード上のカードスロットに入れる
- (ネット経由ではなく直接 PC に繋ぐ場合のみ) ボード上の USB JTAG UART MicroUSB ポートと自分の PC を USB ケーブルで繋ぐ
- イーサネットケーブルをルータやスイッチに繋ぐ
- 電源をつける
電源をつけたときに,赤の LED と黄色の LED がいくつか光っているのが確認できれば,電源がしっかり供給されていることがわかります.
さらに,赤の LED が黄色に変わったらシステムがブートされているとわかります.
うまくいっていない場合は特に Dip Switch や PYNQ イメージの種類が間違っていたりすることが多いので見直しましょう.
それでも難しい場合はもう一度マイクロ SD カードに PYNQ イメージを入れ直してみましょう.
実際に起動に成功するとこんな感じになります.
起動には1分弱ほどかかったりもするので,焦らずにお待ちください.
2.4. ボードに接続
うまく起動すれば,ボードに接続して,Jupyter Notebook を使用できるはずなので,試してみましょう.
PC と直接繋ぐ場合については公式の ZCU104 Setup Guide を見てください.
ネットワーク経由で繋ぐ場合について解説しておきます.
まず,ボードと自分の PC を同じネットワークに繋ぎます.
次に,terminal で $ arp -a
をするなどして,FPGA に割り当てられていそうなローカル IP を調べます.(分からなければとりあえず,総当たりしてください )
ルータの設定画面に入れるなら,そちらから割り当てているローカル IP を探す方が正確かもしれません.
それっぽい IP を見つけたら,Chrome などのブラウザで http://(それっぽい IP アドレス)
にアクセスしてください.
うまくいくと下のような画面が表示されると思うので,Password
に xilinx
と入れて,ログインしてください.
ログインできると Jupyter Notebook が表示されるはずです.
また,Jupyter Notebook 以外にも,terminal から
$ ssh root@(IP アドレス)
として,ssh でログインして作業することも可能です.
公開鍵を設置しておくと ssh でのログイン時に便利です.
また,IP が起動するたびに変わると不便なので,IP も固定しておくと便利です.
おすすめはルータの設定に DHCP で振り分ける IP を Mac アドレスごとに固定できる設定があれば,それを利用して,毎回 FPGA に同じ IP を与えてもらえるようにしましょう.
それが難しそうなら,ssh でログインした後,
$ vim /etc/network/interfaces.d/eth0
として,ネットワークの設定ファイルを開き,アドレスを以下のように変更しましょう.
address 192.168.2.99 -> address (自分の設定したい IP アドレス)
これで,PYNQ の基本的な設定がおしまいです.
次はようやく PYNQ を使っていきましょう.
3. 生成された bitstream を PYNQ にコピーする
ここでは,タイトルの通り生成された bitstream を PYNQ にコピーしていきます.
必要なファイルは design_1_wrapper.bit
と design_1.hwh
になります.
これらを前回作った Vivado のプロジェクト内から探して,PYNQ にコピーします.
まずは,PYNQ にこのプロジェクト用のフォルダを作っておいてください.
ssh で入って mkdir をしてもいいですし,Jupyter Notebook でフォルダを作っても構いません.
作ったフォルダのパスは /path/to/pynq
としておきます.
次に,必要なファイル design_1_wrapper.bit
と design_1.hwh
を探します.
それぞれ,
/path/to/test/project_1.runs/impl_1/design_1_wrapper.bit
/path/to/test/project_1.gen/sources_1/bd/design_1/hw_handoff/design_1.hwh
にありました.
Vivado のバージョンが古い場合は二つ目のファイルは
/path/to/test/project_1.srcs/sources_1/bd/design_1/hw_handoff/design_1.hwh
にあるかもしれません.
見つけたらこの二つのファイルを先ほど PYNQ 内に作ったフォルダにコピーして,design_1_wrapper.bit
は design_1.bit
に名前を変更しておいてください.
コマンドでやるなら
$ scp /path/to/test/project_1.runs/impl_1/design_1_wrapper.bit root@(IP アドレス):/path/to/pynq/design_1.bit
$ scp /path/to/test/project_1.gen/sources_1/bd/design_1/hw_handoff/design_1.hwh root@(IP アドレス):/path/to/pynq/design_1.hwh
とすればできるはずです.
これで,動かす準備が整いました.
4. PYNQ で回路にデータを流して結果を得る
いよいよ動かすところまで来ました.
無事動くと願って進んでいきましょう.
これから書いていくソースコードは以下にあるので適宜参照してください.
4.1. 準備
まずは入力として与える lena-gray.png
を PYNQ にダウンロードしておいてください.
$ cd /path/to/pynq
$ wget https://github.com/hashi0203/Vitis_HLS_Gaussian/raw/main/pynq/lena-gray.png
また,pynq
フォルダの中に run_gaussian.ipynb
というファイルも作っておきましょう.
ここには,bitstream を読み込み,できた回路にデータを流すコードを書きます.
これで,pynq
フォルダの中には run_gaussian.ipynb
,design_1.bit
,design_1.hwh
,lena-gray.png
の 4 つのファイルがある状態になったと思います.
4.2. コードを書く
次に,Jupyter Notebook を使って,bitstream を読み込み,できた回路にデータを流すコードを書いていきましょう.
先程の run_gaussian.ipynb
を開いてください.
4.2.1. パッケージのインポート
まず,必要なパッケージをインポートしておきます.
from pynq import Overlay
from pynq import MMIO
from pynq import allocate
import pynq.lib.dma
import numpy as np
import cv2
%matplotlib inline
import matplotlib . pyplot as plt
import time
上の4つは回路を操作するのに必要なパッケージで,それ以降は画像を読み込んだり,表示したり,時間を計測したりするのに使います.
4.2.2. Overlay の読み込み
次に,Overlay を読み込み, base
という名前をつけておきます.
Overlay には bitstream ,Vivado の TCL,Python API などを含んでおり,読み込みが完了すれば Python からアクセスが可能になります.
# 回路情報を含むオーバーレイを読み込む
base = Overlay("./design_1.bit")
dir(base)
# ['__class__',
# --- 中略 ---
# '_register_drivers',
# 'axi_dma_0',
# 'axi_intc_0',
# --- 中略 ---
# 'free',
# 'gaussian_0',
# 'gpio_dict',
# --- 中略 ---
# 'timestamp',
# 'zynq_ultra_ps_e_0']
このような出力が得られると思います.
ここでは,axi_dma_0
と gaussian_0
が大事になります.
それぞれ DMA,自作 IP を操作するのに必要なので,使いやすいようにdma
,registers
と名前をつけておきましょう.
# DMA と レジスタをを操作するハンドラを設定
dma = base.axi_dma_0
registers = base.gaussian_0.register_map
4.2.3. 流し込む画像の用意
まず,画像を読み込んで,第一回で Vitis HLS で回路を書いた時に設定した画像サイズと一致するか確認しておきましょう.
# 入力画像を読み込む
src = cv2.imread("lena-gray.png", cv2.IMREAD_GRAYSCALE)
print(src.shape)
# (512, 512)
そして,回路に流し込むデータを格納するバッファ (input_buffer
と output_buffer
) を用意し,先ほど読み込んでおいた画像を input_buffer
に入れておきます.
# 画像の高さと幅を定義
height = 512
width = 512
# PL に流し込む入力と,出てくる出力の領域を確保する
input_buffer = allocate(shape=(height,width), dtype=np.uint32, cacheable=False)
output_buffer = allocate(shape=(height,width), dtype=np.uint32, cacheable=False)
# PL に流し込む入力に入力画像を入れる
input_buffer[:] = src[:height, :width]
input_buffer.flush()
4.2.4. データを流し込む
回路にデータの処理を開始させるためにフラグを立て,データを DMA 経由で流し込みます.
フラグを立てないと動かなかったりするので,キーになる部分かもしれません.
# 適当なレジスタに 1 を入れて転送を開始することを伝える
registers.CTRL.AP_START = 1
registers.CTRL.AUTO_RESTART = 1
# PL にデータを読み込ませる
dma.sendchannel.transfer(input_buffer)
# PL にデータを書き込ませる
dma.recvchannel.transfer(output_buffer)
データを流し込むのを待ち,データが送り返されるのを待ちます.
# 動作の終了を待つ
dma.sendchannel.wait()
dma.recvchannel.wait()
4.2.5. 出力結果を確認する
返ってきたデータを可視化して,実際に正しく処理が行われたのか確認しましょう.
下の図のような結果がえられて,確かにスムージングされていることが確認できると思います.
# 得られた画像を表示する
plt.figure(figsize=(10,10))
plt.subplot(1,2,1)
plt.imshow(src, cmap = "gray")
plt.title("input image")
plt.subplot(1,2,2)
plt.imshow(output_buffer, cmap="gray")
plt.title("output image")
plt.show()
4.2.6. 時間の計測
データを LOOP
回だけ送り込み,全ての結果が返ってくるまでの時間を計測し,一回あたりの時間を求めます.
SIZE = height*width*4
LOOP = 1024
t0 = time.time()
for l in range(LOOP):
dma.sendchannel.transfer(input_buffer)
dma.recvchannel.transfer(output_buffer)
dma.sendchannel.wait()
dma.recvchannel.wait()
t1 = time.time()
print("Total elapsed time:", t1-t0, "s")
print("Elapsed time per picture:", ((t1-t0) / LOOP) * 1000, "ms")
print("Throughput:", (SIZE*LOOP) / 1024 / 1024 / (t1-t0), "MBps")
# Total elapsed time: 2.9339468479156494 s
# Elapsed time per picture: 2.8651824686676264 ms
# Throughput: 349.0179110529817 MBps
結果は 2.9 秒ぐらいで 1024 枚の画像を処理できたことになり,スループットは 349 MBps ぐらいになりました.
かなり高速に処理されているのではないでしょうか.
5. まとめ
これでようやく Vitis HLS で書いた回路を PYNQ で動かすことができました
長い道のりでしたが,無事動いた方はおめでとうございます.
まだまだ分からないことだらけで不十分なところも多かったと思いますが,最後までたどり着いてくださった皆様ありがとうございます.