自作回路を PYNQ につなぐ様々な方法 (1)

愛知工業大学の藤枝です。この度また ACRi ブログでの執筆の機会をいただきました。今回は、2020年第1クォーター (20Q1) で三好さんが執筆された、「PYNQ を使って Python で手軽に FPGA を活用」の内容を補完するコースとなります。

FPGA や Zynq での設計にそれなりに慣れた人であれば、上記のコースを読んだあと少しばかり試行錯誤をすれば、すぐに自分の設計した回路を PYNQ 上で動かせるかもしれません。ただ、FPGA 開発に関する初級~中級レベルのコースで、これまで私が担当してきた「シリアル通信で Hello, FPGA」「IP の世界からこんにちは」「AXI でプロセッサとつながる IP コアを作る」でステップアップしてきた方には、まだ少しばかり乗り越えるべき段差が大きいと感じられたため、このコースを企画しました。自作回路を PYNQ から使うためのいくつかのパターンについて、実際の設計例を通じて理解を深めていただければと思います。

なお、このコースも中級レベルの内容となりますので、基本的なディジタル回路設計や、IP コアの作成方法、プロセッサとの接続方法については、上述したコース群を通じて理解していることを前提に話を進めます。

対象とするボードは、PYNQ-Z1 です。ところで,以前 PYNQ-Z1 ボードも ACRi ルームで使えるように準備中と書いたのですが、実現のために必要な技術的な課題が多く、少し時間がかかっています。もうしばらくお待ち下さい。

第1回では、PYNQ-Z1 の導入から自作回路を含んだオーバーレイの使用方法までを、簡潔に説明します。全体の流れをまとめるために、一部は他の記事とも重複する内容になりますが、ご容赦ください。

PYNQ-Z1 の導入

まずは、PYNQ-Z1 を使用するための最低限の機材一式を用意します。具体的には、

  • PYNQ-Z1 本体
  • microUSB ケーブル (給電・シリアル通信用)
  • Ethernet ケーブル
  • microSD カード (8 GB 以上のもの)
  • 有線 LAN アダプタ (またはルータ・ハブ)

が必要です。既に有線 LAN がセットアップされていて、すぐ近くにルータやハブが接続できる環境であれば、それを使うのが良いでしょう。そうでない場合や、あるいはボードをインターネットに接続するのは不安ということであれば、USB 接続の有線 LAN アダプタを使うのが便利です。その場合、あらかじめ 192.168.2.* (99以外) の IP アドレスをアダプタに割当てておきましょう。これらの機材の例を下の写真に示します。

PYNQ-Z1 とその周辺の機材一式。

その後、PYNQ のイメージファイルのダウンロードページから、PYNQ-Z1 向けのイメージファイルをダウンロードします。イメージファイルのバージョンは時々アップデートされていて、導入されているパッケージのバージョンや、開発に使用されている Vivado のバージョンが異なります。バージョンの違いで大きな問題は生じないと思いますが、念のため Vivado のバージョンはイメージファイルに使われているものと同じものを使うとよいでしょう。この記事を執筆している2021年4月時点では、Vivado 2020.1 に対応した v2.6 のイメージファイルが公開されていますので、以後の説明はそれに従います。

zip ファイルがダウンロードできたら、ファイルを展開し、中にある .img ファイルを microSD に書き込みます。Windows の場合は Rufus などを使うとよいでしょう。

ここまで進んだら、PYNQ-Z1 に microUSB ケーブルと Ethernet ケーブルを接続し、イメージファイルを書き込んだ microSD カードをボード裏面に挿入します。ジャンパピンが JP4 は SD、JP5 は USB の位置に挿入されていることを確認し、ボード左下の電源スイッチをオンにします。起動後しばらくして右下の LED が点滅したら、サーバの起動と準備は完了しています。

もし有線 LAN アダプタを使用している場合は、PYNQ-Z1 にあらかじめ割当てられた固定 IP である 192.168.2.99 にブラウザでアクセスすると、Jupyter Notebook が開きます。

もし既存の有線 LAN で DHCP が有効になっている場合は、まずは PYNQ-Z1 に割当てられた IP アドレスを知る必要があります。Tera Term などでシリアルポートを開く (通信速度は 115,200 bps にする) と、プロンプトが表示されます。このとき何も表示されない場合は、エンターキーを押してみてください。プロンプトが表示されたら、ifconfig と入力してエンターキーを押すと、ネットワークの設定状態が表示されます。そこからボードに割当てられた IP アドレスを確認して、そのアドレスにブラウザでアクセスすると、Jupyter Notebook が開きます。

