Ready/Valid ハンドシェイクを使用してブロック RAM に AXI FIFO を作成する方法
AXI モジュールにインターフェイスするためのロジックを初めて作成しなければならなかったとき、AXI インターフェイスの特殊性に少し悩まされました。通常のビジー/有効、フル/有効、またはエンプティ/有効の制御信号の代わりに、AXI インターフェイスは「ready」と「valid」という名前の 2 つの制御信号を使用します。私の欲求不満はすぐに畏敬の念に変わりました.
AXI インターフェイスには、追加の制御信号を使用しないビルトイン フロー制御があります。ルールは簡単に理解できますが、FPGA に AXI インターフェイスを実装する際に考慮しなければならない落とし穴がいくつかあります。この記事では、VHDL で AXI FIFO を作成する方法について説明します。
AXI が 1 サイクル遅延の問題を解決
オーバーリードとオーバーライトの防止は、データ ストリーム インターフェイスを作成する際の一般的な問題です。問題は、2 つのクロック ロジック モジュールが通信する場合、各モジュールは 1 クロック サイクルの遅延で対応するモジュールからの出力しか読み取れないことです。
上の画像は、write enable/full を使用する FIFO に書き込むシーケンシャル モジュールのタイミング図を示しています。 シグナリング方式。インターフェイス モジュールは、wr_en をアサートすることによって FIFO にデータを書き込みます。 信号。 FIFO は full をアサートします 別のデータ要素用のスペースがない場合に信号を送信し、データ ソースに書き込みを停止するよう促します。
残念ながら、インターフェイス モジュールは、クロック ロジックのみを使用している限り、時間内に停止する方法がありません。 FIFO は full を発生させます クロックの立ち上がりエッジで正確にフラグを立てます。同時に、インターフェイス モジュールは次のデータ要素の書き込みを試みます。 full をサンプリングして反応することはできません 手遅れになる前に信号を送ってください。
1 つの解決策は、追加の almost_empty を含めることです。 これは、VHDL チュートリアルでリング バッファー FIFO を作成する方法で行いました。追加のシグナルは empty の前にあります これにより、インターフェイス モジュールに反応する時間が与えられます。
準備完了/有効な握手
AXI プロトコルは、各方向に 2 つの制御信号のみを使用してフロー制御を実装します。そのうちの 1 つは ready と呼ばれます。 そして他の valid . ready 信号は受信機によって制御され、論理 '1' この信号の値は、受信側が新しいデータ項目を受け入れる準備ができていることを意味します。 valid 一方、信号は送信者によって制御されます。送信者は valid を設定する必要があります '1' まで データ バス上に提示されたデータがサンプリングに有効な場合。
重要な部分は次のとおりです: ready の両方の場合にのみデータ転送が行われます と valid '1' です 同じクロック サイクルで。受信者は、データを受け入れる準備ができたときに通知し、送信者は、送信するものがあれば、単にデータをそこに置きます.転送は、送信者が送信する準備ができており、受信者が受信する準備ができている場合に、両者が同意したときに発生します。
上記の波形は、1 つのデータ項目のトランザクションの例を示しています。サンプリングはクロック ロジックの場合と同様に、立ち上がりクロック エッジで発生します。
実装
VHDL で AXI FIFO をインプリメントするには、さまざまな方法があります。シフト レジスタの場合もありますが、ブロック RAM に FIFO を作成する最も簡単な方法であるため、リング バッファー構造を使用します。変数とシグナルを使用して 1 つの巨大なプロセスですべてを作成することも、機能を複数のプロセスに分割することもできます。
この実装では、更新が必要なほとんどの信号に対して個別のプロセスを使用します。同期が必要なプロセスのみがクロックに敏感であり、他のプロセスは組み合わせロジックを使用します。
実体
エンティティ宣言には、入力ワードと出力ワードの幅の設定に使用される汎用ポートと、RAM にスペースを予約するスロットの数が含まれています。 FIFO の容量は、RAM の深さから 1 を引いた値に等しくなります。 FIFO がいっぱいか空かを区別するために、1 つのスロットは常に空に保たれます。
entity axi_fifo is
generic (
ram_width : natural;
ram_depth : natural
);
port (
clk : in std_logic;
rst : in std_logic;
-- AXI input interface
in_ready : out std_logic;
in_valid : in std_logic;
in_data : in std_logic_vector(ram_width - 1 downto 0);
-- AXI output interface
out_ready : in std_logic;
out_valid : out std_logic;
out_data : out std_logic_vector(ram_width - 1 downto 0)
);
end axi_fifo;
ポート宣言の最初の 2 つの信号は、クロックおよびリセット入力です。この実装は同期リセットを使用し、クロックの立ち上がりエッジに敏感です。
Ready/Valid 制御信号と汎用幅の入力データ信号を使用する AXI スタイルの入力インターフェイスがあります。最後に、入力と同様の信号を持つ AXI 出力インターフェイスがありますが、方向が逆になっています。入力および出力インターフェイスに属する信号には、in_ という接頭辞が付きます または out_ .
1 つの AXI FIFO からの出力を別の FIFO の入力に直接接続することができ、インターフェイスは完全に適合します。ただし、それらを積み重ねるよりも良い解決策は、 ram_depth を増やすことです より大きな FIFO が必要な場合はジェネリック。
シグナル宣言
VHDL ファイルの宣言領域の最初の 2 つのステートメントは、RAM タイプとその信号を宣言します。 RAM は、汎用入力から動的にサイズ調整されます。
-- The FIFO is full when the RAM contains ram_depth - 1 elements type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
コードの 2 番目のブロックは、新しい整数サブタイプとそれからの 4 つのシグナルを宣言します。 index_type RAM の深さを正確に表すサイズになっています。 head 信号は常に、次の書き込み操作で使用される RAM スロットを示します。 tail 信号は、次の読み取り操作でアクセスされるスロットを指します。 count の値 信号は常に FIFO に現在格納されている要素の数と等しく、count_p1 1 クロック サイクル遅れた同じ信号のコピーです。
-- Newest element at head, oldest element at tail subtype index_type is natural range ram_type'range; signal head : index_type; signal tail : index_type; signal count : index_type; signal count_p1 : index_type;
in_ready_i という名前の 2 つのシグナルが続きます。 と out_valid_i .これらはエンティティ出力 in_ready の単なるコピーです と out_valid . _i postfix は internal を意味します 、それは私のコーディング スタイルの一部です。
-- Internal versions of entity signals with mode "out" signal in_ready_i : std_logic; signal out_valid_i : std_logic;
最後に、同時の読み取りと書き込みを示すために使用されるシグナルを宣言します。その目的については、この記事の後半で説明します。
-- True the clock cycle after a simultaneous read and write signal read_while_write_p1 : std_logic;
サブプログラム
シグナルの後に、カスタムの index_type をインクリメントする関数を宣言します . next_index 関数は read を調べます と valid パラメータを使用して、進行中の読み取りまたは読み取り/書き込みトランザクションがあるかどうかを判断します。その場合、インデックスはインクリメントまたはラップされます。そうでない場合は、変更されていないインデックス値が返されます。
function next_index(
index : index_type;
ready : std_logic;
valid : std_logic) return index_type is
begin
if ready = '1' and valid = '1' then
if index = index_type'high then
return index_type'low;
else
return index + 1;
end if;
end if;
return index;
end function;
繰り返し入力する手間を省くために、head を更新するロジックを作成します。 と tail 2 つの同一のプロセスではなく、プロシージャ内のシグナル。 update_index 手順は、index_type の信号であるクロック信号とリセット信号を受け取ります 、 ready 信号、および valid 入力としてのシグナル。
procedure index_proc(
signal clk : in std_logic;
signal rst : in std_logic;
signal index : inout index_type;
signal ready : in std_logic;
signal valid : in std_logic) is
begin
if rising_edge(clk) then
if rst = '1' then
index <= index_type'low;
else
index <= next_index(index, ready, valid);
end if;
end if;
end procedure;
この完全同期プロセスは next_index を使用します index を更新する関数 モジュールがリセットされていないときに信号を送信します。リセットすると、index index_type の仕組みにより、信号は常に 0 である、表現可能な最小値に設定されます。 と ram_type 宣言されています。リセット値として 0 を使用することもできましたが、ハードコーディングをできるだけ避けるようにしています.
内部信号を出力にコピー
これら 2 つの同時実行ステートメントは、出力信号の内部バージョンを実際の出力にコピーします。 VHDL ではモード out でエンティティ シグナルを読み取ることができないため、内部コピーを操作する必要があります。 モジュールの内部。別の方法として、in_ready を宣言することもできます。 と out_valid モード inout で ですが、ほとんどの企業のコーディング標準では inout の使用が制限されています エンティティ シグナル。
in_ready <= in_ready_i; out_valid <= out_valid_i;
頭と尻尾を更新
index_proc についてはすでに説明しました head を更新するために使用される手順 と tail 信号。適切なシグナルをこのサブプログラムのパラメーターにマッピングすることにより、2 つの同一のプロセス (FIFO の入力を制御するためのプロセスと出力を制御するためのプロセス) に相当するものを取得します。
-- Update head index on write PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid); -- Update tail index on read PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);
head そして tail リセット ロジックによって同じ値に設定されると、FIFO は最初は空になります。これがこのリング バッファの仕組みです。両方が同じインデックスを指している場合、FIFO が空であることを意味します。
ブロック RAM を推測
ほとんどの FPGA アーキテクチャでは、ブロック RAM プリミティブは完全同期コンポーネントです。これは、合成ツールで VHDL コードからブロック RAM を推測する場合は、クロック プロセス内に読み出しポートと書き込みポートを配置する必要があることを意味します。また、ブロック RAM に関連付けられたリセット値がない場合もあります。
PROC_RAM : process(clk)
begin
if rising_edge(clk) then
ram(head) <= in_data;
out_data <= ram(next_index(tail, out_ready, out_valid_i));
end if;
end process;
読み取り可能はありません または書き込み可能 ここでは、AXI には遅すぎます。代わりに、head が指す RAM スロットに継続的に書き込みます。 索引。次に、書き込みトランザクションが発生したと判断したら、単純に head を進めます。 書き込まれた値をロックします。
同様に、out_data クロック サイクルごとに更新されます。 tail 読み取りが発生すると、ポインターは単に次のスロットに移動します。 next_index に注意してください 関数は、読み取りポートのアドレスを計算するために使用されます。読み取り後に RAM が十分に速く反応し、次の値の出力を開始するようにするために、これを行う必要があります。
FIFO 内の要素数をカウントする
RAM 内の要素数を数えるには、単純に head を引くだけです。 tail から . head の場合 RAM のスロットの合計数だけオフセットする必要があります。 ram_depth を通じてこの情報にアクセスできます 汎用入力からの定数。
PROC_COUNT : process(head, tail)
begin
if head < tail then
count <= head - tail + ram_depth;
else
count <= head - tail;
end if;
end process;
count の以前の値も追跡する必要があります。 信号。以下のプロセスは、1 クロック サイクル遅れたバージョンを作成します。 _p1 postfix は、これを示すための命名規則です。
PROC_COUNT_P1 : process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
count_p1 <= 0;
else
count_p1 <= count;
end if;
end if;
end process;
ready を更新します 出力
in_ready 信号は '1' でなければなりません このモジュールが別のデータ項目を受け入れる準備ができたとき。これは、FIFO がいっぱいでない限り当てはまるはずであり、まさにこのプロセスのロジックが示していることです。
PROC_IN_READY : process(count)
begin
if count < ram_depth - 1 then
in_ready_i <= '1';
else
in_ready_i <= '0';
end if;
end process;
読み取りと書き込みの同時検出
次のセクションで説明する特殊なケースのため、同時の読み取り操作と書き込み操作を識別できる必要があります。同じクロック サイクル中に有効な読み取りおよび書き込みトランザクションが発生するたびに、このプロセスは read_while_write_p1 を設定します。 '1' への合図 次のクロック サイクルで。
PROC_READ_WHILE_WRITE_P1: process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
read_while_write_p1 <= '0';
else
read_while_write_p1 <= '0';
if in_ready_i = '1' and in_valid = '1' and
out_ready = '1' and out_valid_i = '1' then
read_while_write_p1 <= '1';
end if;
end if;
end if;
end process;
有効を更新 出力
out_valid 信号は、データが out_data で提示されたことを下流のモジュールに示します 有効で、いつでもサンプリングできます。 out_data 信号は RAM 出力から直接来ます。 out_valid の実装 ブロック RAM の入力と出力の間に余分なクロック サイクル遅延があるため、この信号は少しトリッキーです。
ロジックは組み合わせプロセスで実装されるため、変化する入力信号に遅延なく反応できます。プロセスの最初の行は、out_valid を設定するデフォルト値です。 '1' への合図 .後続の 2 つの If ステートメントのいずれもトリガーされない場合、これが一般的な値になります。
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1)
begin
out_valid_i <= '1';
-- If the RAM is empty or was empty in the prev cycle
if count = 0 or count_p1 = 0 then
out_valid_i <= '0';
end if;
-- If simultaneous read and write when almost empty
if count = 1 and read_while_write_p1 = '1' then
out_valid_i <= '0';
end if;
end process;
最初の If ステートメントは、FIFO が空であるか、前のクロック サイクルで空であったかをチェックします。明らかに、FIFO に 0 要素がある場合、FIFO は空ですが、前のクロック サイクルでの FIFO のフィル レベルも調べる必要があります。
以下の波形を考えてみましょう。 count で示されるように、FIFO は最初は空です。 信号は 0 です .次に、3 番目のクロック サイクルで書き込みが発生します。 RAM スロット 0 は次のクロック サイクルで更新されますが、out_data にデータが表示されるまでにさらに 1 サイクルかかります。 出力。 or count_p1 = 0 の目的 ステートメントは out_valid であることを確認することです '0' のまま 値が RAM を介して伝播している間 (赤丸で囲んだ部分)。
最後の If ステートメントは、別のまれなケースを防ぎます。現在および以前の FIFO フィル レベルをチェックすることにより、空書き込みの特殊なケースを処理する方法について説明しました。しかし、count のときに同時読み取りと書き込みを実行するとどうなりますか? 既に 1 です ?
以下の波形はそのような状況を示しています。最初に、FIFO には 1 つのデータ項目 D0 が存在します。しばらくそこにあったので、両方とも count と count_p1 0 です .次に、3 番目のクロック サイクルで読み取りと書き込みが同時に行われます。 1 つのアイテムが FIFO を離れ、新しいアイテムが FIFO に入る場合、カウンターは変更されません。
読み取りおよび書き込みの時点で、RAM には出力可能な次の値がありません。これは、塗りつぶしレベルが 1 よりも高い場合に発生する可能性があるためです。入力値が出力に現れるまで、2 クロック サイクル待つ必要があります。追加情報がなければ、このコーナー ケースと out_valid の値を検出することは不可能です。 次のクロック サイクル (赤一色でマーク) では、誤って '1' に設定されます。 .
read_while_write_p1 が必要なのはそのためです。 信号。同時読み取りと書き込みがあったことを検出し、 out_valid を設定することでこれを考慮することができます '0' へ
Vivado での合成
ザイリンクス Vivado でスタンドアロン モジュールとしてデザインをインプリメントするには、最初にジェネリック入力に値を指定する必要があります。これは、Settings を使用して Vivado で実現できます。 → 一般 → ジェネリック/パラメータ 下の画像に示すように、メニュー。
ジェネリック値は、ターゲット デバイスであるザイリンクス Zynq アーキテクチャの RAMB36E1 プリミティブと一致するように選択されています。実装後のリソースの使用状況を下の図に示します。 AXI FIFO は 1 つのブロック RAM と少数の LUT およびフリップフロップを使用します。
AXI は準備完了/有効以上のものです
AXI は Advanced eXtensible Interface の略で、ARM の Advanced Microcontroller Bus Architecture (AMBA) 標準の一部です。 AXI 標準は、読み取り/有効なハンドシェイク以上のものです。 AXI について詳しく知りたい場合は、以下のリソースをさらに読むことをお勧めします。
- ウィキペディア:AXI
- ARM AXI の紹介
- ザイリンクス AXI の概要
- AXI4 仕様
VHDL