前回の続き
目的
- AXI4-Streamを理解する
対象
- FPGAを勉強し始めた初心者の方
AXI-Streamで設計してみる
初心者の方にとっては一見難しそうに見えるかと思いますが、一つ一つの機能を理解すれば大したことはありません。今回、AXI-Streamの理解のためにStateMachine、FIFOを導入していますので、それぞれの理解を重点的に行います。
ブロック図
設計のポイント
AXI4-Streamの設計概要
- A/Dコンバータ(ADC)からデータを収集し、各ADC Clock のedgeでサンプリングします。
- サンプリングされたデータを非同期FIFOにロードします。
- AXI4-Stream Masterは、この非同期FIFOからデータを取得し、AXI4ストリームバスを介してスレーブに送信します。
フロー制御のためのステートマシン
-
ステートマシンを使用してフロー制御を行います。
-
FIFOが空でないことを示すフラグを使用して、データが送信準備ができていることを示します。
-
スレーブのTREADY信号を使用して、スレーブがデータを受信する準備ができていることを示します。
AXI-StreamにおけるState Machineを理解する
状態遷移図
-
INIT
: 初期状態。リセット信号がアクティブであれば、この状態になります。 -
VALID_DATA
: 有効データ状態。FIFOが空でなく、AXIストリームが準備完了である場合、この状態になります。 -
STALL_DATA
: データ停滞状態。FIFOが空で、AXIストリームが準備完了である場合、この状態になります。 -
SLAVE_STALL
: スレーブ停滞状態。FIFOが空でなく、AXIストリームが準備完了でない場合、この状態になります。
Verilog HDL のコード
State Machine
FIFOのデータをステートマシンで処理をします。
`timescale 1ns / 1ps
module axis_master_adc
(
input wire adc_clk, // ADCクロック入力
input wire [13: 0] adc_data, // ADCデータ入力
input wire m_axis_aclk, // AXIストリームクロック入力
input wire m_axis_aresetn, // AXIストリームリセット入力(アクティブロー)
output wire [15: 0] m_axis_tdata, // AXIストリームデータ出力
output wire [1 : 0] m_axis_tstrb, // AXIストリームストローブ出力
output wire [1 : 0] m_axis_tkeep, // AXIストリームキープ出力
output reg m_axis_tvalid, // AXIストリームデータ有効信号出力
input wire m_axis_tready, // AXIストリーム準備完了信号入力
output reg m_axis_tlast // AXIストリーム最後のデータ信号出力
);
// FIFO関連の信号宣言
wire fifo_empty; // FIFO空信号
wire fifo_afull; // FIFOほぼ満杯信号
reg rd_en; // FIFO読み出しイネーブル信号
reg wr_en; // FIFO書き込みイネーブル信号
reg [5:0] data_count; // データカウンタ
wire [13:0] rd_data; // FIFOから読み出されたデータ
reg wr_en_sync; // 書き込みイネーブル同期信号
reg m_axis_aresetn_reg; // リセット信号のレジスタ
// ステートマシンの状態宣言
localparam INIT = 0,
VALID_DATA = 1,
STALL_DATA = 2,
SLAVE_STALL = 3;
reg [1:0] state, next_state = 2'b00;
// 非同期FIFOのインスタンス
async_fifo fifo_inst
(
.wr_clk(adc_clk),
.wr_en(wr_en),
.wr_data(adc_data),
.rd_clk(m_axis_aclk),
.rd_en(rd_en),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.fifo_afull(fifo_afull)
);
// AXIストリーム出力信号の割り当て
assign m_axis_tkeep = 2'b11;
assign m_axis_tstrb = 2'b11;
assign m_axis_tdata[13:0] = rd_data;
assign m_axis_tdata[15:14] = 2'b00;
// ADCクロックの立ち上がりエッジで書き込みイネーブル信号を同期化
always @(posedge adc_clk)
begin
if (state == INIT)
wr_en_sync <= 1'b0;
else
wr_en_sync <= 1'b1;
wr_en <= wr_en_sync;
end
// ステートマシンの次状態遷移ロジック
always @(state, fifo_empty, m_axis_tready, data_count) begin
next_state = state;
case (state)
INIT: begin
if ((m_axis_aresetn == 0) || (m_axis_aresetn_reg == 0))
next_state = INIT;
else if (fifo_empty == 1)
next_state = STALL_DATA;
else if (m_axis_tready == 0)
next_state = SLAVE_STALL;
else if (fifo_empty == 0)
next_state = VALID_DATA;
end
VALID_DATA: begin
if (m_axis_tready == 0)
next_state = SLAVE_STALL;
else if (fifo_empty == 1)
next_state = STALL_DATA;
end
STALL_DATA: begin
if ((m_axis_tready == 0) && (fifo_empty == 0))
next_state = SLAVE_STALL;
else if (fifo_empty == 0)
next_state = VALID_DATA;
end
SLAVE_STALL: begin
if ((m_axis_tready == 1) && (fifo_empty == 0))
next_state = VALID_DATA;
else if (m_axis_tready == 1)
next_state = STALL_DATA;
end
default: next_state = INIT;
endcase
end
// AXIストリームクロックの立ち上がりエッジで状態を更新
always @(posedge m_axis_aclk)
begin
m_axis_aresetn_reg <= m_axis_aresetn;
if (m_axis_aresetn == 0)
state <= INIT;
else
state <= next_state;
end
// データカウンタと最後のデータ信号の制御
always @(posedge m_axis_aclk)
begin
if (next_state == INIT) begin
data_count <= 0;
m_axis_tlast <= 0;
end
else if ((next_state == VALID_DATA) && (data_count == 63)) begin
data_count <= 0;
m_axis_tlast <= 1;
end
else if (next_state == VALID_DATA) begin
data_count <= data_count + 1;
m_axis_tlast <= 0;
end
end
// ステートマシンの出力ロジック
always @(next_state) begin
case (next_state)
INIT: begin
m_axis_tvalid = 0;
rd_en = 0;
end
VALID_DATA: begin
m_axis_tvalid = 1;
rd_en = 1;
end
STALL_DATA: begin
m_axis_tvalid = 0;
rd_en = 0;
end
SLAVE_STALL: begin
m_axis_tvalid = 1;
rd_en = 0;
end
endcase
end
endmodule
非同期FIFOを理解する
非同期FIFO (First In, First Out) は、異なるクロックドメイン間でデータを転送するために使用されるデータバッファリングの一種です。
非同期FIFOの基本概念
FIFOとは
FIFOは「先入れ先出し」の略で、最初に入力されたデータが最初に出力されるデータ構造です。
非同期FIFOの特徴
- 非同期: データの書き込みと読み出しが異なるクロックで行われます。
- クロックドメインの分離: 異なるクロックドメイン間でデータ転送を行うための工夫が必要です。
ブロック図
この非同期FIFOモジュールは、異なるクロックドメイン間でデータをバッファリングするためのものです。以下に、モジュールの各部分について説明します。
書き込み操作
- 書き込みクロックの立ち上がりエッジで、
FIFO Full
が0でWirte Enable
が1の場合に書き込みアドレスがインクリメントされ、データがFIFOメモリに書き込まれます。 - 同時に、読み出しアドレスが同期化されます。
読み出し操作
- 読み出しクロックの立ち上がりエッジで、
FIFO Empty
が0でRead Enable
が1の場合に読み出しアドレスがインクリメントされます。 - 同時に、書き込みアドレスが同期化されます。
アドレス変換
- 書き込みと読み出しのアドレスはグレイコードに変換され、同期用レジスタを介して同期されます。
- 同期されたアドレスはバイナリに戻されます。
空きスペースと状態の計算
- FIFOの空きスペースは書き込みアドレスと同期された読み出しアドレスの差で計算されます。
- FIFOが満杯かほぼ満杯、または空であるかを示す信号が生成されます。
Verilog HDL のコード
FIFO
ブロック図を基に非同期FIFO(First In, First Out)を設計します。
`timescale 1ns / 1ps
// 非同期FIFO(First In, First Out)モジュール
module async_fifo
(
input wire wr_clk, // 書き込みクロック
input wire wr_en, // 書き込みイネーブル信号
input wire [13: 0] wr_data, // 書き込みデータ
input wire rd_clk, // 読み出しクロック
input wire rd_en, // 読み出しイネーブル信号
output wire [13: 0] rd_data, // 読み出しデータ
output wire fifo_empty, // FIFO空信号
output wire fifo_afull // FIFOほぼ満杯信号
);
// 書き込みと読み出しのアドレス
reg [3:0] wr_addr = 4'b0000;
reg [3:0] rd_addr = 4'b0000;
// グレイコード変換されたアドレスとその同期用レジスタ
wire [3:0] wr_addr_grey;
reg [3:0] wr_addr_grey_sync;
reg [3:0] wr_addr_grey_sync2;
wire [3:0] wr_addr_sync;
wire [3:0] rd_addr_grey;
reg [3:0] rd_addr_grey_sync;
reg [3:0] rd_addr_grey_sync2;
wire [3:0] rd_addr_sync;
// FIFOメモリ空間
reg [13:0] fifo_memory [0:15];
// FIFOの空きスペース、満杯、空の状態
wire [3:0] fifo_space;
wire full;
wire empty;
// 書き込みアドレスをグレイコードに変換
assign wr_addr_grey = {wr_addr[3],wr_addr[3] ^ wr_addr[2],wr_addr[2] ^ wr_addr[1],wr_addr[1] ^ wr_addr[0]};
// 同期化された書き込みアドレスをバイナリに戻す
assign wr_addr_sync = {wr_addr_grey_sync2[3],wr_addr_grey_sync2[3] ^ wr_addr_grey_sync2[2],
wr_addr_grey_sync2[3] ^ wr_addr_grey_sync2[2] ^ wr_addr_grey_sync2[1],
wr_addr_grey_sync2[3] ^ wr_addr_grey_sync2[2] ^ wr_addr_grey_sync2[1] ^ wr_addr_grey_sync2[0]};
// 読み出しアドレスをグレイコードに変換
assign rd_addr_grey = {rd_addr[3],rd_addr[3] ^ rd_addr[2],rd_addr[2] ^ rd_addr[1],rd_addr[1] ^ rd_addr[0]};
// 同期化された読み出しアドレスをバイナリに戻す
assign rd_addr_sync = {rd_addr_grey_sync2[3],rd_addr_grey_sync2[3] ^ rd_addr_grey_sync2[2],
rd_addr_grey_sync2[3] ^ rd_addr_grey_sync2[2] ^ rd_addr_grey_sync2[1],
rd_addr_grey_sync2[3] ^ rd_addr_grey_sync2[2] ^ rd_addr_grey_sync2[1] ^ rd_addr_grey_sync2[0]};
// FIFOの空きスペースを計算
assign fifo_space = wr_addr - rd_addr_sync;
assign full = (fifo_space > 14) ? 1:0;
assign fifo_afull = (fifo_space > 13) ? 1:0;
assign empty = ((rd_addr == wr_addr_sync) ? 1:0);
assign fifo_empty = empty;
// 書き込みクロックの立ち上がりエッジで書き込みアドレスをインクリメント
always @(posedge wr_clk)
begin
if ((full == 0) && (wr_en == 1))
wr_addr <= wr_addr + 1;
end
// 書き込みクロックの立ち上がりエッジで読み出しアドレスを同期化
always @(posedge wr_clk)
begin
rd_addr_grey_sync <= rd_addr_grey;
rd_addr_grey_sync2 <= rd_addr_grey_sync;
end
// 書き込みクロックの立ち上がりエッジでデータをFIFOメモリに書き込む
always @(posedge wr_clk)
begin
if ((full == 0) && (wr_en == 1))
fifo_memory[wr_addr] <= wr_data;
end
// 読み出しデータの割り当て
assign rd_data = fifo_memory[rd_addr];
// 読み出しクロックの立ち上がりエッジで書き込みアドレスを同期化
always @(posedge rd_clk)
begin
wr_addr_grey_sync <= wr_addr_grey;
wr_addr_grey_sync2 <= wr_addr_grey_sync;
end
// 読み出しクロックの立ち上がりエッジで読み出しアドレスをインクリメント
always @(posedge rd_clk)
begin
if ((rd_en == 1) && (empty == 0))
rd_addr <= rd_addr + 1;
end
endmodule
Verilog HDLを用いたサンプルコード
ADC
簡易的なADCのモデルを作成します。
`timescale 1ns / 1ps
module adc_model
(
input wire adc_clk,
output reg [13: 0] adc_data
);
initial begin
adc_data = 14'b0;
end
always@(posedge adc_clk) begin
adc_data <= adc_data + 1;
end
endmodule
テストベンチ
`timescale 1ns / 1ps
// テストベンチモジュール
module t_axis_master_adc;
reg adc_clk; // ADCクロック信号
wire [13:0] adc_data; // ADCデータ信号
reg m_axis_aclk; // AXIストリームクロック信号
reg m_axis_aresetn; // AXIストリームリセット信号(アクティブロー)
wire [15 : 0] m_axis_tdata; // AXIストリームデータ信号
wire [1 : 0] m_axis_tstrb; // AXIストリームストローブ信号
wire [1 : 0] m_axis_tkeep; // AXIストリームキープ信号
wire m_axis_tvalid; // AXIストリームデータ有効信号
wire m_axis_tready; // AXIストリームレディ信号
wire m_axis_tlast; // AXIストリーム最後のデータ信号
reg [5:0] lfsr; // 擬似乱数生成器
reg [15:0] data_check; // データチェック用
reg [15:0] rem_value; // リマインダー値
reg error; // エラーフラグ
reg tlast_error; // 最後のデータエラーフラグ
integer i; // ループカウンタ
// 初期化
initial
begin
adc_clk = 1'b0; // ADCクロック初期化
m_axis_aclk = 1'b0; // AXIストリームクロック初期化
m_axis_aresetn = 1'b0; // リセット初期化
lfsr = 6'b100000; // LFSR初期化
for (i = 0; i < 5; i = i + 1) @(posedge m_axis_tlast); // tlastの立ち上がりエッジを5回待つ
$finish; // シミュレーション終了
end
// クロック生成
always #20 m_axis_aclk = ~m_axis_aclk; // 20ns周期でAXIクロックを反転
always #50 adc_clk = ~adc_clk; // 50ns周期でADCクロックを反転
// DUT(デバイスアンダーテスト)のインスタンス化
axis_master_adc uut (
.adc_clk(adc_clk),
.adc_data(adc_data),
.m_axis_aclk(m_axis_aclk),
.m_axis_aresetn(m_axis_aresetn),
.m_axis_tdata(m_axis_tdata),
.m_axis_tstrb(m_axis_tstrb),
.m_axis_tkeep(m_axis_tkeep),
.m_axis_tvalid(m_axis_tvalid),
.m_axis_tlast(m_axis_tlast),
.m_axis_tready(m_axis_tready)
);
// ADCモデルのインスタンス化
adc_model adc_model_inst (
.adc_clk(adc_clk),
.adc_data(adc_data)
);
// リセットシーケンス
initial
begin
m_axis_aresetn = 1'b0; // リセットアサート
#200 m_axis_aresetn = 1'b1; // 200ns後にリセットデアサート
end
// LFSRの生成
always @(posedge m_axis_aclk) begin
lfsr[0] <= lfsr[5] ^ lfsr[4] ^ 1'b1;
lfsr[5:1] <= lfsr[4:0];
end
assign m_axis_tready = lfsr[5]; // tready信号をLFSRの最上位ビットに設定
// データチェック
initial begin
while (!((m_axis_tvalid === 1) && (lfsr[5] == 1))) @(posedge m_axis_aclk); // tvalidとLFSRが1になるのを待つ
data_check = m_axis_tdata;
rem_value = m_axis_tdata % 64;
while (1) begin
@(posedge m_axis_aclk);
if ((m_axis_tvalid == 1) && (lfsr[5] == 1))
data_check = data_check + 1;
if ((m_axis_tdata != data_check) && (m_axis_tvalid == 1) && (lfsr[5] == 1))
error = 1;
else
error = 0;
if (((m_axis_tdata % 64) != rem_value) && (m_axis_tvalid == 1) && (m_axis_tlast == 1) && (lfsr[5] == 1))
tlast_error = 1;
else
tlast_error = 0;
end
end
endmodule
波形図
- axi_master_adc.v
- async_fifo.v
- adc_model.v
- t_axi_master_adc.v
上記の4つのモジュールを用いてシミュレーションを行います。
無事に波形が観測できました。