Jupyter Notebook では最初にログインを求められますので、xilinx と入力してログインします (初期設定では、ユーザ名・パスワードともに xilinx と設定されています)。ログインに成功すると、Jupyter Notebook のメイン画面が開きます。

FPGA (PL) 部分の開発

回路の設計

今回は、簡単な LED 制御回路として、ナイトライダー回路を取り上げます。ここでは、ナイトライダー回路を、PYNQ-Z1 に搭載された4つの LED のうち1つを、時間とともに左右にバウンドするパターンで点灯させるものと定義します。点灯パターンを図示すると下図の通りとなります。

ナイトライダー回路の LED 点灯パターン。

点灯する LED を切り替えるまでの時間は、入力によって制御できるものとします。具体的には、回路は32ビットの入力をもち、その入力と同じだけのクロックサイクルが経過するたびに、次の LED を点灯させることにします。FPGA (PL; Programmable Logic) 部分の動作周波数はデフォルトでは 100 MHz ですので、例えば入力を 10,000,000 (10M) に設定すれば、0.1 秒ごとに LED が切り替わることになります。

まずはナイトライダー回路を設計して、HDL で記述することにしましょう。まずは回路の外見、入出力から考えます。入力は、クロック (CLK) と負論理のリセット (RESETN)、そして待ち時間を表す32ビットの入力 (CDIV) の3つです。出力は4ビットの LED だけですね。

ナイトライダー回路の内部構造。

回路の中身は、カウンタ2つとデコーダを組み合わせることで作れそうです。ナイトライダー回路の内部構造を上図に示します (リセット入力は省略しています)。

第1のカウンタは、待ち時間 CDIV まで数えたかどうかを判断する、CDIV 進のカウンタです。第2のカウンタは、左端の LED が点灯した時を 0 として、何回 LED が切り替わったかを数えます。左端の LED が最初に点灯してからもう1度点灯するまでの間には6回の LED 切り替えが必要ですから、第2のカウンタは6進カウンタとなります。カウンタ1が最後まで数え終わる (TC; Terminal Count) と、カウンタ2が有効 (EN; ENable) となりインクリメントされます。最後に、カウンタ2の値 (C; Count) を、デコーダ回路でLED の点灯/消灯に変換すれば完成です。

以上を Verilog HDL で記述すると、以下のソースコードが得られます。本当は SystemVerilog で書きたかったのですが、Vivado では SystemVerilog 記述を IP コアのトップモジュールにできません。wirereglogicalwaysalways_combalways_ff と、心の中で読み替えてください。

