この記事は、AXI を使ってプロセッサと連携する回路を作ることを目標に、そうした回路の設計例について説明するコースの第2回です。
前回は、Zynq と AXI のインタフェースの概要を説明しました。今回は、AXI-Lite の利用例として、プロセッサとの制御や少量のデータのやりとりを行うインタフェース回路について説明します。
なお、今回~第3回で使用する回路の SystemVerilog 記述は、GitHub からダウンロードできるようにしています。今回取り上げる回路記述に対応するファイルは、このうちの sender/hdl/AXI_ctrl.sv です。
AXI における信号の依存関係
ハンドシェークの基本ルール
「IP の世界からこんにちは (4)」や、第1回で、AXI の各チャネルには valid と ready という種類の信号があり、これらがともに ‘1’ であるときにそのチャネルでの通信が成立する、ということを説明しました。このような回路間の通信成立のルールをハンドシェーク (握手という意味) とよびます。
これには一定のルールが必要です。仮に、「データなどの送信側は valid を ‘1’ にする前に受信側の ready が ‘1’ になるのを待つ」という制御を構成したとします。同様に、「受信側は ready を ‘1’ にする前に送信側の valid が ‘1’ になるのを待つ」という制御を構成したとします。そうすると、困ったことに、送信側・受信側ともに相手の信号が ‘1’ になるまで待ち続けてしまい、永久に通信が成立しなくなる事態に陥ります。これをデッドロック (手詰まりという意味) といいます。それぞれが好き勝手に制御を構成してしまうと、こうなる可能性があります。
デッドロックは一般に、ある信号が別の信号を待ち合わせる関係、すなわち依存関係が、ループを作っているときに起こります。デッドロックを回避するには、少なくとも片方はこのような制御を行わないという制限を加えて、ループを作らないようにしなければなりません。

AXI では、送信側に制限を与えることで、デッドロックの問題を解決します。つまり、データなどの送信側が valid を ‘1’ にする際、受信側の ready が ‘1’ になるのを待つことは禁止されます。このルールは全てのチャネルに適用される「AXI の基本ルール」です。このルールを下図に示します。これ以降、同様のルールは青い矢印で示します。

なお、一旦 ‘1’ にした valid を、ready が ‘1’ になる前に ‘0’ に戻すことも禁止されています。これを許容してしまうと、やはり永久に通信が成立しなくなる可能性があるためです。
読み出し・書き込みにおける追加ルール
また、AXI や AXI-Lite において、読み出しは AR・R チャネル、書き込みは AW・W・B チャネルを使った一連の流れで読み書きを行います。ここではチャネルをまたいだ依存関係のルールが存在します。それは、読み書きのリクエスト成否を相手側に送るときには、リクエストが完了している必要がある、ということです。リクエストの途中で勝手に成否を送ってはいけません。
以上を踏まえて、下図に読み出しのルールを示します (仕様書をもとに再構成)。

同チャネルの valid – ready 間のルールにより、ARVALID と ARREADY の間、RVALID と RREADY の間は、それぞれ青い矢印で結ばれます。また、上述したルールで読み出しのリクエストが完了している、というのは、ARVALID と ARREADY とがともに ‘1’ であるときです。つまり、RVALID を ‘1’ にする際は、ARVALID と ARREADY が両方 ‘1’ になるのを待つ必要があります。図中ではこのルールを赤い二重矢印で示しています。
書き込みについてはもう少しだけ複雑になります。下図に書き込みのルールを示します (同じく、仕様書をもとに再構成)。

