一、异步 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(2²)的 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
vs0
,低两位10
vs10
→满状态。
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;
begin
bin = gray;
for (i = 1; i <= ADDR_WIDTH; i = i + 1)
bin = bin ^ (gray >> i);
end
gray_to_bin = bin;
endfunction
// 写操作
always @(posedge wclk or negedge wrst_n) begin
if (!wrst_n) begin
w_ptr_bin <= 'b0;
w_ptr_gray <= 'b0;
end
else if (w_en && !full) begin
fifo_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); // 更新格雷码指针
end
end
// 读操作
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) begin
r_ptr_bin <= 'b0;
r_ptr_gray <= 'b0;
r_data <= 'b0;
end
else if (r_en && !empty) begin
r_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); // 更新格雷码指针
end
end
// 两级同步器 - 写指针同步到读时钟域
always @(posedge rclk or negedge rrst_n) begin
if (!rrst_n) begin
w_ptr_gray_sync1 <= 'b0;
w_ptr_gray_sync2 <= 'b0;
end
else begin
w_ptr_gray_sync1 <= w_ptr_gray;
w_ptr_gray_sync2 <= w_ptr_gray_sync1;
end
end
// 两级同步器 - 读指针同步到写时钟域
always @(posedge wclk or negedge wrst_n) begin
if (!wrst_n) begin
r_ptr_gray_sync1 <= 'b0;
r_ptr_gray_sync2 <= 'b0;
end
else begin
r_ptr_gray_sync1 <= r_ptr_gray;
r_ptr_gray_sync2 <= r_ptr_gray_sync1;
end
end
// 空标志判断(在读时钟域)
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) begin
if (!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) begin
if (!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) begin
if (!wrst_n)
count <= 4'd0;
else begin
case ({w_en & !full, r_en & !empty})
2'b00: count <= count; // 无读写
2'b01: count <= count - 1; // 只读
2'b10: count <= count + 1; // 只写
2'b11: count <= count; // 同时读写,计数器不变
endcase
end
end
endmodule
四、IP 核应用
大多数 FPGA 厂商提供了 FIFO 的 IP 核,使用 IP 核可以快速配置和实例化 FIFO