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

自作回路を PYNQ から使うための設計・開発法に関するコースの第2回です。今回は、第1回で作成したナイトライダー回路の IP コアに少し手を加えることで、自身が制御用の AXI Lite インタフェースを持っている場合の設計例を説明したいと思います。また、自作の IP コア専用のドライバクラスを Python で作成する方法についても取り上げます。

本コースで使用したハードウェア記述は、GitHub リポジトリ上で公開しています。今回の関連するファイルは、knight2 フォルダ内にあります。

改良版ナイトライダー回路

AXI GPIO を使う方法の問題点

第1回で紹介した AXI GPIO を使う方法は、回路記述が非常にシンプルになるという利点があります。記述において AXI インタフェースのことを考えなくて済むからです。PS との間で受け渡しが必要な入出力が少ないのであれば、この方法で手早くつないでしまうのも1つの選択でしょう。

問題は、受け渡しが必要な入出力が増えてきた場合です。こうなると、1個の AXI GPIO では賄いきれなくなってしまうかもしれません。複数の AXI GPIO を使っても良いのですが、ブロック図もごちゃごちゃしてきますし、それらをソフトウェアで制御するための Python 記述の可読性も落ちてしまうことでしょう。

この場合は、「AXI でプロセッサとつながる IP コアを作る (2)」で紹介したように、AXI-Lite のインタフェース回路を自作回路に組み込んでしまった方が、システム全体で見たときの複雑さでは有利になることでしょう。もちろん、回路 (IP コア) そのものの複雑度は、少しばかり増すことになりますが……。

回路の設計

さて、今回は前回使用したナイトライダー回路を少しばかり改良することとしましょう。変更点として、LED の点灯パターンを、パターン1~3の3種類から選べるようにします (もはやそれをナイトライダーと呼んでいいのかはさておき)。また、パターン0が選ばれた場合には、LED はすべて消灯するものとします。点灯パターンと、点灯する LED を切り替えるまでの時間は、PS から制御できるものとします。

具体的なパターンを下図に示します。パターン1は前回と同じものです。パターン2と3では、パターン1で点灯していた LED に対し、パターン2ではその左にある、パターン3ではその右にある、全ての LED もまた点灯するものとします。

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

ここで、前回のナイトライダー回路の内部構造を再掲します。

ナイトライダー回路の内部構造 (再掲)。

この回路は、カウンタ2つ (CDIV 進、6進) とデコーダがあれば作れるのでしたね。改良版ナイトライダー回路では、この中のカウンタ2つには一切手を加えずに済みます。変更が必要なのはデコーダです。デコーダに新たな入力 PATTERN を用意し、この入力と第2のカウンタの値との両方の場合分けによって、LED の点灯/消灯を決めれば良いわけです。

また、待ち時間 CDIV とパターンの種類 PATTERN を PS 側から受け取るための AXI-Lite のインタフェース回路も必要になります。以上より、改良版ナイトライダー回路の全体の内部構造は、下図に示すものとなります。

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

それではまず、AXI-Lite のインタフェース回路を除いた部分の Verilog HDL 記述を見ていきます。前回同様、これが IP コアのトップモジュールになりますので、SystemVerilog ではなく Verilog HDL を使用しています。

module knight2_pynq (
    input  wire        CLK,
    input  wire        RESETN,
    input  wire [ 3:0] AXI_CTRL_AWADDR,
    input  wire [ 2:0] AXI_CTRL_AWPROT,
    input  wire        AXI_CTRL_AWVALID,
    output wire        AXI_CTRL_AWREADY,
    input  wire [31:0] AXI_CTRL_WDATA,
    input  wire [ 3:0] AXI_CTRL_WSTRB,
    input  wire        AXI_CTRL_WVALID,
    output wire        AXI_CTRL_WREADY,
    output wire [ 1:0] AXI_CTRL_BRESP,
    output wire        AXI_CTRL_BVALID,
    input  wire        AXI_CTRL_BREADY,
    input  wire [ 3:0] AXI_CTRL_ARADDR,
    input  wire [ 2:0] AXI_CTRL_ARPROT,
    input  wire        AXI_CTRL_ARVALID,
    output wire        AXI_CTRL_ARREADY,
    output wire [31:0] AXI_CTRL_RDATA,
    output wire [ 1:0] AXI_CTRL_RRESP,
    output wire        AXI_CTRL_RVALID,
    input  wire        AXI_CTRL_RREADY,
    output reg   [3:0] LED);

    wire  [1:0] pattern;
    wire [31:0] cdiv;

