計(jì)核心解析:從同步到異步的跨時鐘域與Verilog實(shí)現(xiàn))
做FPGA和數(shù)字IC設(shè)計(jì)的人幾乎沒有繞開過FIFO的。不管是圖像數(shù)據(jù)流的跨時鐘域處理、以太網(wǎng)包緩存、還是CPU與外設(shè)之間的數(shù)據(jù)交互你總能碰到這個長得不太起眼、卻無處不在的緩沖結(jié)構(gòu)。我剛接觸硬件設(shè)計(jì)那會兒總覺得FIFO不過是一個“先進(jìn)先出的隊(duì)列”把它當(dāng)成一個現(xiàn)成的IP核調(diào)一下、接上讀寫端口就完事了。后來真正開始自己動手搭RTL、調(diào)試異步FIFO的空滿信號、甚至為了一個假滿問題折騰了兩天之后才意識到這個模塊的“水”比想象中深得多。這篇文章我就想把自己的理解完整梳理一遍FIFO到底是怎么工作的為什么它能解決跨時鐘域的數(shù)據(jù)傳輸問題同步FIFO和異步FIFO在設(shè)計(jì)思路上有什么本質(zhì)區(qū)別以及當(dāng)你需要自己寫一個FIFO的Verilog代碼時哪些核心細(xì)節(jié)決定了它能不能在工程中穩(wěn)定跑起來。適合剛?cè)腴TFPGA、數(shù)字IC驗(yàn)證、以及想把手寫FIFO用在實(shí)際項(xiàng)目里的朋友我會盡量用大白話把原理和代碼拆開講透。1. FIFO到底解決什么問題1.1 從數(shù)據(jù)結(jié)構(gòu)到硬件模塊我們在軟件課里學(xué)到的FIFOFirst In First Out描述的是一個數(shù)據(jù)緩沖隊(duì)列最先寫入的數(shù)據(jù)最先被讀出就像排隊(duì)買東西一樣先來的人先結(jié)賬。這個邏輯在硬件世界里同樣成立只是它不再是一個抽象的數(shù)據(jù)結(jié)構(gòu)而是由一組存儲單元、讀寫指針、狀態(tài)邏輯共同構(gòu)成的真實(shí)電路。硬件FIFO的核心組成可以拆成幾塊存儲體通常用雙口RAM或者寄存器陣列實(shí)現(xiàn)、寫指針、讀指針以及空/滿狀態(tài)的判斷邏輯。數(shù)據(jù)從寫端口進(jìn)來寫到寫指針指向的地址寫完寫指針加一數(shù)據(jù)從讀端口出去從讀指針指向的地址讀出讀完讀指針加一。當(dāng)寫指針追上讀指針說明存儲區(qū)被填滿當(dāng)讀指針追上寫指針說明存儲區(qū)被清空。但真正讓FIFO在硬件設(shè)計(jì)里“封神”的并不是它的隊(duì)列特性而是它能天然地橫跨兩個不同的時鐘域。比如一個ADC采集模塊工作在50MHz一個總線接口工作在150MHz兩邊沒法直接相連中間塞一個FIFO兩邊各寫各的、各讀各的互不干擾。這就是同步FIFO和異步FIFO在實(shí)際項(xiàng)目中最重要的應(yīng)用場景數(shù)據(jù)緩沖和時鐘域隔離。1.2 為什么不用寄存器堆直接接有人可能會問不就是兩個模塊之間傳數(shù)據(jù)嗎我在中間放一堆寄存器用握手信號挨個傳不行嗎當(dāng)然行但代價很高。如果數(shù)據(jù)速率是400Mbps每次傳輸都要握手應(yīng)答那握手信號本身的建立保持時間約束、跨時鐘域打拍延遲會嚴(yán)重拉低吞吐量。而且當(dāng)數(shù)據(jù)的產(chǎn)生和消費(fèi)速率不完全匹配時中間任何一個環(huán)節(jié)稍微堵塞立刻會丟數(shù)據(jù)或者卡住上游。FIFO的好處在于它把“存儲”和“流控”綁在了一起。只要FIFO不滿寫端就可以持續(xù)往里面灌數(shù)據(jù)只要FIFO不空讀端就可以持續(xù)往外取數(shù)據(jù)。它的滿信號天然就是一個反壓信號告訴上游“你先停一停我消化不過來”它的空信號天然就是一個就緒信號告訴下游“我已經(jīng)有貨了你可以來取”。這種“自我調(diào)理”的能力讓FIFO幾乎成了跨時鐘域傳輸?shù)哪J(rèn)答案。2. 同步FIFO的核心原理與Verilog實(shí)現(xiàn)2.1 同步FIFO的讀寫指針與空滿判斷所謂同步FIFO就是讀寫兩端共用一個時鐘只是數(shù)據(jù)速率或節(jié)奏可能不同。比如某個模塊在時鐘上升沿連續(xù)寫入8個數(shù)據(jù)然后暫停另一個模塊在同一時鐘下慢慢讀出。因?yàn)楣餐瑫r鐘指針變化天然同步空滿判斷比較簡單。用計(jì)數(shù)器的方式來理解最直觀寫指針指向下一個要寫入的地址讀指針指向下一個要讀出的地址。當(dāng)寫操作發(fā)生且FIFO未滿寫指針加一當(dāng)讀操作發(fā)生且FIFO未空讀指針加一當(dāng)讀和寫同時發(fā)生時保持一個數(shù)據(jù)條目被抵消。要判斷空還是滿最直接的方法是維護(hù)一個計(jì)數(shù)器寫入時加一讀出時減一讀寫同時發(fā)生時不變計(jì)數(shù)器為0表示空計(jì)數(shù)器等于深度表示滿。這個方案在小型FIFO里完全夠用代碼清晰、邏輯簡單、仿真也容易驗(yàn)證。但當(dāng)你把FIFO深度做到1024、4096甚至更大用二進(jìn)制計(jì)數(shù)器的組合邏輯去算空滿信號路徑延遲可能會變得很難看。更常見的做法是直接比較讀寫指針讀指針等于寫指針時可能空也可能滿需要額外加一個附加位來區(qū)分。比如把指針位寬擴(kuò)展一位最高位不同的同時低幾位相同就表示滿完全相等則表示為空。2.2 一個可直接使用的同步FIFO代碼下面我寫一個參數(shù)化、帶計(jì)數(shù)器的同步FIFO這個結(jié)構(gòu)在中小規(guī)模數(shù)據(jù)緩存里很實(shí)用代碼結(jié)構(gòu)也適合做二次修改module sync_fifo #( parameter DATA_WIDTH 32, parameter DEPTH 16 )( input wire clk, input wire rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, input wire rd_en, output reg [DATA_WIDTH-1:0] rd_data, output wire full, output wire empty ); localparam ADDR_WIDTH $clog2(DEPTH); reg [ADDR_WIDTH:0] cnt; reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; reg [ADDR_WIDTH-1:0] wr_ptr; reg [ADDR_WIDTH-1:0] rd_ptr; assign full (cnt DEPTH); assign empty (cnt d0); always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr d0; rd_ptr d0; cnt d0; end else begin case ({wr_en ~full, rd_en ~empty}) 2b10: cnt cnt 1b1; 2b01: cnt cnt - 1b1; default: cnt cnt; endcase if (wr_en !full) begin mem[wr_ptr] wr_data; wr_ptr wr_ptr 1b1; end if (rd_en !empty) begin rd_data mem[rd_ptr]; rd_ptr rd_ptr 1b1; end end end endmodule這段代碼有幾個細(xì)節(jié)值得注意。寫入條件的wr_en !full和計(jì)數(shù)器的更新條件保持一致避免出現(xiàn)“計(jì)數(shù)器覺得沒滿但寫入指針已經(jīng)溢出”的時序風(fēng)險。讀出數(shù)據(jù)用了寄存輸出雖然多了一拍延遲但能夠有效避免在同一個周期內(nèi)讀指針變化導(dǎo)致讀數(shù)據(jù)不穩(wěn)定的問題。如果項(xiàng)目對讀延遲敏感可以把rd_data改成組合邏輯直接接mem[rd_ptr]代價是時序收斂難度略增。2.3 參數(shù)設(shè)計(jì)里容易被忽略的兩個點(diǎn)第一點(diǎn)是深度必須是2的整數(shù)次冪嗎上面代碼里直接用wr_ptr 1b1做回卷隱含假設(shè)了地址可以自然溢出。如果深度不是2的冪次比如你需要12個深度的FIFO那指針就要額外做回卷判斷空滿計(jì)數(shù)器的計(jì)算也會變得繁瑣。工程上我建議優(yōu)先把深度設(shè)成2的冪次如果實(shí)在需要非2冪次深度就得在指針自增后加邊界判斷代碼復(fù)雜度和出錯概率都會上升。第二點(diǎn)是$clog2函數(shù)在部分老工具里可能不支持或者綜合結(jié)果和你想的不一樣。如果你的開發(fā)環(huán)境比較老建議直接用常量定義比如localparam ADDR_WIDTH 4;同時寫一行注釋說明深度是16。用$clog2不是不行但一定要在仿真階段就確認(rèn)參數(shù)展開是否正確別等到綜合報(bào)錯才發(fā)現(xiàn)問題。3. 異步FIFO的設(shè)計(jì)難點(diǎn)與經(jīng)典解法3.1 跨時鐘域的本質(zhì)風(fēng)險異步FIFO的讀寫端口分別工作在兩個完全獨(dú)立的時鐘域里比如寫時鐘100MHz讀時鐘75MHz兩個時鐘之間沒有相位關(guān)系甚至頻率都不成整數(shù)倍。這時讀寫指針都各自在自己的時鐘域里變化如果直接把讀指針的二進(jìn)制值拿到寫時鐘域里去比較就會遇到一個致命的跨時鐘域問題多位信號同時變化時采樣結(jié)果可能“四不像”。舉個例子讀指針從0111變成1000二進(jìn)制下最低四位同時翻轉(zhuǎn)。如果恰好在這個變化的瞬間寫時鐘域?qū)λ蓸硬蓸咏Y(jié)果可能是0000、0111、1000或者任何中間組合。這種沒有意義的中間值一旦被用于空滿判斷FIFO就可能發(fā)出錯誤的滿信號直接導(dǎo)致寫端停寫丟數(shù)據(jù)或者發(fā)出錯誤的空信號讓讀端讀到垃圾數(shù)據(jù)。這是異步FIFO設(shè)計(jì)里最核心的坎也是面試時必考、工程里必踩的坑。3.2 格雷碼讓跨時鐘域指針安全傳遞解決多位跨時鐘域傳遞的標(biāo)準(zhǔn)手段是把指針從二進(jìn)制轉(zhuǎn)換成格雷碼。格雷碼的特點(diǎn)是相鄰兩個數(shù)值之間只有1位發(fā)生變化。這意味著指針從一個狀態(tài)遞增到下一個狀態(tài)時跨時鐘域采樣最多只有一個比特處于變化中。對于單比特信號只要接收端用兩級寄存器打拍同步就能以極高的概率采到正確的穩(wěn)態(tài)值不會出現(xiàn)“中間組合值”的問題。那是不是用格雷碼就萬事大吉了還差一步同步后的格雷碼指針在判斷空滿時不能直接拿來和本地的二進(jìn)制指針比較。通常的做法是寫時鐘域里把寫指針轉(zhuǎn)成格雷碼再打兩拍同步到讀時鐘域讀時鐘域里把讀指針轉(zhuǎn)成格雷碼再打兩拍同步到寫時鐘域。判斷滿時用寫時鐘域里的寫指針格雷碼可以再轉(zhuǎn)回二進(jìn)制或直接格雷碼比較和同步過來的讀指針格雷碼比較判斷空時用讀時鐘域里的讀指針格雷碼和同步過來的寫指針格雷碼比較。指針從二進(jìn)制轉(zhuǎn)格雷碼的公式是gray (bin 1) ^ bin這個公式到處都是但我還是建議親手推一遍。它的本質(zhì)是保留二進(jìn)制最高位然后每一位都和上一位做異或。解碼也就是把格雷碼變回二進(jìn)制需要從最高位逐位向下異或綜合出來是一串組合邏輯鏈。如果FIFO深度很大這個譯碼鏈的延遲也會成為一個時序關(guān)注點(diǎn)不過對于一般深度在32到1024范圍內(nèi)的FIFO通常都能跑得很穩(wěn)。3.3 異步FIFO空滿判斷的精確語義異步FIFO的空滿判斷本質(zhì)上是一個“近似判斷”。因?yàn)樽x指針同步到寫時鐘域需要兩拍寫指針同步到讀時鐘域也需要兩拍所以任何一端看到的對端指針都可能是兩個時鐘周期之前的狀態(tài)。這個延遲會帶來兩個現(xiàn)象假滿和假空。假滿的意思是寫端看到滿信號拉高了但實(shí)際上因?yàn)樽x端正在讀數(shù)據(jù)、FIFO已經(jīng)有空位了這時候?qū)懚诉x擇停寫就會損失一些寫帶寬。假空同理讀端看到空信號但因?yàn)閷懚苏趯憯?shù)據(jù)FIFO里其實(shí)馬上就要有數(shù)據(jù)了這時候讀端等待就會出現(xiàn)一段讀空檔。異步FIFO設(shè)計(jì)里我們寧可出現(xiàn)假滿假空也絕不能讓滿信號在FIFO真正滿了之后才拉高或者空信號在FIFO真正空了之后才拉低——前者導(dǎo)致覆蓋寫入、數(shù)據(jù)被沖掉后果極其嚴(yán)重。設(shè)計(jì)時我們用格雷碼比較來判斷空滿。以“滿”為例在寫時鐘域?qū)懼羔樃窭状a的每一位和同步過來的讀指針格雷碼要滿足特定關(guān)系。最簡單的方法是當(dāng)二進(jìn)制指針的最高兩位不同、其余位都相同時表示FIFO已滿。轉(zhuǎn)換成格雷碼之后的判斷是最高位和次高位相反其余位相同。這個關(guān)系我能記住是因?yàn)橛幸淮握{(diào)試時發(fā)現(xiàn)自己的判斷條件寫反了導(dǎo)致FIFO從不通告滿信號結(jié)果上游數(shù)據(jù)嘩嘩往里灌存儲區(qū)被覆蓋得面目全非。那次查了一晚上最終就是把~和對調(diào)了一個位置。3.4 異步FIFO的核心代碼框架這里展示一個經(jīng)典異步FIFO的關(guān)鍵邏輯結(jié)構(gòu)。省略了存儲體部分重點(diǎn)看指針同步和空滿判斷的框架module async_fifo #( parameter DSIZE 8, parameter ASIZE 4 )( input wire wclk, wrst_n, input wire winc, input wire [DSIZE-1:0] wdata, output wire wfull, input wire rclk, rrst_n, input wire rinc, output wire [DSIZE-1:0] rdata, output wire rempty ); wire [ASIZE-1:0] waddr, raddr; wire [ASIZE:0] wptr, rptr; wire [ASIZE:0] wq2_rptr, rq2_wptr; // 讀指針同步到寫時鐘域用于滿判斷 sync_r2w sync_r2w_inst ( .wclk(wclk), .wrst_n(wrst_n), .rptr(rptr), .wq2_rptr(wq2_rptr) ); // 寫指針同步到讀時鐘域用于空判斷 sync_w2r sync_w2r_inst ( .rclk(rclk), .rrst_n(rrst_n), .wptr(wptr), .rq2_wptr(rq2_wptr) ); // 滿判斷寫指針與同步讀指針的格雷碼差一個“來回” assign wfull (wptr {~wq2_rptr[ASIZE:ASIZE-1], wq2_rptr[ASIZE-2:0]}); // 空判斷讀指針與同步寫指針完全相等 assign rempty (rptr rq2_wptr); // 存儲體、讀寫指針自增邏輯略 endmodule嚴(yán)格說這個框架里的wptr、rptr應(yīng)該以格雷碼形式存儲和傳遞實(shí)際代碼里會在指針模塊內(nèi)部維護(hù)一份二進(jìn)制指針用于尋址再額外輸出格雷碼指針用于跨域同步。上面的結(jié)構(gòu)重點(diǎn)展示了滿判斷條件當(dāng)寫指針格雷碼的高兩位等于同步讀指針格雷碼高兩位的反時認(rèn)為FIFO滿。這是異步FIFO設(shè)計(jì)中一個非常經(jīng)典的判斷方式也提醒了我空和滿的判斷條件是不對稱的千萬不要圖省事復(fù)制粘貼。4. 設(shè)計(jì)中的常見坑與排查方法4.1 實(shí)測最容易翻車的三個場景第一復(fù)位釋放的跨時鐘域同步問題。異步FIFO的復(fù)位信號如果是異步復(fù)位、釋放時又和各自的時鐘沒有對齊可能讓指針處于一個非法的中間狀態(tài)。工程上比較穩(wěn)妥的做法是在讀寫時鐘域分別做異步復(fù)位同步釋放確保復(fù)位信號在本地時鐘域內(nèi)干凈地釋放。我見過最詭異的一個現(xiàn)象是復(fù)位明明拉低了但第一個寫操作寫完FIFO直接報(bào)滿——原因是復(fù)位釋放時寫指針的格雷碼變了一個非法值同步到讀時鐘域后又引發(fā)了對端判斷混亂。第二讀寫同時發(fā)生時計(jì)數(shù)器更新順序。在同步FIFO里讀寫同時發(fā)生的優(yōu)先級到底給誰這看起來是個小問題但處理不好會導(dǎo)致計(jì)數(shù)器和指針不一致。我慣用的方式是讓寫優(yōu)先也就是先保證寫不丟因?yàn)樽x端如果等一個周期再讀通常只是損失一點(diǎn)讀吞吐而寫端一旦因?yàn)镕IFO誤判為滿而丟數(shù)據(jù)這個數(shù)據(jù)就是永久性丟失了。第三FIFO深度和指針位寬不匹配。深度16需要4位地址指針但空滿判斷有時需要額外擴(kuò)展一位做區(qū)分。如果直接在原來的$clog2(DEPTH)基礎(chǔ)上加1代碼沒問題但如果有人圖省事把指針直接定義成[DEPTH-1:0]仿真時看不出來綜合后大概率會出問題。寫代碼之前把參數(shù)列一張表深度、地址位寬、指針位寬、格雷碼位寬一一對應(yīng)寫清楚后面排查會省很多事。4.2 空滿標(biāo)志的常見陷阱速查現(xiàn)象可能原因排查方向?qū)懚诉€沒寫幾個數(shù)FIFO就報(bào)滿讀指針沒有正確同步到寫時鐘域格雷碼判斷條件寫反檢查同步器輸出波形檢查滿判斷條件高兩位的反邏輯讀端一直讀不到數(shù)據(jù)empty拉高不釋放寫指針同步有問題復(fù)位釋放異常看寫時鐘域里寫指針是否正常遞增檢查格雷碼是否出現(xiàn)多比特跳變FIFO數(shù)據(jù)被覆蓋舊數(shù)據(jù)丟失滿信號來得太晚寫端在未滿時提前寫入了非法地址確認(rèn)滿信號和寫指針自增的時序關(guān)系仿真打標(biāo)記檢查是否出現(xiàn)寫穿仿真正常上板后偶發(fā)錯數(shù)存儲體讀寫碰撞時鐘偏斜未處理讀延遲差異檢查雙口RAM讀寫時序確認(rèn)是否有組合邏輯路徑未約束這個表格里的每一種現(xiàn)象我都碰到過。印象最深的是“仿真正常上板偶發(fā)錯數(shù)”當(dāng)時用的是IP核生成的異步FIFO但沒留意IP核里讀數(shù)據(jù)有額外的輸出寄存器選項(xiàng)導(dǎo)致讀路徑比預(yù)期晚了一拍和后續(xù)邏輯的采樣窗口錯開。從那以后我每次例化FIFO IP都會先做一次極簡環(huán)回驗(yàn)證寫端往FIFO里灌0到255讀端再讀出來比對跑一遍全流程再放到大工程里。5. 選型建議與工程實(shí)踐心得5.1 手寫還是調(diào)IP核現(xiàn)在Xilinx、Intel的FPGA開發(fā)環(huán)境里都有非常成熟的FIFO IP核支持同步、異步、標(biāo)準(zhǔn)模式、首字直通模式、almost full/empty標(biāo)志甚至還能生成計(jì)數(shù)與錯誤標(biāo)志。如果你是在FPGA上做項(xiàng)目絕大多數(shù)情況下直接用IP核就好原因是IP核經(jīng)過廠商充分驗(yàn)證時序約束、復(fù)位同步、格雷碼轉(zhuǎn)換都已經(jīng)處理好了比自己在RTL里手搓要省心得多。那什么時候需要手寫FIFO一類是ASIC設(shè)計(jì)里沒有現(xiàn)成IP可用所有東西都得從標(biāo)準(zhǔn)單元搭另一類是FIFO的邏輯非常特殊比如需要同時讀寫多個端口、需要帶優(yōu)先級的覆蓋寫、深度或位寬非常規(guī)。還有一個場景是你在做IC驗(yàn)證或面試準(zhǔn)備想弄明白FIFO內(nèi)部到底怎么工作這時候手寫一個同步FIFO、再寫一個異步FIFO收獲遠(yuǎn)比直接調(diào)IP大得多。我自己就是靠“手寫三個不同版本的FIFO”徹底搞懂了空滿判斷和格雷碼的深水區(qū)。5.2 從AXI Stream FIFO看FIFO的延伸如果你接觸過AXI Stream接口你會發(fā)現(xiàn)AXI Stream FIFO和經(jīng)典FIFO不完全一樣。它不只是把數(shù)據(jù)存進(jìn)隊(duì)列還要處理valid/ready握手信號當(dāng)FIFO不滿時寫側(cè)ready拉高表示愿意接收數(shù)據(jù)當(dāng)FIFO不空時讀側(cè)valid拉高表示有數(shù)據(jù)可供讀取。它比傳統(tǒng)FIFO多了一層“流控協(xié)議”的封裝。在設(shè)計(jì)這類FIFO時核心思路是一樣的只不過把“空滿信號”翻譯成了握手語義。full對應(yīng)寫側(cè)ready拉低empty對應(yīng)讀側(cè)valid拉低。如果再加almost full、almost empty這些水位線信號還能在數(shù)據(jù)量接近上限或下限時提前給出預(yù)警這在DMA傳輸、DDR讀寫調(diào)度里非常有用。理解了基礎(chǔ)FIFO的原理再去看各種衍生FIFO基本就是加一個協(xié)議殼子的事。5.3 我在實(shí)際項(xiàng)目里的幾點(diǎn)習(xí)慣做FIFO設(shè)計(jì)這么多年我總結(jié)出幾個談不上高大上但非常實(shí)用的習(xí)慣。第一所有FIFO的讀寫指針在仿真時一定要用$monitor或者斷言實(shí)時監(jiān)控不能等到跑完整波形成噸的數(shù)據(jù)后才去翻波形。好的做法是在每個寫操作完成后斷言!full或full之前不允許繼續(xù)寫把這個斷言寫進(jìn)testbench里只要有微小的空滿判斷漏洞仿真立刻紅一大片。第二異步FIFO的同步器打拍寄存器必須使用不帶復(fù)位的觸發(fā)器。很多人會在同步器上順手接復(fù)位信號但復(fù)位信號本身如果和接收時鐘域不同步反而給同步器引入了新的不確定狀態(tài)。FIFO內(nèi)部同步器的復(fù)位一般由接收時鐘域自己的復(fù)位生成并且打拍寄存器只做移位不做任何復(fù)位干預(yù)。這個細(xì)節(jié)在ASIC設(shè)計(jì)規(guī)范里寫得很清楚但FPGA工程里有時會被忽略。第三如果在調(diào)試時看到FIFO指針出現(xiàn)格雷碼中不存在的編碼別急著改邏輯先檢查跨時鐘域采樣路徑上是否有組合邏輯。格雷碼指針在跨域之前必須是寄存器直接輸出任何組合邏輯操作都可能破壞“每次只變一位”這個前提。我一同事曾經(jīng)為了圖方便把格雷碼指針做了一次加一操作再跨域結(jié)果那個數(shù)據(jù)路徑上出現(xiàn)毛刺整整排查了三天才定位到。6. 擴(kuò)展視角分清不同類型FIFO的適用場合6.1 同步FIFO、異步FIFO與首字直通模式同步FIFO適合兩端同頻但節(jié)奏不匹配的場景比如同一個時鐘域內(nèi)的數(shù)據(jù)打包、解包、速率適配。異步FIFO適合兩端完全異頻的場景它把跨時鐘域的難題從“數(shù)據(jù)線跨域”轉(zhuǎn)移成了“指針格雷碼跨域”代價是兩拍同步延遲帶來的假空假滿。首字直通模式First Word Fall Through是另一個常被提及的變體數(shù)據(jù)寫入后不需要讀請求就自動出現(xiàn)在讀數(shù)據(jù)總線上只是empty為低。它減少了一拍讀延遲在一些需要極低延遲的數(shù)據(jù)通路里非常實(shí)用。代價是讀數(shù)據(jù)總線的組合邏輯路徑邊長時序收斂難度更高。選哪種模式核心取決于你的后續(xù)模塊對讀數(shù)據(jù)延遲的容忍度。6.2 深度、位寬與寬度轉(zhuǎn)換FIFO可以同時做位寬轉(zhuǎn)換比如寫側(cè)32位、讀側(cè)8位FIFO內(nèi)部存儲的還是32位數(shù)據(jù)讀側(cè)每讀一次取其中8位這需要額外的移位和使能控制。反過來8位寫32位讀也常見。位寬轉(zhuǎn)換會直接影響有效深度的計(jì)算因?yàn)榇鎯卧獢?shù)量不變但讀到的最小粒度變了。設(shè)計(jì)時建議把“存儲單元寬度”和“端口氣位寬”分開思考否則很容易把FIFO容量估錯。還有一種情形是讀寫兩側(cè)數(shù)據(jù)速率嚴(yán)格相等、但交接時中間有毛刺這時一個深度很小的同步FIFO就夠用比如深度4到8之間。深度太小會頻繁拉滿、拉低反壓深度過大又浪費(fèi)存儲資源且增加延遲。按我的經(jīng)驗(yàn)先估算最壞情況下寫端連續(xù)burst長度再留出兩級以上裕量是一個比較穩(wěn)妥的深度選擇方式。6.3 在大系統(tǒng)中的定位緩存、反壓還是異步橋FIFO在一個完整系統(tǒng)里扮演的角色可以分成三種來看。第一種是純粹的數(shù)據(jù)緩存比如CPU要寫一批數(shù)據(jù)到低速外設(shè)先把數(shù)據(jù)暫存在FIFO里外設(shè)慢慢取。第二種是反壓接口FIFO的滿信號直接給上游模塊的ready做門控讓上游的突發(fā)傳輸自動停下。第三種是異步橋把兩個時鐘域的數(shù)據(jù)無縫地接在一起FIFO本身相當(dāng)于一個“蓄水池”讀寫兩端各自對池子進(jìn)水、放水互不干擾。明白了這三種角色你就能更好地理解為什么有的FIFO需要almost_full信號有的需要programmable_full有的需要錯誤標(biāo)志。不是因?yàn)镕IFO復(fù)雜而是它要適配的系統(tǒng)場景本來就復(fù)雜。把FIFO放進(jìn)系統(tǒng)的整體視野里去看遠(yuǎn)比孤立地研究一個隊(duì)列結(jié)構(gòu)更有價值。我在實(shí)際做過的Zynq平臺在線升級項(xiàng)目里就用過一套異步FIFO來緩存BOOT引導(dǎo)數(shù)據(jù)和升級鏡像。寫側(cè)是PS端通過AXI總線灌數(shù)據(jù)讀側(cè)是PL端的SPI Flash控制器按扇區(qū)慢慢寫。FIFO深度選的是204832位寬幾乎在升級全程沒有出現(xiàn)過反壓丟包靠的就是設(shè)計(jì)之初對兩端最壞速率的準(zhǔn)確估算和異步FIFO的空滿信號合理使用。那次之后我對FIFO體感最深的一句話是它像一個知道什么時候該喝水的緩沖池而你真正要設(shè)計(jì)的是池子的水位信號和進(jìn)出水閘門。