一、異步 FIFO 的基本概念
1.1 定義與核心作用
????????異步 FIFO(Asynchronous FIFO)是一種讀寫時鐘完全獨立的先進先出(First-In-First-Out)數據緩沖器,主要用于跨時鐘域數據傳輸場景。在數字系統中,當兩個模塊工作在不同時鐘頻率或相位下時,異步 FIFO 可作為數據中轉站,解決數據傳輸中的時序沖突、速率不匹配問題,避免數據丟失或錯誤。
1.2 與同步 FIFO 的核心區別
二、異步 FIFO 的基本結構
????????異步 FIFO 的核心結構由 5 部分組成:
存儲單元(RAM);空滿檢測邏輯(解決跨時鐘域問題);寫指針(寫時鐘域);讀指針(讀時鐘域);讀寫接口(時鐘,數據,使能)
2.1 存儲單元(Storage Element)
存儲單元是 FIFO 的數據緩沖區,在 FPGA 中通常采用兩種實現方式:
- 塊 RAM(Block RAM):適用于深度較大的 FIFO(如深度 > 64),資源利用率高,速度快(FPGA 內置的硬核 RAM)。
- 分布式 RAM(Distributed RAM):適用于小深度 FIFO(如深度≤32),由 LUT(查找表)拼接而成,靈活性高但資源消耗大。
存儲單元的核心參數:
- 數據位寬(DATA_WIDTH):單次傳輸的數據寬度(如 8/16/32 位)。
- 深度(DEPTH):可存儲的數據個數(通常為 2?,便于地址編碼)。
2.2 指針(Pointers)
指針用于跟蹤 FIFO 的讀寫位置,分為寫指針和讀指針:
- 寫指針(Write Pointer):在寫時鐘(wr_clk)驅動下,指示下一個待寫入數據的地址;每完成一次寫入(wr_en 有效且非滿),指針 + 1。
- 讀指針(Read Pointer):在讀時鐘(rd_clk)驅動下,指示下一個待讀出數據的地址;每完成一次讀出(rd_en 有效且非空),指針 + 1。
指針的關鍵特性:
指針采用n+1 位二進制編碼(n 為地址位寬),其中低 n 位為實際存儲地址,最高位(第 n 位)用于區分 “空” 和 “滿” 狀態(指示指針是否多循環一圈)。例如:深度為 4(22)的 FIFO,地址位寬 2 位,指針為 3 位(bit2-bit0)。
2.3 空滿檢測邏輯(Empty/Full Logic)
空滿狀態是 FIFO 的核心控制信號:
- 空信號(empty):讀時鐘域輸出,指示 FIFO 無數據可讀(防止空讀錯誤)。
- 滿信號(full):寫時鐘域輸出,指示 FIFO 無空間可寫(防止滿寫錯誤)。
核心挑戰:讀寫指針屬于不同時鐘域,直接跨域比較會導致亞穩態(Metastability),需通過特殊設計實現準確判斷。
三、異步 FIFO 的核心技術難點與解決方案
3.1 跨時鐘域同步與亞穩態問題
3.1.1 亞穩態產生原因
當一個時鐘域的信號(如讀指針)直接進入另一個時鐘域(如寫時鐘域)時,若信號在目標時鐘的建立時間(Setup Time)?或保持時間(Hold Time)?窗口內變化,觸發器會進入亞穩態(輸出不穩定),導致后續邏輯錯誤。
關于亞穩態數據的跳變:
當輸入數據在時鐘沿附近的?([T{su}, Th])?窗口內發生變化(即數據跳變時間落在時鐘沿前?(T{su})?到時鐘沿后?(Th)?的區間內),觸發器的輸出無法穩定到明確的 0 或 1,而是處于介于高電平與低電平之間的不穩定狀態,這種狀態稱為亞穩態。亞穩態的持續時間是隨機的,最終會隨機穩定到 0 或 1,但在此期間輸出信號是不確定的,可能導致后續邏輯錯誤。
3.1.2 兩級同步器解決方案
為解決亞穩態,異步 FIFO 中采用兩級 D 觸發器同步器(Two-Stage Synchronizer)將指針從一個時鐘域同步到另一個時鐘域:
- 一級 DFF:捕獲輸入信號,可能進入亞穩態,但亞穩態持續時間會隨時間衰減。
- 二級 DFF:在一級 DFF 穩定后采樣,將亞穩態概率降低到可接受范圍(FPGA 中通常可忽略)。
3.2 格雷碼(Gray Code)的應用
3.2.1 為什么需要格雷碼?
二進制指針在跨域同步時存在 “多位跳變” 問題(如二進制011
→100
跳變 3 位),同步器可能采樣到中間錯誤狀態。而格雷碼的相鄰數值僅一位不同,可最大限度降低同步錯誤概率。
3.2.2 二進制與格雷碼的轉換
- 二進制轉格雷碼:
gray_code = binary_code ^ (binary_code >> 1)
例:4 位二進制0101
→格雷碼0101 ^ 0010 = 0111
。 - 格雷碼轉二進制:
binary_code[MSB] = gray_code[MSB]
;binary_code[i] = binary_code[i+1] ^ gray_code[i]
(從高位到低位)。
3.2.3 指針編碼規則
異步 FIFO 中,讀寫指針的二進制值先轉換為格雷碼,再通過兩級同步器跨時鐘域傳輸,確保同步后的值接近真實值。
3.3 空滿狀態的準確判斷
3.3.1 空狀態(Empty)判斷
空狀態定義:讀指針追上寫指針,FIFO 中無數據。
判斷邏輯(讀時鐘域):
當讀指針格雷碼等于同步到讀時鐘域的寫指針格雷碼時,FIFO 為空:
empty = (rd_gray == sync_wr_gray_to_rd_clk)
3.3.2 滿狀態(Full)判斷
滿狀態定義:寫指針比讀指針多繞 FIFO 一圈(即多走一個深度),FIFO 中數據已滿。
判斷邏輯(寫時鐘域):
當寫指針格雷碼的低 n 位等于同步到寫時鐘域的讀指針格雷碼的低 n 位,且最高位相反時,FIFO 為滿:
full = (wr_gray == {~sync_rd_gray_to_wr_clk[MSB], sync_rd_gray_to_wr_clk[MSB-1:0]})
示例(深度 = 4,指針 3 位):
- 寫指針二進制 = 4(100)→格雷碼 = 110;讀指針二進制 = 0(000)→格雷碼 = 000。
- 同步到寫時鐘域的讀指針格雷碼為 000,滿條件:
110 == {~000[2], 000[1:0]} → 110 == 100?
?不,修正:滿條件應為寫指針格雷碼與同步讀指針格雷碼的 “高兩位相反,低 n 位相同”。
正確例:寫指針格雷碼110
(100),同步讀指針格雷碼010
(010)→高兩位1
?vs?0
,低兩位10
?vs?10
→滿狀態。
3.4?類比FIFO工作過程
給異步 FIFO 起個生活化的名字:“跨節奏倉庫”
想象你家樓下有個小倉庫,專門用來臨時放東西。這個倉庫有?4 個格子(編號 0、1、2、3),就像 FIFO 的存儲空間。
倉庫有兩個 “操作員”:
- 寫小哥:負責往倉庫里存東西(寫操作),他干活的節奏是 “每 2 秒存一個”(寫時鐘,比如 2Hz)。
- 讀小哥:負責從倉庫里取東西(讀操作),他干活的節奏是 “每 3 秒取一個”(讀時鐘,比如 3Hz)。
兩人節奏不一樣(異步),但要同時干活,還不能出問題:不能把倉庫塞滿了還硬塞(溢出),也不能倉庫空了還硬取(讀空)。
核心工具:兩個 “位置箭頭” 和 “同步小紙條”
為了讓兩人配合好,他們各有一個 “箭頭”(指針):
- 寫箭頭:寫小哥每次存完東西,箭頭就往前挪一格,標記 “下一個要存東西的位置”。
- 讀箭頭:讀小哥每次取完東西,箭頭就往前挪一格,標記 “下一個要取東西的位置”。
但兩人節奏不一樣,互相看不到對方的箭頭,所以需要 “同步小紙條”:
寫小哥會把自己的箭頭位置寫在紙條上,傳給讀小哥(寫指針同步到讀時鐘域),讓讀小哥知道 “倉庫里現在有多少東西可以取”。
- 讀小哥也會把自己的箭頭位置寫在紙條上,傳給寫小哥(讀指針同步到寫時鐘域),讓寫小哥知道 “倉庫還有多少空位置可以存”。
具體過程:讀寫同時進行的一天
初始狀態:倉庫是空的,寫箭頭和讀箭頭都指著 “0 號格子”(剛開始都從 0 開始)。
第 1 秒:寫小哥先干活
寫小哥帶了個 “蘋果”,看了看自己的箭頭(指著 0),就把蘋果放進 0 號格子。存完后,寫箭頭往前挪一格,指向 1 號格子。
此時倉庫里有:0 號格子(蘋果)。
寫小哥把 “寫箭頭現在在 1” 寫在紙條上,傳給讀小哥(同步寫指針到讀端)。
第 3 秒:讀小哥第一次干活(讀時鐘到了)
讀小哥收到紙條,看到寫箭頭在 1,自己的讀箭頭還在 0。他知道:“寫箭頭在 1,我在 0,說明 0 號格子有東西可以取!”
于是他取走 0 號格子的蘋果,取完后,讀箭頭往前挪一格,指向 1 號格子。
此時倉庫里:空了 0 號格子,還剩(暫時沒新東西)。
讀小哥把 “讀箭頭現在在 1” 寫在紙條上,傳給寫小哥(同步讀指針到寫端)。
第 4 秒:寫小哥第二次干活(寫時鐘到了)
寫小哥收到紙條,看到讀箭頭在 1,自己的寫箭頭在 1。他知道:“我的箭頭和讀箭頭沒重合,倉庫沒滿,可以存!”
于是把 “香蕉” 放進 1 號格子,存完后寫箭頭挪到 2 號格子。
倉庫里現在有:1 號格子(香蕉)。
第 6 秒:寫小哥第三次干活
寫小哥又存 “橙子” 到 2 號格子,寫箭頭挪到 3 號格子。
倉庫里:1 號(香蕉)、2 號(橙子)。
第 6 秒:讀小哥第二次干活(剛好和寫小哥同時)
此時讀小哥的讀時鐘也到了(和寫小哥存橙子的動作同時發生)。
讀小哥看自己的讀箭頭在 1,同步過來的寫箭頭在 3(寫小哥剛挪到 3)。他知道:“1 號格子有香蕉可以取!”
于是取走 1 號格子的香蕉,讀箭頭挪到 2 號格子。
倉庫里現在剩:2 號格子(橙子)。
第 8 秒:寫小哥第四次干活
寫小哥存 “葡萄” 到 3 號格子,寫箭頭本來要挪到 4 號,但倉庫只有 4 個格子(0-3),所以繞回 0 號格子(箭頭變成 0)。
倉庫里現在有:2 號(橙子)、3 號(葡萄)。
寫小哥把 “箭頭在 0” 同步給讀小哥。
第 9 秒:讀小哥第三次干活
讀小哥看到寫箭頭同步后是 0,自己的讀箭頭在 2。他知道:“2 號格子有橙子,取!”
取走橙子,讀箭頭挪到 3 號格子。
倉庫里剩:3 號格子(葡萄)。
第 10 秒:寫小哥第五次干活(和讀小哥可能有重疊)
寫小哥想存 “西瓜”,看自己的箭頭在 0,同步過來的讀箭頭在 3(讀小哥剛挪到 3)。
他判斷:“我的下一個要存的位置是 0,讀箭頭在 3,沒重合,倉庫沒滿!” 于是把西瓜存到 0 號格子,寫箭頭挪到 1 號。
倉庫里現在有:3 號(葡萄)、0 號(西瓜)。
第 12 秒:讀小哥第四次干活
讀小哥取走 3 號格子的葡萄,讀箭頭挪到 0 號格子。
倉庫里剩:0 號格子(西瓜)。
就這樣,寫小哥每 2 秒存一個,讀小哥每 3 秒取一個,動作可能重疊(同時進行),但通過 “箭頭標記位置” 和 “同步紙條”,寫小哥永遠知道倉庫有沒有空位置(不會存滿溢出),讀小哥永遠知道有沒有東西可以取(不會取空出錯)。
關鍵總結
- 異步 FIFO 就像這個 “跨節奏倉庫”,寫和讀可以按自己的節奏同時干。
- 指針(箭頭)標記當前存 / 取的位置,同步后互相 “看一眼” 對方的位置。
- 空滿判斷:讀箭頭追上寫箭頭(沒東西了)就停讀;寫箭頭快追上讀箭頭(倉庫快滿了)就停寫。
- 這樣即使節奏不同、動作同時發生,數據也不會丟、不會亂~
同步指針的速度(同步操作)和存放東西的速度(讀寫操作)完全沒關系,兩者各走各的節奏,互不影響快慢。
四、FPGA中FIFO的應用
?一。FIFO中的FPGA 中主要用于:
- 跨時鐘域數據傳輸(異步 FIFO)
- 速率匹配(例如高速發送端→低速接收端)
- 數據緩沖(突發數據暫存)
核心參數:
- 深度:可存儲的數據個數(如深度 8 表示存 8 個數據)
- 寬度:每個數據的位寬(如寬度 32bit 表示每個數據 32 位)
- 空滿標志:控制讀寫操作的關鍵信號
二、Verilog 代碼實現(異步 FIFO)
下面是一個深度為 8、位寬為 8 的異步 FIFO 的完整 Verilog 代碼:
module async_fifo #(parameter DATA_WIDTH = 8, // 數據位寬parameter ADDR_WIDTH = 3 // 地址位寬(2^3=8深度)
)(// 寫時鐘域input wire wclk, // 寫時鐘input wire wrst_n, // 寫復位(低有效)input wire w_en, // 寫使能input wire [DATA_WIDTH-1:0] w_data, // 寫入數據// 讀時鐘域input wire rclk, // 讀時鐘input wire rrst_n, // 讀復位(低有效)input wire r_en, // 讀使能output reg [DATA_WIDTH-1:0] r_data, // 讀出數據// 狀態標志output wire full, // 滿標志output wire empty // 空標志
);// 內部信號定義reg [ADDR_WIDTH:0] w_ptr_bin; // 寫指針(二進制)reg [ADDR_WIDTH:0] r_ptr_bin; // 讀指針(二進制)reg [ADDR_WIDTH:0] w_ptr_gray; // 寫指針(格雷碼)reg [ADDR_WIDTH:0] r_ptr_gray; // 讀指針(格雷碼)// 跨時鐘域同步的指針reg [ADDR_WIDTH:0] w_ptr_gray_sync1, w_ptr_gray_sync2;reg [ADDR_WIDTH:0] r_ptr_gray_sync1, r_ptr_gray_sync2;// 存儲器陣列reg [DATA_WIDTH-1:0] fifo_mem [0:(1<<ADDR_WIDTH)-1];// 二進制轉格雷碼function [ADDR_WIDTH:0] bin_to_gray(input [ADDR_WIDTH:0] bin);bin_to_gray = bin ^ (bin >> 1);endfunction// 格雷碼轉二進制function [ADDR_WIDTH:0] gray_to_bin(input [ADDR_WIDTH:0] gray);integer i;reg [ADDR_WIDTH:0] bin;beginbin = gray;for (i = 1; i <= ADDR_WIDTH; i = i + 1)bin = bin ^ (gray >> i);endgray_to_bin = bin;endfunction// 寫操作always @(posedge wclk or negedge wrst_n) beginif (!wrst_n) beginw_ptr_bin <= 'b0;w_ptr_gray <= 'b0;endelse if (w_en && !full) beginfifo_mem[w_ptr_bin[ADDR_WIDTH-1:0]] <= w_data; // 寫入數據w_ptr_bin <= w_ptr_bin + 1'b1; // 寫指針遞增w_ptr_gray <= bin_to_gray(w_ptr_bin); // 更新格雷碼指針endend// 讀操作always @(posedge rclk or negedge rrst_n) beginif (!rrst_n) beginr_ptr_bin <= 'b0;r_ptr_gray <= 'b0;r_data <= 'b0;endelse if (r_en && !empty) beginr_data <= fifo_mem[r_ptr_bin[ADDR_WIDTH-1:0]]; // 讀出數據r_ptr_bin <= r_ptr_bin + 1'b1; // 讀指針遞增r_ptr_gray <= bin_to_gray(r_ptr_bin); // 更新格雷碼指針endend// 兩級同步器 - 寫指針同步到讀時鐘域always @(posedge rclk or negedge rrst_n) beginif (!rrst_n) beginw_ptr_gray_sync1 <= 'b0;w_ptr_gray_sync2 <= 'b0;endelse beginw_ptr_gray_sync1 <= w_ptr_gray;w_ptr_gray_sync2 <= w_ptr_gray_sync1;endend// 兩級同步器 - 讀指針同步到寫時鐘域always @(posedge wclk or negedge wrst_n) beginif (!wrst_n) beginr_ptr_gray_sync1 <= 'b0;r_ptr_gray_sync2 <= 'b0;endelse beginr_ptr_gray_sync1 <= r_ptr_gray;r_ptr_gray_sync2 <= r_ptr_gray_sync1;endend// 空標志判斷(在讀時鐘域)assign empty = (r_ptr_gray == w_ptr_gray_sync2);// 滿標志判斷(在寫時鐘域)assign full = (w_ptr_gray == {~r_ptr_gray_sync2[ADDR_WIDTH:ADDR_WIDTH-1], r_ptr_gray_sync2[ADDR_WIDTH-2:0]});endmodule
三、原語實現
FPGA 廠商提供了專用的存儲器原語,例如 Xilinx 的BRAM
原語,使用原語可以更高效地實現 FIFO:
module fifo_bram_primitive (input wire wclk, wrst_n, w_en,input wire [7:0] w_data,input wire rclk, rrst_n, r_en,output reg [7:0] r_data,output wire full, empty
);// 地址和計數器reg [2:0] w_addr, r_addr;reg [3:0] count; // 深度8需要4位計數器// 生成滿空標志assign full = (count == 4'd8);assign empty = (count == 4'd0);// 實例化BRAM原語RAMB16_S8_S8 #(.WRITE_WIDTH_A(8), // 寫端口寬度.WRITE_WIDTH_B(8), // 讀端口寬度.SRVAL_A(8'h00), // 復位值.SRVAL_B(8'h00)) bram_inst (.CLK_A(wclk), // 寫時鐘.EN_A(w_en & !full), // 寫使能.WE_A(1'b1), // 寫使能.ADDR_A(w_addr), // 寫地址.DI_A(w_data), // 寫入數據.DO_A(), // 寫端口輸出(不用).CLK_B(rclk), // 讀時鐘.EN_B(r_en & !empty), // 讀使能.WE_B(1'b0), // 讀端口禁止寫.ADDR_B(r_addr), // 讀地址.DI_B(8'h00), // 讀端口數據輸入(不用).DO_B(r_data) // 讀出數據);// 寫地址控制always @(posedge wclk or negedge wrst_n) beginif (!wrst_n)w_addr <= 3'd0;else if (w_en & !full)w_addr <= w_addr + 1'b1;end// 讀地址控制always @(posedge rclk or negedge rrst_n) beginif (!rrst_n)r_addr <= 3'd0;else if (r_en & !empty)r_addr <= r_addr + 1'b1;end// 計數器控制(使用寫時鐘)always @(posedge wclk or negedge wrst_n) beginif (!wrst_n)count <= 4'd0;else begincase ({w_en & !full, r_en & !empty})2'b00: count <= count; // 無讀寫2'b01: count <= count - 1; // 只讀2'b10: count <= count + 1; // 只寫2'b11: count <= count; // 同時讀寫,計數器不變endcaseendendendmodule
四、IP 核應用
大多數 FPGA 廠商提供了 FIFO 的 IP 核,使用 IP 核可以快速配置和實例化 FIFO