PYNQ を使って Python で手軽に FPGA を活用 (4)

みなさんこんにちは。このコースでは、Python で FPGA を手軽に利用できる PYNQ とその活用方法を紹介していきます。ゴールは、Jupyter Notebook の環境を使って Python から手軽に FPGA を利用できるようになることです。

前回は、いくつかの例を交えて PYNQ を使った FPGA の操作方法を紹介しました。今回は、自分で設計したロジックを PYNQ 上で利用する開発手順を紹介します。

開発の手順

自分で設計したロジックを PYNQ の上で利用する手順は次の通りです。

  1. Vivado を使って、PS (プロセッサ) を含むロジックを実装する
  2. 合成、配置配線をしてビットストリームとハードウェア情報ファイルを作る
  3. ビットストリームとハードウェア情報ファイルを PYNQ に転送する
  4. ハードウェアを呼び出す Python スクリプトを書く

設計のエントリポイントとして Vivado の IP Integrator (IPI) というツールを用いるのが便利です。IPI の細かい使い方は必要に応じて学んでいくこととして、今回は PYNQ 向けのデザインを作るにあたって最小限の操作を紹介します。前回に引き続き、 Digilent の PYNQ-Z1 を対象に説明します。

なお、この記事では、PYNQ のバージョンの都合により Vivado 2019.1 を利用します。

PYNQ 向けデザインの概要

オリジナルのロジックを作る前に、サンプルデザインがどういう構成になっているか見てみましょう。サンプルデザインは GitHub で公開されています。

PYNQ-Z1 用のサンプルデザインは次のブロック図のようになっています。PS (プロセッサ) を中心に汎用 I/O や HDMI 入出力、音声入出力用の IP コアが接続されていることがわかります。

PS のまわりを拡大したものが次の図です。

PS と周辺ブロックは、「PS から PL (プログラマブルロジック) にアクセスする M_AXI_GP」と「PL から PS にアクセスする M_AXI_HP」で接続されています。PYNQ 上の Python プログラムで PL を制御する際には、おもに M_AXI_GP を利用します。

デザインを作ってみよう

シンプルなロジックを例題に開発の手順を辿っていきましょう。今回は、プログラムで指定した値でボード上の LED を点灯/消灯するロジックを実装します。

準備

LED のような FPGA の外のデバイスにアクセスするためにはロジックと I/O を対応づける必要があります。Vivado には、あらかじめ用意されたボードファイルを用いることで対応づけの手間を省く仕組みがあります。

PYNQ-Z1 のボードファイルはここで公開されています。ダウンロードして展開してから、Vivado のデータフォルダ (data/boards/board_files/) の下にコピーすれば利用できるようになります。

たとえば、 /tools/Xilinx/Vivado/2019.1/ に Vivado をインストールしている場合は、/tools/Xilinx/Vivado/2019.1/data/boards/board_files/ に配置します。

設計と合成

設計の手順は次の通りです。

  1. Vivado の IP Integrator (IPI) で必要な部品をおいていく
  2. ラッパーモジュールを作る
  3. 必要に応じて IPI の外側で必要なロジックを追加する。

まずはプロジェクト作成

まずは、いつものように Vivado でプロジェクトを新規作成します。プロジェクト名を pynq_overlay_test としました。

ボードファイルをインストールしているとプロジェクト作成時のデバイス選択が簡単になります。下の図がプロジェクト作成ウィザードのサマリです。PYNQ-Z1をターゲットボードに選んでいることがわかります。

IPI を使ってデザイン

プロジェクトを作成したら IPI を使ってデザインを作ります。IPI を使うには、Flow Navigator の IP INTEGRATOR にある Create Block Design をダブルクリックします。

デザインにつける名前はなんでも構いませんが、今回はデフォルトの design_1 のまますすめます。

下の図が IPI での開発の準備が整ったところです。IPI を使った設計では、右ペインに GUI でデザインを組み立てていきます。設計のながれは、(1) ボタンをクリックしてデザインに組み込みたいモジュールを選択してインスタンスを生成、(2) 生成したインスタンスのパラメタを設定する、(3) インスタンス同士を適切に接続する、の繰り返し作業です。

ボタンをクリックすると利用可能なモジュールのリストが表示されます。まずは、デザインに必須の PS のインスタンスを作ります。リストの検索フィールドに Processing と入力すると、PS をデザインに組み込むための ZYNQ7 Processing System が現れます。

IPI 上に PS のインスタンスを作成できました。上部にあらわれる Run Block Automation をクリックすると、デフォルトの設定をしてくれます。PS には動作周波数やプロセッサに接続するデバイスなどを細々と設定する必要があるのですが、その設定もボードファイルの情報を使って自動的にセットしてくれます。

Run Block Automation をクリックすると、設定の対象を選択するダイアログが開きます。「自動で設定したいアイテム」と「後で自分で設定したいアイテム」を選ぶことができるのですが、ここではデフォルトのまま OK をクリックして次にすすみます。

自動的に PS の設定と周辺の接続が完了した様子が次の図です。DDRFIXED_IO は最終的にボード上の DDR メモリや IO に接続するためのポートです。

PS のインスタンス生成と設定が終わったので、次は LED にアクセスするためのモジュールを準備しましょう。ボード上の LED にアクセスするには、BLOCK DESIGN の左上のペインで Board タブを開き LEDs を選んでダブルクリックします。

LED にアクセスするために使うモジュールを選択するダイアログが開くので、PS の M_AXI_GP との接続が容易な AXI GPIO を選びます。

