公開日:2023/10/07
27th Season
#27-6 FIFO回路の設計
Cmod A7ボード(FPGA)を使って設計しているオリジナル16ビットCPUコアMoscoviumにて、「ちょっと、FIFOマクロが無駄に大きいよな〜」と、思ってしまいました。FIFO回路くらい、自前で設計して持っておく方が何かと便利なので、今回は汎用のFIFO回路を設計して行こうと思います。
FIFOマクロの特徴
Vivadoが提供しているFIFOマクロには、以下の様な特徴があります。
- 便利
- FIFOマクロとして提供されている。
- 適切なインターフェイスを取ってインスタンスするだけで、容易に使用可能。
- RAMマクロを消費
- VivadoのRAMマクロは、18Kビット(2Kバイト)または36Kビット(4Kバイト)なので、サイズが大きすぎる。
- 仮に小さく64バイトに設定しても、結局18KビットRAMマクロを1個占有使用してしまう。
- RAMマクロの無駄使い。
Moscoviumマイコンでは、UART8N1ユニットの受信バッファなどにFIFOマクロを使用しています。簡単に使えて便利なのですが、精々数100k[baud]程のUARTに1KバイトものFIFOバッファは冗長が過ぎます。本来なら数10バイトあれば十分です。
数10バイトで充足するFIFOバッファの為に、貴重なRAMマクロを1個占有使用してしまうのは勿体ないです。RTLを使って小さなFIFOバッファを作成し、この無駄を省く事が今回のテーマです。
FIFO回路の目標仕様と課題
以下を目標仕様として設計したいと思います。
目標仕様:
- UART8N1ユニットなど低速な通信ユニット用として、小容量のFIFO回路とする。
- 非同期クロック間のデータ通信に使える様、読み出し側/書き込み側で非同期クロックが使える。
- データのビット幅や段数については、コンパイルオプションとして実装する。
- データビット幅: 任意のビット幅を指定可能。例)8ビット、12ビットなど。
- 段数: 2冪数とする。例)64段、128段、256段など。
- ※必要なメモリ容量は、データビット幅×段数[ビット]。
- 通常のエンプティ(空)、フル(満)フラグに加え、先行エンプティ、先行フルフラグを出力する。
- VivadoのFIFOマクロから置き換えが容易な様に、同等のインターフェイスとする。
ソフトウェアでは、以下の記事でリングバッファを使ってFIFOを作成しました。
「#13-8 DUEのWAVファイル再生を向上する」
正直ソフトウェアとしては、それほど難しくも無いプログラムです。しかし、これをハードウェアで実装するのは、相当に難易度が高いのです。
しかも、読み出し側と書き込み側のクロックを非同期で動かすとなると、それはもう、職人技の範疇です。しっかりと戦略を練って進めた方が良いでしょう。
FIFO回路の設計
まずは、ブロック図を書きながら、イメージを整理してみました。
上図を使って、以下解説を続けます。
- fwrcブロック
- 書き込み要求信号、書き込みデータバスなどを受け、FIFOにデータを書き込む処理を制御する。
- 書き込みクロックclk_wrに同期して動作。
- 分散RAM(LUT RAM)への書き込みポインタptr_wrを持っていて、書き込む度にポインタを+1インクリメントする。
- frdcブロック
- 読み出し要求信号を受け、FIFOから読み出したデータを出力する処理を制御する。
- 読み出しクロックclk_rdに同期して動作。
- 分散RAM(LUT RAM)からの読み出しポインタptr_rdを持っていて、読み出す度にポインタを+1インクリメントする。
- fsyncブロック
- 互いに非同期なクロックで動作する書き込み制御(fwrc)、読み出し制御(frdc)間の同期化処理を実行。
- fwrcブロック、frdcブロック共、互いのポインタ値ptr_wr/ptr_rdを参照して、エンプティ/フル等のフラグを出力する必要があるため、同期化が必要。
- クロック(clk_wr/clk_rd)が非同期設定(コンパイルオプション)の場合、同期化処理(ハンドシェイク)を実行する。
- クロック(clk_wr/clk_rd)が同期設定の場合、同期化する必要が無いので、信号を通過させる。
- lramブロック
- 分散RAM(LUT RAM)を使って実装する。
- 一般に「2ポートRAM」と呼ばれる、書き込みポート×1本と読み出しポート×1本のRAM。
- ※所謂「デュアルポートRAM」は、読み/書き可能なポートが×2本備わっている物を指す。「シングルポートRAM」は、読み/書き可能なポートが×1本なので、「2ポートRAM」と似ているけど、メモリセルの構造的には全く異なる。
- VivadoのFIFOマクロの場合、RAMマクロに置き換わる部分。
- 数10〜数100バイト程度の小さなFIFO回路の場合、RAMマクロを無駄遣いするよりも、論理ゲート数(LUT)で決着してしまった方が、かえって無駄を省く事が可能。
- RAMマクロのメモリセルは、インバータループ回路を使って1ビットデータを記憶し、トランスファゲート+アナログ回路(差動増幅器)で読み/書きしている。
- 分散RAMのメモリセルは、NORループまたは、NANDループ、即ちRSラッチ回路(普通にデジタル回路)を使って1ビットデータを記憶する。
インターフェイス
以下の通り、Vivadoが提供するFIFOマクロの置き換えで使用可能なインターフェイスとします。
【入力信号】:
- clk_wr,clk_rd: 動作クロック
- 異クロック間の非同期タイミングを吸収可能な、マルチクロックに対応。
- 同期クロックとして使用する場合は、clk_wr/clk_rdに同じクロックを接続して使用可能。
- rst_wr_n: 同期リセット
- clk_wr同期、負論理。
- fsyncブロックでclk_rdに同期したリセット信号を生成するので、読み出し側にもリセットがかかる。
- clk_rdで同期化できる必要があるので、最低でもclk_rd×2サイクルを超える期間アサート/ネゲートしておく必要がある。
- wr_n: 書き込み要求
- clk_wr同期、負論理。
- wr_n=0と同相で書き込みデータdat_wr[ ]を入力する。
- rd_n: 読み出し要求
- clk_rd同期、負論理。
- rd_n=0の次相で読み出しデータがdat_rd[ ]に出力される。
- dat_wr[ ]: 書き込みデータ
- wr_n=0と同相で書き込みデータdat_wr[ ]を入力する。
- データビット幅は、コンパイルオプションで指定可能。
【出力信号】:
- dat_rd[ ]: 読み出しデータ
- clk_rd同期。
- rd_n=0の次相で読み出しデータがdat_rd[ ]に出力される。
- データビット幅は、コンパイルオプションで指定可能。
- full_wr: 書き込みデータフル
- clk_wr同期、正論理。
- FIFOがフルの場合にアサートされる。
- full_wr_adv: 先行書き込みデータフル
- clk_wr同期、正論理。
- もうすぐフルになる場合に先行してアサートされる。
- 先行出力の閾値はコンパイルオプションで指定可能。
- empty_rd: 読み出しデータエンプティ
- clk_rd同期、正論理。
- FIFOが空の場合にアサートされる。
- empty_rd_adv: 先行読み出しデータエンプティ
- clk_rd同期、正論理。
- もうすぐ空になる場合に先行してアサートされる。
- 先行出力の閾値はコンパイルオプションで指定可能。
FIFOビット幅、FIFO段数、先行出力閾値の指定は、RTLコンパイル時のオプションとし、実行時の動的変更はできません。
また、書き込みデータビット幅と、読み出しデータビット幅を異なる値に設定可能な場合もありますが、今回のFIFO回路では対応しません。
ポインタ制御
こちらに示したブロック図で、書き込みポインタptr_wr、および、読み出しポインタptr_rdは、「次にアクセスする分散RAMのアドレス」を示しています。
つまり、ptr_wr=ptr_rdの場合、次に書き込もうとするアドレスptr_wrと、次に読み出そうとするアドレスptr_rdが一致してしまっているのだから、「読み出し可能なデータがない」⇒エンプティ(空)であることを示していることになります。
この状態になったら、empty_rdフラグがアサートされます。empty_rd=1の状態でrd_n要求をアサートして読み出す行為は禁止です。もし読み出すと無視され、常にデータ0が読み出されます。
通常の場合、書き込みデータが溜まって行き、追いかけるように読み出しポインタが追従するので、ptr_wr>ptr_rdで推移します。
分散RAMはリングバッファになっていて、最大アドレスを超えるとアドレス0に戻って増加し続けます。
読み出しが間に合わず、書き込みデータが滞留すると、いずれ、ptr_wr+1=ptr_rdとなって、「書き込み可能な分散RAM領域が残っていない」⇒フル(満)状態となります。
この状態になったら、full_wrフラグがアサートされます。full_wr=1の状態でwr_n要求をアサートして書き込む行為は禁止です。もし書き込むと無視されます。
※エラーフラグ等は用意しないので、使用者がempty_rd/full_wrフラグをモニタしながら、禁止アクセスを実行しないように制御しなくてはなりません。
上で説明した内容を、もう少し具体的に整理して行きます。
- FIFO段数をAMAX段とする。
- ptr_wr/ptr_rdは、次にアクセスすべきアドレスを示していて、アクセスすると+1インクリメントする。
- ptr_wr=AMAX−1の次のアドレスは、ptr_wr=0に戻る。ptr_rdも同様。
- 0≦ptr_wr≦AMAX−1
- 0≦ptr_rd≦AMAX−1
- ptr_wr/ptr_rdは無符号なので、符号ビット"0"を追加して減算する。
- sub_ptr=ptr_wr−ptr_rd
- −AMAX+1≦sub_ptr≦AMAX−1
- sub_ptr≧0なら:
- sub_ptrがFIFOバッファ内の読み出し可能なデータ数を表す。
- 特に、sub_ptr=0の場合は、エンプティ状態を示す。
- sub_ptr<0の場合:
- ptr_wrがオーバフロウしてAMAX−1の状態から0へ遷移したケースであり、ptr_wrとptr_rdの見かけ上の大小関係が逆転している。
- 本来は、ptr_wr’=ptr_wr+AMAXと考えて大小比較すべきなので、
- sub_ptr’=ptr_wr+AMAX−ptr_rd=sub_ptr+AMAX
- sub_ptr’がFIFOバッファ内の読み出し可能なデータ数を表す。
- sub_ptrには符号ビットが追加されているため、さらに仮数部1ビットを追加しないと、AMAXの加算はできない。
- これを考慮すると、sub_ptr’=sub_ptr&(〜AMAX)と等価である。
- 「〜」は、ビット毎論理否定演算子。
- sub_ptrの正負に関わらず、上の式でsub_ptr’を計算して良い。
- 即ち、sub_ptrの符号ビットを剥ぎ取って、無符号として大小比較すれば良い。
- エンプティおよび、フルフラグ条件:
- FIFOが空の場合、ptr_wr=ptr_rdなので、empty_rd=( sub_ptr=0 )。
- FIFOがフルの場合、
- ptr_wr+1=ptr_rdなので、full_wr=( sub_ptr=−1 )、または、
- ptr_wr+1−AMAX=ptr_rdの場合もフルなので、full_wr=( sub_ptr=AMAX−1 )。
- 先行出力の条件:
- 先行エンプティフラグの閾値をADVE、先行フルフラグの閾値をADVFとする。
- 先行空: empty_rd_adv=( sub_ptr’≦ADVE )
- 先行フル: full_wr_adv=( sub_ptr’≧ADVF )
コンパイルオプション
RTLとしては、以下の様なコンパイルオプション(`defineマクロ)を用意して、所望のFIFO回路を生成可能な様にしておこうと思います。
- FIFOIP_DBIT: データビット幅
- FIFOIP_ABIT: アドレスビット幅
- FIFOIP_SYNC: 入力クロックは同期
- 同期クロックとして使う場合「`define」、
- 非同期クロックとして使う場合「`undef」。
- FIFOIP_AMAX: FIFO段数
- FIFOIP_ADVE: 先行エンプティ段数
- FIFOIP_ADVF: 先行フル段数
- FIFOIP_TOP: 最上位モジュール名
目標仕様としては、「非同期クロックが使えること」としましたので、その様な回路を作り込んであります。
しかし、非同期クロック間通信の基本はハンドシェイクであり、通信には大変時間がかかります。
「#19-5 FPGAボードのAD変換器を使う」の非同期設計をご確認ください。
つまり、例えば以下の様なケースが考えられます。
- 書き込んでから、エンプティフラグがクリアされるまで、とか、
- 読み出してから、フルフラグがクリアされるまで、のタイムラグが長くなる。
- 特に、非同期クロックのclk_wrまたはclk_rdの内、どちらか一方の周波数が極端に遅い場合に顕著となる。
- 同期化回路通過のため、片道2クロックのタイムラグがある。
- ハンドシェイクではそのタイムラグが往復🔁で利いて来るので、似通った周波数であっても、相当に大きなタイムラグとなる。
まず、最初の応用として考えているUART8N1ユニットで使う場合は、読み出し/書き込み共同じクロック、すなわち同期クロックで使います。同期クロックで使う場合には、fsyncブロックにて信号を通過させれば、タイムラグは無くなります。
つまり、そういうコンパイルオプション(FIFOIP_SYNC)も必要だったという事です。
尚、同期クロックとして使う場合、clk_wrおよびclk_rd端子には、同じクロックを接続して使わなくては、なりません。
モジュール名ルール
1本のRTLから、コンパイルオプションによって様々な構成のFIFO回路を作ることができるため、以下の様な命名ルールを定めておきました。
FIFO回路命名ルール
- "fifo"+<ビット幅>+[sa]+<段数>
- <ビット幅>: 8、16など、データのビット幅=FIFOIP_DBIT。
- [sa]:
- 同期クロック(`define FIFOIP_SYNC)の場合は[s]ynchronous。
- 非同期クロック(`undef FIFOIP_SYNC)の場合は[a]synchronous。
- <段数>: 64、128等、FIFO段数=FIFOIP_AMAX。
UART8N1ユニット用としては、8ビット×64段(同期クロック)を考えているので、「fifo8s64」がモジュール名となります。
分散RAMの推定
分散RAMは、以下の様なreg宣言を使って記述しました。
// memory
(* ram_style = "distributed" *)
reg [`FIFOIP_DBIT - 1:0] mem[0:`FIFOIP_AMAX - 1];
(* 〜 *)は、ブロックコメント/* 〜 */に似ていますが、Verilog-HDLの属性指定記述です。主に、論理合成ツールに対して、属性を指定するために使用します。中身の「ram_style = "distributed"」は、恐らく、Xilinx Vivado固有の属性だと思われます。distributed(分散)を指定することで、RAMマクロではなく、分散RAMを推定するように指示しています。
これが無いと、何らかの閾値を超えてサイズが大きくなった場合、RAMマクロを推定されてしまいます。最初、128バイト程の分散RAMを使おうとしただけでRAMマクロを推定されてしまいましたので、閾値は、かなり小さいみたいです。
※他にも、特定の信号名を論理合成後にも保存する「(* dont_touch = "yes" *)」等の属性があります。
論理検証
以下の条件で単体検証を実行して仕上げました。
- 1)`define FIFOIP_SYNC、clk_wr=clk_rd
- 同期クロックに設定して、
- clk_wr=clk_rdとして検証。
- 2)`undef FIFOIP_SYNC、clk_wr=clk_rd
- 3)`undef FIFOIP_SYNC、clk_wr>clk_rd
- 4)`undef FIFOIP_SYNC、clk_wr<clk_rd
- 非同期クロックに設定して、
- 各クロック周波数の大小関係で検証。
その後、Moscoviumマイコンに組み上げて、結合検証もざっくり、こなしておきました。
最後に、Cmod A7ボードに書き込んで、UART8N1ユニットから大量のデータを受信する、ブートモードなどが正常に動作していることも確認済みです。
デザイン
従来型名のまま、UART8N1ユニットをインスタンスすると、デフォルトで64バイトFIFO回路が接続されるように変更してあります。
万一、旧来の1KバイトFIFOマクロを使用したい場合には、以下のコンパイルオプションを設定すると、復活させることができます。
// Compile option
// I/O unit
`define MCOC_UART_FIFO_SIZE 1024
VHファイルに上の行を追加すると、従来の1KバイトFIFOマクロを使うようになります。たぶん、1Kバイトなんて巨大なFIFOは必要なかったはずですが、必要に応じて、構成を変更して使ってください。
Cmod A7ボード用Moscoviumマイコンの設計データ(Vivadoプロジェクト)は、GitHubにて配布しております。以下バナーよりアクセスしてください。
ダウンロード方法などについては、トップのREADME.mdをご覧ください。
Cmod A7ボードへの書き込み方法は、こちらを参照してください。
コンパイル
Moscoviumマイコン全体でコンパイルしても、「何の事やら?」と言う感じなので、mcoc_comn.vファイル内のmcoc_uartモジュール階層で単体コンパイルして見ました。mcoc_uartモジュール階層には、UART8N1ユニット本体回路と、受信FIFOバッファ回路が含まれています。
論理規模比較 |
今回のFIFO回路
fifo8s64(64バイト)
|
VivadoのFIFOマクロ
xpm_fifo_sync(1Kバイト)
|
mcoc_uart全体 |
LUT数(合算) |
268 |
286 |
F/F数 |
250 |
297 |
|
UART8N1
ユニット
|
LUT数 |
216 |
218 |
F/F数 |
223 |
223 |
FIFO |
ロジック
LUT数
|
40 |
68 |
F/F数 |
27 |
74 |
分散RAM
LUT数
|
12 |
0 |
18Kビット
RAMマクロ数
|
0 |
1 |
Vivadoでコンパイルして結果報告を見ると、使用したLUT数がロジックのLUT数と分散RAMのLUT数に分離され、別々に報告されています。上表で、全体のLUT数では合算しています。
結果として、今回のfifo8s64モジュールはVivadoのFIFOマクロと比べてもコンパクトに仕上がっています。
- ロジックLUT数およびF/F数は、FIFO制御回路の論理規模を示し、
- 分散RAM LUT数は、分散RAMに要したLUT数を示している。
- fifo8s64モジュールは、ロジックLUT数、F/F数とも、FIFOマクロと比べて小さい(青色セル)。
- 分散RAM(64バイト分)のLUT数(12)を合算してもなお、FIFOマクロより小さい。
- 18KビットRAMマクロを消費せずに構成できて、論理規模(LUT数、F/F数)も小さい(黄色セル)。
- 64バイトもの分散RAMが、たったの12LUTで実装可能なことも驚き。
勿論、本当に1Kバイト超のFIFOが必要なケースでは、FIFOマクロ(RAMマクロ)を使った方が費用は低く抑えられますので、適材適所で選定することが大事です。
今まで小容量のFIFOマクロが使い難かったため、FIFOを搭載したかったけど回避したモジュールが有るのですが、今後は、今回設計したモジュールを応用することができます。自前(RTL)でFIFOモジュールを持っておくと、色々と応用が利くので、お勧めです。