Ultra96でPYNQを使う

Pocket

ACRiブログ PYNQ を使って Python で手軽に FPGA を活用 (4) に寄稿した文章では、PYNQ-Z1を使ってPYNQの使い方を紹介しました。その記事に対して、Ultra96だとどうなの?というコメントがフォーラムに寄せられていたので、簡単にフォローです。

準備

Pynqの準備

まずは、AvnetのGitHubリポジトリのリリースページからv2.5 PYNQ for Ultra96 を ダウンロードします。1.7GBなので、それなりに時間がかかるかもしれません。ダウンロードできたら展開してMicroSDカードに書き込みます。

$ unzip ultra96v1_v2.5.zip
$ sudo dd if=ultra96v1_v2.5.img of=/dev/sda

Vivadoの準備

PLに自分のロジックを実装して利用するためには Vivado のインストールが必要です。 Vivado 2019.1 をインストールします。また、Ultra96を簡単に利用するために Avnet が提供しているボード定義ファイルもインストールしておきましょう。https://codeload.github.com/Avnet/bdf/zip/master にアクセスして、 bdf-master.zip をダウンロードします。ダウンロードしたら展開し、bdf-master以下のディレクトリを、/tools/Xilinx/Vivado/2019.1/data/boards/board_files 以下にコピーします(パスは自分がVivadoをインストールしたときにあわせてください)。

電源投入とJupyter Notebookへのアクセス

USBケーブルと電源を接続してパワーオンします。Ultra96とはUSBポート接続越しでTCP/IP通信ができます.電源を投入すると

  • IPアドレスは Ultra96 から振られる
  • ホストPCには192.168.3.100が振られた

という動作をします。Ultra96は 192.168.3.1 というIPアドレスなので、ブラウザから 192.168.3.1 へアクセスします。アクセスしたら、

  • http://192.168.3.1:9090/notebooks/sensors96b/sensors_mezzanine_examples.ipynb をやってみる(Groveモジュールもってなくても,sensors96b.bitによるオーバレイの動作確認として)
  • http://192.168.3.1:9090/notebooks/common/overlay_download.ipynb でビットストリームのロード時間が測定できる

として、Jupyter Notebook経由でFPGAを操作できていることが確認できます。

適当なデザインを作ってみる

PLにオリジナルの回路をいれてみましょう。次のような手順です。

  • Vivado 2019.1 でプロジェクトを作る。たとえば、ultra96_pynq_testという名前でプロジェクト作りましょう。プロジェクト設定では、ターゲットボードとして Ultra96v1を選択します(ターゲットボードに Ultra96v1がでてこない場合にはボード定義ファイルを正しくセットアップできてません)。

プロジェクト作成のサマリはこんな感じです(画像をクリックで拡大)。

Vivado IPIでブロックデザインを作る

  • Zynq UltraScale+ MPSoCとGPIOのインスタンスを生成(画像をクリックで拡大)

  • GPIOのインスタンス axi_gpio_0 を Enable Dual Channel にチェックをいれる
  • GPIOを All Outputsに GPIO2 を All Inptusにセット

  • Run Block Automationをクリック
  • Run Connection Automation クリック。All Automation に チェックをいれて実行
  • Create Portで pl_clk という名前のクロック出力ポートを生成し、pl_clk_0 に接続
  • maxihpm1_fpd_aclk を pl_clk_0 に接続

最終的にこんな感じのブロックデザインが出来上がります(画像クリックで拡大)。M_AXI_HPM1_FPDがオープンなので、Run Connection Automationが表示されますが無視します。

  • ブロックデザインを保存して Create HDL Wrapper… で,生成したブロックダイアグラムのHDLラッパーを作成

トップモジュールを用意して合成

  • トップモジュール(top.v)を作って合成.(GPIOの0bit目を0->1にすると内部レジスタに100を加算するロジック)
//
`default_nettype none

module top();
  wire [31:0]gpio_rtl_0_tri_i;
  wire [31:0]gpio_rtl_o;
  wire pl_clk;
  design_1_wrapper design_1_wrapper_i(
    .gpio_rtl_0_tri_i(gpio_rtl_0_tri_i), // PL -> PS
    .gpio_rtl_tri_o(gpio_rtl_o), // PS -> PL
    .pl_clk(pl_clk)
  );

  reg [31:0] data = 32'h0;
  reg [31:0] gpio_rtl_o_reg = 32'h0;

  always @(posedge pl_clk) begin
    gpio_rtl_o_reg <= gpio_rtl_o;
    if(gpio_rtl_o_reg[0] == 0 && gpio_rtl_o[0] == 1) begin
      data <= data + 100;
    end
  end
  assign gpio_rtl_0_tri_i = data;

endmodule

`default_nettype wire
//

合成がおわると,

  • ./ultra96_pynq_test.srcs/sources_1/bd/design_1/hw_handoff/design_1.hwh
  • ./ultra96_pynq_test.runs/impl_1/top.bit

ができあがります。

PYNQで動作確認

次の手順で実環境上で動作確認ができます。

  • top.bitとdesign_1.hwhをJupyter Notebookにアップロード
  • top.bitは,design_1.bitに名前を変更
  • Python3スクリプトを新規作成.
  • ビットストリームをFPGAにセット(以下のようなコード片をJupyter Notebookに入力して実行)
from pynq import Overlay
base = Overlay("./design_1.bit")
  • GPIOのインスタンス axi_gpio_0 へのハンドラを取得(以下のようなコード片をJupyter Notebookに入力して実行)
from pynq import MMIO
mmio = MMIO(base_addr = base.ip_dict['axi_gpio_0']['phys_addr'], length = 0x1000, debug = True)
  • axi_gpio_0 のポート0のビット0をパタパタしてaxi_gpio_0 のポート1から値を読み出(以下のようなコード片をJupyter Notebookに入力して実行)
mmio.write(0,0); mmio.write(0,1); mmio.write(0,0) # ビット0をパタパタする
mmio.read(8) # ポート1の値(PL->PS)を読み出す

mmio.read(8)で、PL内部で100ずつ加算された結果を読み出すことができ、正しくロジックが動作していること、Jupyter Notebookから制御できていることがわかります。