valid – ready 間のルールは、AW チャネルと W チャネルをまたぐときにも同様に適用されます。AW と W とでチャネルが分かれているため、ついうっかり、アドレス (AW チャネル) に関する通信が完了したのを確認してから、データ (W チャネル) を準備する、というような制御を書きたくなります。しかしこれは、WVALID → AWREADY の間のルールに反することです。
また、書き込みの成否を送る際は、W チャネルで全ての書き込みデータの送受信を完了している必要があります。書き込みデータの末尾は WLAST が ‘1’ になることで示されます。そのため、BVALID を ‘1’ にするには WLAST、WVALID と WREADY が全て ‘1’ になるのを待つ必要があります。AXI-Lite の場合は、データの長さは必ず 1 ワードなので、WLAST をチェックする必要はありません (というか、そもそも WLAST がありません)。
もちろん、このデータに対応するリクエストが AW チャネルを通じて送られている必要もあります。これは AWVALID や AWREADY と BVALID の間にも依存関係のルールがある、という意味です。ただ、一般にはデータよりもアドレスが後に送られるということはまれでしょうから、上図には示していません。
AXI-Lite のインタフェース回路
「IP の世界からこんにちは (4)」 では、AXI-Lite でプロセッサからの読み書きを受け取る IP コアのテンプレートをざっと眺めました。しっかりと眺めてもらった方はお気づきかもしれませんが、このテンプレートはほとんどの信号に対するロジックを個別に生成していて、全体のつながりを理解しづらい記述になっています。
そこで今回は、(私自身の練習も兼ねて) AXI におけるハンドシェークの制御を、上述した仕様書に対する理解をもとに、ゼロから書き直してみました。
ハンドシェーク制御の概要
AXI のハンドシェークのルールを守りつつ、できるだけ簡単な制御とするには、以下のような手順を踏むことが考えられます。
- 普段はプロセッサからのリクエストを受理できない状態にしておき、リクエストが送られてくるのを待つ。
- リクエストが送られてきたら、アドレスを記憶してから、リクエストを受理できる状態にする。
- 2 の次のサイクルで読み書きを行い、リクエストを再び受理できない状態にし、結果の準備ができたことをプロセッサに通知する。
- 結果がプロセッサによって受理されたら、結果を無効化する。
ちょうどこれは、先に見てきた依存関係のルールの図を、U の字を書くようになぞっていくことに相当します。このことを下図に示します。左上・左下・右下・右上が、それぞれのステップとおおむね対応します (AXI-Lite では WLAST は不要なので、グレーで示しています)。

ready の扱いには2通り考えられます。1つは、普段は ‘1’ にしておいて、 受け入れ不能なときだけ ‘0’ にする、という考え方です。もう1つは逆に、普段は ‘0’ にしておいて、受け入れるときだけ ‘1’ にする、という考え方です。
前者は、一連の通信に必要なサイクル数が少ないというメリットがあります。ただ、valid が ‘1’ となったらそのサイクルで通信が成立してしまいます。通信成立のタイミングをプロセッサ側に委ねているとも言えます。また、特に書き込みにおいては AW チャネルと W チャネルとの valid が異なるタイミングで ‘1’ になった場合の扱いが面倒です。
後者は、IP コアが好きなタイミングで ready を ‘1’ にすることで、通信を成立させるやり方です。通信成立の主導権は IP コア側にあるので、制御が単純で済みます。デメリットは必要なサイクル数が増加することですが、AXI-Lite で多くのデータをやりとりするのはまれです。制御や引数を受け渡しする時の数サイクルの得や損を議論するよりも、その先にある回路の最適化を検討するべきでしょう。
実際の SystemVerilog 記述
記述したインタフェース回路の一部を示します。書き込み制御にあたる部分は42行目からです。なお、ある信号の次回の値を、信号名に「n_」をつけることで示しています。また、前回の値には、信号名に「d_」をつけて示しています。
logic [ 1: 0] d_awaddr;
logic n_wready, n_bvalid;
logic reg_we;
assign AXI_CTRL_AWREADY = AXI_CTRL_WREADY;
// -- 書き込み制御
always_comb begin
n_wready = AXI_CTRL_WREADY;
n_bvalid = AXI_CTRL_BVALID;
reg_we = 1'b0;
if (AXI_CTRL_AWVALID & AXI_CTRL_WVALID) begin
if (~ AXI_CTRL_WREADY) begin
n_wready = 1'b1; // 1 -> 2
end else begin
n_wready = 1'b0; // 2 -> 3
n_bvalid = 1'b1;
reg_we = 1'b1;
end
end
if (AXI_CTRL_BVALID & AXI_CTRL_BREADY) begin
n_bvalid = 1'b0; // 3 -> 4
end
end
always_ff @ (posedge AXI_CTRL_ACLK) begin
if (~ AXI_CTRL_ARESETN) begin
AXI_CTRL_WREADY <= 1'b0;
AXI_CTRL_BVALID <= 1'b0;
d_awaddr <= 2'b00;
end else begin
AXI_CTRL_WREADY <= n_wready;
AXI_CTRL_BVALID <= n_bvalid;
if (n_wready) begin
d_awaddr <= AXI_CTRL_AWADDR[3:2];
end
end
end
先ほどの手順は以下の通り具体化されます。
- AWREADY と WREADY を ‘0’ に初期化し (69~70行)、AWVALID と WVALID がともに ‘1’ になるのを待つ (53行)。
- AWADDR を記憶し (75~77行)、AWREADY と WREADY を ‘1’ にする (55行)。
- 2 の次のサイクルで読み書きを行い (59行)、AWREADY と WREADY を ‘0’ にし (57行)、BVALID を ‘1’ にする (58行)。
- BREADY が ‘1’ になったら、BVALID を ‘0’ にする (62~64行)。
2と3とは、既に WREADY を ‘1’ にしたかどうかで区別しています (54行)。
データを実際に書き込む部分は、82行目以降で以下の通り記述されています。
always_ff @ (posedge AXI_CTRL_ACLK) begin
if (~ AXI_CTRL_ARESETN) begin
SENDER_GO <= 1'b0;
end else if (reg_we) begin
if (d_awaddr == 2'd0) begin
if (AXI_CTRL_WSTRB[0]) SENDER_GO <= AXI_CTRL_WDATA[ 0];
end
end
end
ここでは、回路の動作開始を示す1ビット信号 SENDER_GO をユーザ回路に渡す場合を考えています。リセット時の値を定めておいて (84行)、書き込みが発生したら (85行)、アドレスと WSTRB をもとにレジスタへの書き込みを行います (86~88行)。
書き込み制御は97行目から始まりますが、読み出し制御とやっていることはほとんど同じなので、ここでは割愛します。データを読み出す部分は、137行目以降に書かれています。
always_comb begin
if (d_araddr == 2'd0) begin
n_rdata = {31'b0, SENDER_RUN};
end else if (d_araddr == 2'd1) begin
n_rdata = {31'b0, SW};
end else begin
n_rdata = 0;
end
end
ここでは、回路の動作中を示す1ビット信号 SENDER_RUN と、物理的なトグルスイッチ入力 SW をユーザ回路から受け取った場合を考えています。アドレスをもとに読み出しデータ RDATA の次の値を定めています (138~144行)。ただし、このデータが実際に反映されるのは、読み出しが行われるときに限られます (127~129行)。
ここで紹介したインタフェース回路は、アドレスの幅を決め打ちしているなどの若干の制限はありますが、おおむね「IP の世界からこんにちは (4)」 で紹介した Xilinx 社によるテンプレートと同じように使えるかと思います。ただし、このコードは無保証での提供なので、このコードを用いたことによる損害等について、作者は責任を負いかねますので、ご容赦ください。
まとめ
今回は、Zynq の概要と内部接続、および AXI のインタフェースの種類について説明しました。今回の要点は以下のとおりです。
- AXI では、valid を ‘1’ にする際,ready が ‘1’ になるのを待つことは禁止されている。
- 読み出しデータ (RVALID) や書き込み結果 (BVALID) を送ろうとする場合、アドレスや書き込みデータの通信は完了していなければならない。
- リクエストを受ける側を設計するには、相手からのリクエスト (valid) が来てから受け付ける (ready) ようにすると、制御が単純で済む。
次回は、AXI-Stream の使い方について説明したあと、その利用例としてカラーパターンの動画像を生成する IP コアを作成し、これを使って HDMI 接続のディスプレイにカラーパターンを表示するシステムの動作を確認します。
愛知⼯業⼤学 藤枝直輝