はじめに
FPGAの授業の補助資料として
授業中には隠蔽して説明していたCPUとFPGA上の回路間で連携する仕組みと実装方法を説明します。
この記事ではZynq-7000シリーズ環境でのPS(CPU)とPL(FPGA)間でデータのやり取りを行う仕組みを解説し、実際にIPを作成します。
シミュレーション及びCPUプログラムの作成と実機での動作確認は次回以降の記事で説明します。
#環境
Vivado 2018.3 HL Web Pack Edition
AXI4-Lite
AXI(Advanced eXtensible Interface)はARM社が制定したチップ内の回路同士(例えばCPUとIP間)を接続するバスのプロトコルです。
VivadoではIP間の通信インターフェースとしてAXIが用いられています。
AXI-LiteはAXIを簡略化したもので、1トランザクションにつき1つのデータを転送できるものです。
そのため、転送量が少ない制御レジスターなどに向いています。
スレーブで書き込み動作ならAWチャンネルにアドレスが届き、Wチャンネルにデータが届くのでBチャンネルで応答する、読み出し動作ならARチャンネルにアドレスが届き、Rチャンネルに1個データを返せばOKというものです。
なお、Xilinx環境ではデータ幅は32bitのみがサポートされているようです。
Zynq環境でのCPUとIPの通信の仕組み
ZynqではPSとPL間での通信用にAXIが用意されています。7000シリーズではPSがマスターとなるAXIが2系統用意されており、CPUからIPを制御する場合はこちらを用います。
PS内部では図のようにメモリとPL(AXI Master経由)がインターコネクトを介し接続されており、表のようにアドレスが割り当てられています。
そのため0x40000000番地以降のアドレスにCPUからアクセスしに行くとAXIのトランザクションが発行され、インターコネクトを経由しPL側のIPへと送られます。
CPUで書き込んだ場合はAWチャンネルで書き込み先のアドレスが送られ、Wチャンネルで書き込んだ値を送り、CPUから読み込む場合はARチャンネルでアドレスを送り、Rチャンネルでデータを受け取ります。
なお、このアドレス空間のどこにIPのアドレスを割り当てるかはブロック図を作成する際に割当を行います。(次回以降の記事で説明)
AXI-LiteのIPを作成
Vivadoでは、ありがたいことにAXI-Liteを用いたレジスター回路の雛形を生成する機能があるのでここではそれを用います。
AXIのステートマシンなどが自動生成されるので手でコードを書く必要がなく大変楽です。
Vivado上でAXIのIPを作成
Vivadoを起動し、適当なプロジェクトを開いたあと、「Tools」→「Create and Package New IP」を選択します。
※あとで、シミュレーションと実機ようにプロジェクトを作成するため、ここでプロジェクトを作成したあとにIPを作成しても構いません。
ダイアログ最初の画面は内容を確認して「Next」を選択してください。
次の画面で「Create a new AXI1 peripheral」を選択し、「Next」をクリックしてください。
名前や保存場所などを指定し、「Next」を押します。
Add Interface画面でIPのインターフェースを指定します。
今回はAXI-Liteを用いた制御レジスタを作成するので「Interface Type」は「Lite」に、「Interface Mode」は「Slave」にします。
レジスターの数などはお好みで変更し、「Next」を押します。
最後に確認画面が表示されます。このままコードの確認を行いたいので「Edit IP」にチェックを入れ、「Finish」を押します。
自動生成されたAXI-LiteのIPを確認
Edit IPを選択してFinishを押すと編集用のプロジェクトが表示されます。左上の「Sources」内の「Hierarchy」を開くととが生成されていると思います。(IPの保存先/IP名_1.0/hdl以下に保存されています。)
この内AXIインターフェース名.vのほうがAXI-Liteの処理を含む自動生成されたファイルです。
中身の概略は以下の感じです。
module IP名_v1_0_AXIインターフェース名();
// AXI関係の信号
・・・略・・・
// ユーザーレジスタ(指定した数生成)
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
・・・以下略・・・
// AXI-Liteの信号生成
・・・略・・・
// 書き込み処理
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
// ユーザーレジスタ初期化
slv_reg0 <= 0;
slv_reg1 <= 0;
・・・以下略・・・
end
else begin
if (slv_reg_wren)
begin
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
// アドレスに応じてユーザーレジスターに値をセットする
3'h0:
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
3'h1:
・・・以下略・・・
endcase
end
end
end
// AXI-Liteの信号生成
・・・略・・・
// 読み込み処理
always @(*)
begin
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
3'h0 : reg_data_out <= slv_reg0;
3'h1 : reg_data_out <= slv_reg1;
・・・以下略・・・
default : reg_data_out <= 0;
endcase
end
endmodule
このようなコードが生成されていると思います。
もし、レジスターをPLから更新しなければこの処理をそのまま利用できますが、今回はCPUから値を指定すると同時に回路からも更新するようなものを作成します。
なお、AXIのIPで扱うアドレスは、ワード単位で扱うため下位2bit(ADDR_LSB)を切り捨てて用います。
また、アドレスは下位C_S_AXI_ADDR_WIDTH[bit]をIP用空間として用い、CPUからアクセスするときは適当なベースアドレス(ブロック図で設定)にオフセットアドレスを足したものでアクセスします。(IP側では下位のオフセットアドレスだけを見ます。)
乱数を生成するIPを作成
今回はxor shiftで乱数を生成するIPを題材にします。
シード値yと乱数生成開始・ストップと生成結果を取得する3つのレジスタを用意します。
レジスタマップは以下のようにします。
enableが1のときは、1クロックごとに乱数yを更新し、シード値が更新されたときはそのシード値を用いて乱数を生成するという仕様にします。
また、現在の乱数値をCPUからセットすることは想定しないので乱数のみReadOnly、ほかはReadWriteとします。
以下に自動生成されたものから読み書き処理だけ改造したものを示します。(全体のコードは末尾のサンプルコードの部分にリンクを張っておきます。)
// ここからレジスタの更新処理などを記述する
// CPUとやり取りするレジスタ
reg enable;
reg [C_S_AXI_DATA_WIDTH-1:0] seed;
reg [C_S_AXI_DATA_WIDTH-1:0] y;
// AXI-Lite書き込み処理
// ==========================================================================
// アドレス(下位2ビットは無視したもの)
wire [OPT_MEM_ADDR_BITS:0] wr_reg_addr = axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB];
// enableレジスタのセット
always @(posedge S_AXI_ACLK) begin
if (S_AXI_ARESETN == 1'b0) enable <= 0;
// 書き込み && アドレス(下位2ビットは無視したもの)が0 && バイトイネーブル(下位1バイト)が有効
else if (slv_reg_wren && wr_reg_addr == 2'h0 && S_AXI_WSTRB[0]) begin
enable <= S_AXI_WDATA[0];
end
end
wire wrseed = slv_reg_wren && wr_reg_addr == 2'h1;
// シード値のセット
always @(posedge S_AXI_ACLK) begin
if (S_AXI_ARESETN == 1'b0) seed <= 0;
// 書き込み && アドレス(下位2ビットは無視したもの)が1
else if (slv_reg_wren && wr_reg_addr == 2'h1) begin
// 各バイトでバイトイネーブルが有効か判定して更新する
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) begin
if ( S_AXI_WSTRB[byte_index] == 1 ) begin
seed[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
end
end
end
// 内部の更新
// ==========================================================================
// シード値が更新されたことを示すフラグ
reg seed_updated;
always @(posedge S_AXI_ACLK) begin
if (S_AXI_ARESETN == 1'b0) seed_updated <= 0;
// 書き込み && アドレス(下位2ビットは無視したもの)が1
else if (slv_reg_wren && wr_reg_addr == 2'h1) begin
seed_updated <= 1;
end else seed_updated <= 0;
end
// 乱数値の更新(AXIは関係なし)
// シード値が指定された場合のみシードの次の値をセット、それ以外は毎クロック更新する
wire [C_S_AXI_DATA_WIDTH-1:0] y_1 = y ^ (y << 13);
wire [C_S_AXI_DATA_WIDTH-1:0] y_2 = y_1 ^ (y_1 >> 17);
wire [C_S_AXI_DATA_WIDTH-1:0] y_init_1 = seed ^ (seed << 13);
wire [C_S_AXI_DATA_WIDTH-1:0] y_init_2 = y_init_1 ^ (y_init_1 >> 17);
always @(posedge S_AXI_ACLK) begin
if (S_AXI_ARESETN == 1'b0) y <= 0;
else if (seed_updated) begin
y <= y_init_2 ^ (y_init_2 << 5);
end else if (enable) begin
y <= y_2 ^ (y_2 << 5);
end
end
// AXI-Lite読み出し処理
// ==========================================================================
always @(*) begin
// アドレス(下位2ビットは無視したもの)で判定
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
3'h0 : reg_data_out <= {31'd0, enable};
3'h1 : reg_data_out <= seed;
3'h2 : reg_data_out <= y;
default : reg_data_out <= 0;
endcase
end
これでIPは完成です。IPの編集プロジェクトに戻り、「PackageIP」タブを開き、「Review and Package」を選択し、「Re-Package IP」をクリックしてIPを更新します。
更新後はIP編集用プロジェクトを閉じます。
次回予告
ここまででAXI-Liteを用いたIPの作成ができました。
このままでは作った回路がちゃんと動くかわからないため、次回AXIの検証IPを用いてシミュレーションを行います。
サンプルコード
準備中です。最終記事まで公開後にリポジトリを作成します。
参考文献
-
UG585 Zynq-7000 All Programmable SoC テクニカル リファレンス マニュアル
§4システムアドレス, §5インターコネクト