module knight_pynq (
    input  wire        CLK, RESETN,
    input  wire [31:0] CDIV,
    output reg   [3:0] LED);

    reg [31:0] count, n_count;
    reg  [2:0] pos, n_pos;
    
    always @ (pos) begin
        case (pos)
            3'd0: LED = 4'b0001;
            3'd1: LED = 4'b0010;
            3'd2: LED = 4'b0100;
            3'd3: LED = 4'b1000;
            3'd4: LED = 4'b0100;
            3'd5: LED = 4'b0010;
            default: LED = 4'b0000;
        endcase
    end

    always @ (CDIV, count, pos) begin
        if (count >= CDIV) begin
            n_count = 1;
            if (pos == 3'd5) begin
                n_pos   = 3'd0;
            end else begin
                n_pos   = pos + 1'b1;
            end
        end else begin
            n_count = count + 1'b1;
            n_pos   = pos;
        end
    end

    always @ (posedge CLK) begin
        if (~ RESETN) begin
            count <= 0;
            pos   <= 3'd0;
        end else begin
            count <= n_count;
            pos   <= n_pos;
        end
    end
endmodule

このソースコードは knight_pynq.v という名前で保存しておきます。

ブロック図の作成と論理合成

回路記述の準備ができたら、IP インテグレータでブロック図を作成していきます。IP インテグレータの基本的な使い方は、「IP の世界からこんにちは」の第4回を参照してください。なお、今回は HDL で書かれたソースコードが1つだけですので、IP パッケージャで IP コアを作成する手順は省略します。

PYNQ-Z1 を対象にプロジェクトを作成したら、先ほど作成した knight_pynq.v を Add Source(s) でプロジェクトに追加してから、Create Block Design でブロック図を新規作成します。その後、まずは ZYNQ7 Processing System を追加し、Run Block Automation を行います。ここまで終わると下図に示すブロック図が得られます。

ZYNQ7 Processing System の設定が終了した時の様子。

今回は、自作回路の入出力に AXI インタフェースを使っていませんので、「PYNQ を使って Python で手軽に FPGA を活用」の第4回でも紹介されていた、AXI GPIO モジュールを経由した方法を使って、PS (Processing System) とのやり取りをしたいと思います。ZYNQ7 Processing System を追加したときと同じ要領で、AXI GPIO を1つ追加します。追加した AXI GPIO をダブルクリックして設定画面を開き、IP Configuration → All Outputs にチェックを入れます。このときの画面の例を下図に示します。

AXI GPIO の設定画面の一部。

OK ボタンを押して変更を適用し、右側の GPIO 端子の横の + マークをクリックすると、下図に示す gpio_io_o という32ビットの出力ポートがあることが確認できます。

32ビット出力ポートをもつ AXI GPIO。

そして、先ほど作成したナイトライダー回路もブロック図に追加します。ブロック図上の何もない所を右クリックして、Add Module を選択し、knight_pynq をブロック図に追加します。次に Run Connection Automation を実行しますが、GPIO とナイトライダー回路との間はうまく接続できないようですので、GPIO の S_AXI とナイトライダー回路の CLK だけを自動接続します。AXI GPIO の gpio_io_o 出力とナイトライダー回路の CDIV 入力との間は、手動で接続します。

最後に、ナイトライダー回路の LED 出力を Make External し、LED と名前をつけ直せば、ブロック図は完成です。最終的なブロック図の例を下図に示します。

ナイトライダー回路を含むオーバーレイのブロック図。

ブロック図完成からの手順はいつもの通りです。

  • ブロック図に対して Validate Design
  • ブロック図から各種のファイルを Gerenate
  • 制約ファイル (XDC) のプロジェクトへの追加
  • HDL Wrapper の作成
  • Generate Bitstream で論理合成以降の一連の処理

を、それぞれ順番に実行していきます.今回はプロジェクトに knight_pynq.v が既にあるため、HDL Wrapper の作成を忘れると、knight_pynq.v だけを論理合成しようとする (そして失敗する) ので、注意してください。制約ファイル (XDC) は以下のとおりです。

set_property -dict { PACKAGE_PIN R14   IOSTANDARD LVCMOS33 } [get_ports { LED[0] }];
set_property -dict { PACKAGE_PIN P14   IOSTANDARD LVCMOS33 } [get_ports { LED[1] }];
set_property -dict { PACKAGE_PIN N16   IOSTANDARD LVCMOS33 } [get_ports { LED[2] }];
set_property -dict { PACKAGE_PIN M14   IOSTANDARD LVCMOS33 } [get_ports { LED[3] }];

通常の ZYNQ (や MicroBlaze) での開発ではこのあと Vitis を使った作業に移るところですが、PYNQ の場合は生成されたビットストリーム (.bit) とハードウェア情報ファイル (.hwh) を抽出して、それをボードにアップロードすることになります。ブロック図をデフォルト名 (design_1) で作成した場合、ファイルの所在はそれぞれ、

  • プロジェクト名.runs/impl_1/design1_wrapper.bit
  • プロジェクト名.srcs/sources_1/bd/design_1/hw_handoff/design_1.hwh

となります。ファイルを適当な場所にコピーし、ファイル名を knight_pynq に変更しておきます。

2021-11-23 追記: Vivado 2020.2 以降を使っている場合には、.hwh ファイルの所在は プロジェクト名.gen/sources_1/bd/design_1/hw_handoff/design_1.hwh になります。

PYNQ へのアップロードと動作確認

作成した PL 部の回路 (PYNQ ではオーバーレイといいます) を PYNQ から扱うには、まず上述した2つのファイルを PYNQ のファイルシステムの所定の場所にアップロードする必要があります。1つの方法は、Jupyter Notebook のアップロード機能を使って、スクリプトと同じディレクトリにアップロードしてしまう方法です。この場合、ビットストリームにはスクリプトからの相対パス、あるいは絶対パスでアクセスすることになります。

もう1つは、ユーザディレクトリ (/home/xilinx) の pynq/overlays ディレクトリにフォルダを作成し、そこにファイルをアップロードする方法です。この方法では、スクリプトの置き場所に関係なく、ファイル名のみでビットストリームを参照できます。

既に ACRi ルームを利用している Windows ユーザの方でしたら、ACRi ルームのサーバと同じように、WinSCP (窓の杜経由で DL) を使ってファイルをアップロードするのも良いでしょう。ここでは、pynq/overlays/knight_pynq ディレクトリにファイルをアップロードした例を示します。

ファイルを ~/pynq/overlays 以下にアップロードした例。

ここまで終わったら、ブラウザから PYNQ 上の Jupyter Notebook にアクセスし、新しいスクリプトを作成します。オーバーレイを PL 部に書き込み、回路を動作させるための最低限のスクリプトを、以下に示します。

from pynq import Overlay

pl = Overlay("knight_pynq.bit")

PYNQ 関係のライブラリは pynq というパッケージに収められていますので、そこからオーバーレイ関係のライブラリ Overlay をインポートします。その上で、ファイル名を指定して Overlay クラスのインスタンスを作成すると、そのオーバーレイが自動的に PL 部に書き込まれる仕組みになっています。上記のスクリプトを実行したときの様子を下図に示します。

ナイトライダー回路のオーバーレイを PL 部に書き込んだ直後の様子。

…… LED はうっすら点灯しているように見えますが、よくわかりませんね。これは、ナイトライダー回路の入力 CDIV が設定されていない (正確には、0 になっている) ために、超高速で LED が切り替わってしまっていることが原因です。

ということで、AXI GPIO にアクセスして、CDIV の値を書き換えましょう。例えば、高速 (0.1 秒ごと) 切り替えで3秒間、低速 (0.2 秒ごと) 切り替えで3秒間、それぞれ回路を動作させるパターンを3度繰り返したい場合、以下のようなスクリプトを実行すれば OK です。

from pynq import Overlay
import time

pl = Overlay("knight_pynq.bit")
gpio = pl.axi_gpio_0
for i in range(3):
    gpio.write(0, 10000000); time.sleep(3)
    gpio.write(0, 20000000); time.sleep(3)
gpio.write(0, 0xffffffff)

各 IP コアに対応するドライバのインスタンスが、Overlay クラスのインスタンス (ここでは pl) の下に用意されています。ブロック図上で AXI GPIO には axi_gpio_0 と名前がついていましたので、pl.axi_gpio_0 で当該のドライバにアクセスできます。メモリマップされたレジスタには、このインスタンスの read, write メソッドを使ってアクセスできます。

今回は、AXI GPIO の0番地でアクセスできるレジスタの値が CDIV に接続されています。スクリプトでは、これに write メソッドで 10,000,000 (0.1秒)、あるいは 20,000,000 (0.2秒) を書き込み、LED の切り替え速度を変更しています。スクリプトの最後では CDIV に 0xFFFFFFFF を書き込み、LED の切り替えをほぼ停止した状態にしています。厳密には、約42.9秒 (≒ 232 / 100,000,000) ごとに LED が切り替わるはずです。今回は読み出しは行っていませんが、もし AXI GPIO から値を読み出したい場合には、同様に read メソッドを使います。

実際に上記のスクリプトを実行すると、LED が高速・低速を切り替えながら左右にバウンドする様子が確認できます。このときの様子を下記の動画に示します。

LED の切り替え間隔を制御するスクリプトを実行したときの様子。

まとめ

今回は、PYNQ-Z1 の導入から自作回路を含んだオーバーレイの使用方法までの手順を説明するとともに、AXI インタフェースをもたない自作回路を PYNQ に接続する例を示しました。要点は、

  • AXI インタフェースをもたない自作回路を PYNQ に接続する場合は、AXI GPIO との併用が便利
  • メモリマップされた PL のレジスタにアクセスするには、read, write のメソッドを使う

といったところでしょうか。

次回は、IP コア自身が制御用の AXI Lite インタフェースを持っている場合の例を見ていきます。

愛知⼯業⼤学 藤枝直輝

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