入出力として、PS から AXI-Lite のリクエストを受けるための一連の信号が追加されています。この直後の記述で AXI-Lite のインタフェース回路がインスタンス化されていて、一連の信号はすべてその回路へと渡されます。インスタンス化の部分は、長い割に特にコメントすべき点もないので、割愛します。

一方で、前回のナイトライダー回路で入力だった待ち時間 CDIV は内部信号 (wire) になっています。今回新たに加わったパターン設定用の信号 PATTERN も同様です。内部信号であることがわかるように、ここでは小文字で表記しています。

    reg [31:0] count, n_count;
    reg  [2:0] pos, n_pos;
    
    always @ (pattern, pos) begin
        if (pattern == 2'd0) begin
            LED = 4'd0000;
        end else if (pattern == 2'd1) 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 else if (pattern == 2'd2) begin
            case (pos)
                3'd0: LED = 4'b1111;
                3'd1: LED = 4'b1110;
                3'd2: LED = 4'b1100;
                3'd3: LED = 4'b1000;
                3'd4: LED = 4'b1100;
                3'd5: LED = 4'b1110;
                default: LED = 4'b0000;
            endcase
        end else if (pattern == 2'd3) begin
            case (pos)
                3'd0: LED = 4'b0001;
                3'd1: LED = 4'b0011;
                3'd2: LED = 4'b0111;
                3'd3: LED = 4'b1111;
                3'd4: LED = 4'b0111;
                3'd5: LED = 4'b0011;
                default: LED = 4'b0000;
            endcase
        end
    end

第2のカウンタの値 pos に応じて LED の点灯/消灯を決めるデコーダ部分です。入力に pattern が加わっています。前回は単に case 文で pos による場合分けをしていましたが、今回はそれに if 文を用いた pattern による場合分けが加わっています。パターン0、つまり pattern が0の場合には単に全ての LED を消灯するだけですから、pos による場合分けは不要です。このあとにカウンタに相当する記述が続きますが、その部分は前回と全く同じです。

次に、AXI-Lite のインタフェース回路の変更点を見ていきます。回路は、「AXI でプロセッサとつながる IP コアを作る (2)」で紹介したものをテンプレートとして作成します。記述の変更が必要なのは、入出力の定義と、それに対応してデータを実際に書き込んだり読み出したりする部分のみです。

    input  logic         AXI_CTRL_RREADY,
    
    // ユーザ信号
    output logic [ 1: 0] KNIGHT_PATTERN,
    output logic [31: 0] KNIGHT_CDIV);

入出力の定義の部分では、パターンの種類と待ち時間を表す信号を出力として定義します。パターンは2ビット、待ち時間は32ビットです。

    // -- 書き込みデータ
    always_ff @ (posedge AXI_CTRL_ACLK) begin
        if (~ AXI_CTRL_ARESETN) begin
            KNIGHT_PATTERN <= 2'd0;
            KNIGHT_CDIV    <= 0;
        end else if (reg_we) begin
            if (d_awaddr == 2'd0)  begin
                if (AXI_CTRL_WSTRB[0]) KNIGHT_PATTERN     <= AXI_CTRL_WDATA[ 1: 0];
            end else if (d_awaddr == 2'd1)  begin
                if (AXI_CTRL_WSTRB[0]) KNIGHT_CDIV[ 7: 0] <= AXI_CTRL_WDATA[ 7: 0];
                if (AXI_CTRL_WSTRB[1]) KNIGHT_CDIV[15: 8] <= AXI_CTRL_WDATA[15: 8];
                if (AXI_CTRL_WSTRB[2]) KNIGHT_CDIV[23:16] <= AXI_CTRL_WDATA[23:16];
                if (AXI_CTRL_WSTRB[3]) KNIGHT_CDIV[31:24] <= AXI_CTRL_WDATA[31:24];
            end
        end
    end

書き込みデータの定義は、アドレスが0 (バイト) の場合にパターンの種類に、アドレスが4 (バイト) の場合に待ち時間に、それぞれ書き込みが行なえるように記述しました。

    // -- 読み出しデータ
    assign n_rdata = 0;
endmodule

対して、読み出しデータの定義では、単に0を代入しておきます。今回、PS から回路の設定を書き込むことはあっても、PS にデータを返すことはないからです。

これら2つの Verilog HDL, SystemVerilog の回路記述を使って、このあと IP コアを作成します。適当なディレクトリに hdl というフォルダを作成し、その中にこの2つのソースファイルを入れておきましょう。

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

ここからは、IP パッケージャでの IP コアの作成、IP インテグレータによるブロック図の作成、ブロック図から各種ファイルを生成して論理合成、の手順で Vivado での開発を進めていきます。IP パッケージャの詳しい使い方は、「IP の世界からこんにちは (2)」を参照してください。

PYNQ-Z1 向けの Vivado プロジェクトを作成したら、「Tools → Create and Package New IP」で IP パッケージャを起動し、「Package a specified directory」で先ほど hdl フォルダを作成したディレクトリを指定します。手順を進めると、Vivado のウィンドウがもう1つ開き、Package IP の画面が表示されます。

今回は、IP コアのベンダー・ライブラリ名を変更しておくことにしましょう。ここでは、ベンダー名を ACRi、Library を Blog に変更します。この時の画面の様子を下図に示します。IP コアの正式名称は [ベンダー名]:[ライブラリ名]:[コア名]:[バージョン] という形でつけられますので、この場合の正式名称は、ACRi:Blog:knight2_pynq:1.0 です。

IP コアのベンダー・ライブラリ名を変更する様子。

IP コアのパッケージの際に確認が必要な項目は注意マークで示されますので、適宜確認します.私の環境では、File Groups の項目で確認が必要でした。通常は、画面の指示に従って「Merge Changes…」のリンクをクリックすれば、Vivado が適切に修正してくれます。

確認が済んだら、Review and Package の項目で Package IP ボタンを押し、IP コアのパッケージが完了します。このとき、パッケージされた IP コアはプロジェクトの IP リポジトリに自動的に登録され、ブロック図に貼り付けられる状態になります。

次は、IP インテグレータでのブロック図の作成です。Create Block Design でブロック図を新規作成し、ZYNQ7 Processing System を追加し、Run Block Automation を行うところまでは、前回と同じです。ここまでの手順を済ませたときのブロック図を下図に示し (再掲し) ます。

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

先ほどパッケージした knight2_pynq をブロック図に追加し、Run Connection Automation を行うと、PS と IP コアとが自動的に接続されます。そうしたら、前回と同様に IP コアの LED 出力は Make External し、LED と名前をつけ直してあげましょう。以上の作業を終えた時のブロック図を下図に示します。

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

実は、今回のブロック図はこれで完成です。前回と比べるとだいぶ手順が減りました。Validate Design 以降の手順はいつも通り進めましょう。制約ファイル (XDC) は前回と同じものを使用できます。ビットストリームの生成まで終わったら、生成されたビットストリーム (.bit) とハードウェア情報ファイル (.hwh) を抽出して、ファイル名を knight2_pynq に変更しておきます。これらのファイルは、今回も PYNQ のユーザディレクトリの pynq/overlays ディレクトリにフォルダを作成し、そこにアップロードしておきましょう。

Python による動作確認とドライバの作成

改良版ナイトライダー回路の動作確認

それでは、作成した改良版ナイトライダー回路のオーバーレイを PYNQ の PL 部に書き込んで、動作確認をしてみましょう。まずは、Overlay クラスのインスタンスを作成します。

from pynq import Overlay

pl = Overlay("knight2_pynq.bit")

上記のスクリプトを実行すると、ひとまず回路が PL 部に書き込まれます。このときの様子を下図に示します。

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

……前回は LED がうっすら点灯していましたが、今回は完全に消灯していますね。PL に書き込んだ直後の時点では、改良版ナイトライダー回路のパターン pattern は0に設定されていますので、LED は常に消灯します。

つまり今回は、改良版ナイトライダー回路にアクセスして、パターン pattern を0以外に切り替えてから、待ち時間 cdiv を変更する、という内容のスクリプトを書くことになります。AXI-Lite インタフェース回路を記述するときに、パターンはアドレス0、待ち時間はアドレス4に割り当てましたので、スクリプトの記述例は以下の通りとなります。

from pynq import Overlay
import time

pl = Overlay("knight2_pynq.bit")
knight = pl.knight2_pynq_0
for i in range(3):
    knight.write(0, i + 1)
    for j in range(2):
        knight.write(4, 10000000); time.sleep(2.5);
        knight.write(4, 20000000); time.sleep(2.5);
knight.write(0, 0);

ブロック図上で改変版ナイトライダー回路の IP コアには knight2_pynq_0 と名前をつけましたので、ドライバにアクセスするときは pl.knight2_pynq_0 を使います。このスクリプトでは、パターン1~3のそれぞれに対して、高速 (0.1秒ごと) 切り替えで2.5秒、低速 (0.2秒ごと) 切り替えで2.5秒待つ、という動作を行います。

実際に上記のスクリプトを実行したときの様子を下記の動画に示します。

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

自作回路に対するドライバクラス

今回はこれで終わりにしてもいいのですが、もう少しスクリプトが読みやすくなるように、ひと手間かけることにしましょう。現状だと、パターンや待ち時間を変更するのにいずれも write メソッドを使っていたり、0とか4とか10000000という定数が唐突に現れています。これでは、しばらくしてから読み直したとき、あれここ何してるんだっけ……となってしまいそうです。コメントをつけておいても良いですが、根本的な解決策かと言われると微妙です。

ということで、作成した改変版ナイトライダー回路に対するドライバクラスを作成することによって、パターンや待ち時間の変更を、インスタンスのプロパティの変更という形で記述できるようにしてみます。PYNQ では、PL 上の IP コアに対するドライバクラスのひな形として、pynq.overlay.DefaultIP クラスが用意されています。ドライバクラスを自作するときには、このクラスを継承したサブクラスの定義を書いていくことになります。

改変版ナイトライダー回路に対するドライバクラスの定義の例を以下に示します。

from pynq import DefaultIP
from pynq import ps

class KnightDriver(DefaultIP):
    def __init__(self, description):
        super().__init__(description=description)
    
    bindto = ['ACRi:Blog:knight2_pynq:1.0']
    
    pattern = property()
    @pattern.setter
    def pattern(self, value):
        self.write(0, value)
    
    interval = property()
    @interval.setter
    def interval(self, value):
        self.write(4, int(value * ps.Clocks.fclk0_mhz * 1000000))

ポイントは8行目の bindto への代入です。ここで、どの IP コアに対するドライバクラスであるのかをリストアップします。このとき指定するのは IP コアの正式名称です。つまり今回は ACRi:Blog:knight2_pynq:1.0 だけを含んだリストを bindto に代入すれば OK です。また、ドライバクラスのコンストラクタ (5~6行目) では、基底クラスのコンストラクタを呼び出しておきます。こうすることで、PYNQ のシステム側で自動的にこのクラスを IP コアに割り当ててくれます。自作のドライバクラスが定義されてない場合には、単に DefaultIP が割り当てられます。

10行目以降では、プロパティとして pattern と interval と、それぞれに対するセッター関数を定義しています。先ほどのスクリプトで time.sleep の引数は秒単位で指定していましたので、interval もクロックサイクル単位ではなく秒単位で指定できた方が、都合が良さそうです。PL 側のクロック周波数は pynq.ps.Clocks.fclk0_mhz で参照できます (MHz 単位です) ので、これに 1,000,000 を乗じた値を掛けることで、秒からクロックサイクルへの変換を行います。

実際には、value が範囲外の値であった場合には例外を出すといったエラー処理などもあった方が良いのでしょうが、今回は省略しています。

上記のコードブロックを実行した上で改めて Overlay クラスのインスタンスを作成すると、pl.knight2_pynq_0 には DefaultIP ではなく作成した KnightDriver が割り当てられます。ですので、先ほどのスクリプトは下記に示す通りに書き換えることができます。

from pynq import Overlay
import time

pl = Overlay("knight2_pynq.bit")
knight = pl.knight2_pynq_0
for i in range(3):
    knight.pattern = i + 1
    for j in range(2):
        knight.interval = 0.1; time.sleep(2.5);
        knight.interval = 0.2; time.sleep(2.5);
knight.pattern = 0

先ほどと同様の動作をすることが確認できるかと思います。また、ブロックの末尾に pl? と記述してオーバーレイのヘルプを参照すると、確かに pl.knight2_pynq_0 に KnightDriver が割り当てられていることも確認できるはずです。

まとめ

今回は、AXI-Lite インタフェースを組み入れた自作回路を PYNQ に接続する例を示しました。今回のポイントは以下のとおりです。

  • PS から制御すべき入出力が多い場合には、AXI-Lite インタフェースを自作回路に組み入れてしまうのが便利
  • PYNQ では、DefaultIP を継承したクラスを作成すると、自作回路に対するドライバクラスを作成できる

次回取り扱うのは、IP コアがデータ転送用に AXI-Stream インタフェースを持っている場合の例になります。

愛知⼯業⼤学 藤枝直輝

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