AXI GPIO のインスタンス axi_gpio_0 をデザインに組み込むことができました。次は PS と接続するのですが、Run Connection Automation を使って自動的につなげることができます。

PS の自動設定をしたときと同じように、対象となるインスタンスを選ぶダイアログが表示されます。デフォルトのまま次にすすみます。

これで IPI でのデザインが完了です。最終的なデザインは次のようになりました。

PS と axi_gpio_0 の間に AXI Interconnect という接続を仲介するモジュールのインスタンスと、リセットを管理するモジュールのインスタンスが自動的に追加されました。表示がごちゃごちゃしている場合には、ツールボックスの右回転矢印ボタンをクリックすることでいい感じに整列してくれます。

IPI の自動接続では、axi_gpio_0 を PS に物理的に接続 (配線) するとともに PS のメモリアドレス上にマッピングして論理的にも接続してくれます。Address Editor タブを開いて確認すると、axi_gpio_0 は、PS 上の 0x41200000からはじまる 64KB のアドレスにマッピングされていることがわかります。ソフトウェアはこの範囲のアドレスを読み書きすることで axi_gpio_0 のレジスタファイルにアクセスしてインスタンスを制御できます。

ラッパーモジュールの作成

IPI で作成したデザインをトップモジュールとして合成・配置配線することはできません。トップモジュールとして、IPI で作成したデザインをインスタンスとして保持するラッパークラスを作ります。

トップモジュールである design_1_wrapper が生成され、その下に IPI で作ったデザインがインスタンスとしてぶら下がっていることが確認できます。

合成

一通り設計作業が終わったら、いつもどおりに Flow Navigator の PROGRAM AND DEBUG の下にある Generate Bitstream をクリックして合成・配置配線を行いましょう。

ここまでの、一連の作業が完了すると PL 上のロジックを設定する bit ファイルと、PYNQ でハードウェアモジュールにアクセスするために情報が格納された hwh ファイルを得ることができます。それぞれのファイルは、

  • pynq_overlay_test.runs/impl_1/design_1_wrapper.bit
  • pynq_overlay_test.srcs/sources_1/bd/design_1/hw_handoff/design_1.hwh

に作成されます。

PYNQ に転送

作成した design_1_wrapper.bit を design_1.bit に名前を変更し、design_1.bit と design_1.hwh を PYNQ にアップロードします。PYNQ へのファイルの転送には、Jupyter Notebook のファイルアップロード機能を使うと便利です。

今回は、Jupyter Notebook のルートディレクトリの下に my_logic_test というフォルダを作ってファイルをアップロードしました。

Python でプログラム

Jupyter Notebook の「New」→「Python 3」で新しい Python スクリプトの作成をはじめましょう。

from pynq import Overlay
base = Overlay("./design_1.bit")

とすると、合成した bit ファイルで PL を再構成できます。もしボード上の LED が点灯していた場合、それらが消灯することで、新しい bit ファイルが PL に書き込まれたことがわかります。

ここで導入した base というハンドラを使って PL 上のインスタンスにアクセスできます。試しに Jupyter Notebook のセル上で、base. に続いて TAB キーを押すと呼び出し可能なプロパティのリストが表示されます。

PS に接続したインスタンスは、デザイン設計時に決めたメモリアドレスにアクセスすることで制御できます。例えば、mmap を使って簡単にアクセスできます。Linux でデバイスドライバを作らずにデバイス操作をするときと同じですね。PYNQ では、用意されている MMIO モジュールを利用してアクセスするのが便利です。

from pynq import MMIO
mmio = MMIO(base_addr = base.ip_dict['axi_gpio_0']['phys_addr'],
            length = 0x1000,
        debug = True)

import time
mmio.write(4, 0)
for i in range(16):
    mmio.write(0, i); time.sleep(0.5)
    mmio.write(0, 0); time.sleep(0.5)

とするとボード上の LED が点滅する様子を観測することができます。

+α のステップ

ここまでやって LED の点灯・消灯を制御できるだけかな?と思われるかもしれませんが、同じ手順で GPIO モジュールを接続すれば Python から PL にアクセスできるポートを増やすことができて、他の様々なインスタンスも制御できるようになります。

たとえば、新しい GPIO モジュールを追加し、GPIO の先を

  reg [31:0] data;
  reg [31:0] gpio2_r;
  always @(posedge FCLK_CLK0) begin
      gpio2_r <= gpio2_o;
      if(gpio2_r[0] == 0 && gpio2_o[0] == 1)
          data <= data + gpio_o;
  end
  assign gpio_i = data;

のようなロジックに接続すると、ソフトウェアから書き込んだ値を PL 上で加算するようなハードウェアができます。

mmio.write(4, 0)
mmio.write(0xC, 0)
mmio.write(0, 100)
mmio.write(4, 0xffffffff)

for i in range(10):
    mmio.write(8, 1)
    mmio.write(8, 0)
    print(mmio.read(0))

とすると、mmio.write(8,1)gpio2_o[0]0から1に変化させることで、PL 上のレジスタ datagpio_o (つまり100) を足し込むことができます。そのため、mmio.read(0) で読み出した値が100から1000まで増加していることが確認できます。

まとめ

今回は、シンプルな例を使って、オリジナルロジックを PYNQ で使う開発フローを紹介しました。IPI を使った設計などの使う道具が増えてしまいますが、一度身につけてしまえば、Python スクリプトからハードウェアを操作する便利な環境を手にいれることができます。

次回は、PL から PS にアクセスするためのポートの使い方と、もう少し実用的なアプリケーションの開発のためのステップを紹介したいと思います。

参考

わさらぼ・みよしたけふみ

タイトルとURLをコピーしました