Vitis HLS と Vivado で作った回路を PYNQ を使って FPGA 上で動かすまで ー④PYNQ を使って実機で動かすー

公開日:
最終更新日:
PYNQ を使って実機で動かす

高位合成を使って FPGA 上に回路実装をしていくお話の第四弾になります.
これがついに最終回の予定です!
他の回は以下にあるのでご覧ください.

  1. コードを書く
  2. シミュレーションをする
  3. Vivado で回路を組む
  4. PYNQ を使って実機で動かす (この記事)

1. 今回行うこと

全体の流れは以下のようになっています.

  1. C++ を使って Vitis HLS 用に実行したいコードを書く
  2. Vitis HLS を使って,シミュレーションをしてデバッグする
  3. Vitis HLS を使って,Verilog にコンパイルする
  4. コンパイルしてできた IP を使って,Vivado で回路を組む (これが難しい…)
  5. Vivado を使ってできた回路から bitstream を作成する (いわゆるジェネビ)
  6. 生成された bitstream を PYNQ にコピーする
  7. PYNQ で回路にデータを流して結果を得る

今回は 6. 生成された bitstream を PYNQ にコピーする7. PYNQ で回路にデータを流して結果を得る の手順を解説していきたいと思います.

ソースコードは以下に公開しているので適宜参照してください.

2. PYNQ の準備

まずは,PYNQ を準備しないといけないということで,SD カードに PYNQ を書き込んで,FPGA 上で起動し,PC からアクセスできるか確かめていきましょう.

おさらいですが,PYNQ というのは FPGA 上で動く Linux の一種で,回路にデータを送り込んだり,時間を計測したりするのに使用します.

具体的な方法は公式の ZCU104 Setup GuidePYNQ を使って 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 と繋げる必要がなくて楽だからです.

FPGA の設定
FPGA の設定
  1. マイクロ SD カードからブートするモードに設定する
    1. Dip switch 1 (モード 0): オン (写真の向きで下側)
    2. Dip switch 2 (モード 1): オフ (同上側)
    3. Dip switch 3 (モード 2): オフ (同上側)
    4. Dip switch 4 (モード 3): オフ (同上側)
  2. 12V の電源ケーブルを繋ぐ
    1. 上下があるので,間違えないように注意
  3. マイクロ SD カードをボード上のカードスロットに入れる
  4. (ネット経由ではなく直接 PC に繋ぐ場合のみ) ボード上の USB JTAG UART MicroUSB ポートと自分の PC を USB ケーブルで繋ぐ
  5. イーサネットケーブルをルータやスイッチに繋ぐ
  6. 電源をつける

電源をつけたときに,赤の LED と黄色の LED がいくつか光っているのが確認できれば,電源がしっかり供給されていることがわかります.
さらに,赤の LED が黄色に変わったらシステムがブートされているとわかります.
うまくいっていない場合は特に Dip Switch や PYNQ イメージの種類が間違っていたりすることが多いので見直しましょう.
それでも難しい場合はもう一度マイクロ SD カードに PYNQ イメージを入れ直してみましょう.

実際に起動に成功するとこんな感じになります.
起動には1分弱ほどかかったりもするので,焦らずにお待ちください.

起動時の FPGA の状態
起動時の FPGA の状態

2.4. ボードに接続

うまく起動すれば,ボードに接続して,Jupyter Notebook を使用できるはずなので,試してみましょう.
PC と直接繋ぐ場合については公式の ZCU104 Setup Guide を見てください.

ネットワーク経由で繋ぐ場合について解説しておきます.
まず,ボードと自分の PC を同じネットワークに繋ぎます.
次に,terminal で $ arp -a をするなどして,FPGA に割り当てられていそうなローカル IP を調べます.(分からなければとりあえず,総当たりしてください 😇)
ルータの設定画面に入れるなら,そちらから割り当てているローカル IP を探す方が正確かもしれません.

それっぽい IP を見つけたら,Chrome などのブラウザで http://(それっぽい IP アドレス) にアクセスしてください.
うまくいくと下のような画面が表示されると思うので,Passwordxilinx と入れて,ログインしてください.
ログインできると Jupyter Notebook が表示されるはずです.

PYNQ のログイン画面
PYNQ のログイン画面

また,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.bitdesign_1.hwh になります.
これらを前回作った Vivado のプロジェクト内から探して,PYNQ にコピーします.

まずは,PYNQ にこのプロジェクト用のフォルダを作っておいてください.
ssh で入って mkdir をしてもいいですし,Jupyter Notebook でフォルダを作っても構いません.
作ったフォルダのパスは /path/to/pynq としておきます.

次に,必要なファイル design_1_wrapper.bitdesign_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.bitdesign_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.ipynbdesign_1.bitdesign_1.hwhlena-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_0gaussian_0 が大事になります.
それぞれ DMA,自作 IP を操作するのに必要なので,使いやすいようにdmaregisters と名前をつけておきましょう.

# 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_bufferoutput_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()
得られた Lena の画像
得られた Lena の画像

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 で動かすことができました 👏
長い道のりでしたが,無事動いた方はおめでとうございます.
まだまだ分からないことだらけで不十分なところも多かったと思いますが,最後までたどり着いてくださった皆様ありがとうございます.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA