异步 FIFO 用来在两个不同、互不相关的时钟域之间安全传输数据。
比如:
写时钟域 读时钟域
clk_wr = 100 MHz clk_rd = 65 MHz
数据 ──> [ 写控制 ] ──> RAM ──> [ 读控制 ] ──> 数据
↑ ↑
wr_clk rd_clk
两个时钟可能频率不同,也可能相位完全没关系,所以不能直接把多位数据从一个时钟域硬塞给另一个时钟域。
假设 FPGA 里面:
模块 A:100 MHz
模块 B:65 MHz
这就有 CDC 问题。
因为 data_a 可能恰好在 clk_65m 的采样边沿附近变化,导致:
建立时间违例
保持时间违例
亚稳态
而且如果是:
data_a[13:0]
这样的多位数据,甚至可能出现:
原来:01111111
目标:10000000
实际读到:00001111
也就是各个位并不是同一时刻稳定下来。
异步 FIFO 的作用就是把:
写数据
↓
写时钟域 → FIFO → 读时钟域
↓
读数据
这个跨时钟域过程处理安全。
异步 FIFO 的组成
一个标准异步 FIFO 大致是:
Async FIFO
写时钟域 读时钟域
wr_clk rd_clk
│ │
▼ ▼
wr_en ──> 写地址 读地址 <── rd_en
│ │
▼ ▼
┌──────────────────────────────┐
data_in │ Dual Port RAM │ data_out
───────>│ │─────────>
└──────────────────────────────┘
▲ ▲
│ │
Write Pointer Read Pointer
│ │
Binary Binary
│ │
Gray Gray
│ │
└──── CDC ───────────┘
你可以把异步 FIFO 拆成 5 个核心部分:
- 双口 RAM
- 写指针
wptr - 读指针
rptr - Gray Code 跨时钟同步
full / empty判断
这里真正难的不是 RAM。
真正难的是指针跨时钟域。
写指针和读指针
假设 FIFO 深度:
DEPTH = 8
地址范围:
0 ~ 7
写指针负责:
写一次
wptr = wptr + 1
读指针一样:
rptr = rptr + 1
为什么指针不能直接跨时钟域?
这就是你前面问过的:
为什么异步 FIFO 使用格雷码?
举个最经典的例子。
二进制计数:
0111
↓
1000
理论上是一次变化。
但是实际上有 4 bit 同时翻转:
0 → 1
1 → 0
1 → 0
1 → 0
由于 FPGA 内部布线延时不完全一样,另一个时钟域可能看到:
0111
0011
0001
1001
1000
中间状态。
于是对方可能错误判断 FIFO:
空了?
满了?
还有几个数据?
这就危险了。
Gray Code 为什么能解决?
Gray Code 最大特点:
相邻两个数只有 1 bit 发生变化。
例如 3 位 Gray Code:
| Binary | Gray |
|---|---|
| 000 | 000 |
| 001 | 001 |
| 010 | 011 |
| 011 | 010 |
| 100 | 110 |
| 101 | 111 |
| 110 | 101 |
| 111 | 100 |
例如:
Binary:
011 → 100
变化很多位。
而对应 Gray:
010 → 110
只变化:
bit2
因此跨时钟域的时候,就算恰好采在变化边沿附近,最多也就是:
旧 Gray
或者
新 Gray
不会组合出来一个距离特别远的错误值。
所以异步 FIFO 通常是:
Binary Pointer
│
▼
Gray Pointer
│
▼
2级寄存器同步
│
▼
另一个时钟域
Gray Code 怎么计算?
这个公式你最好记下来,面试非常常见:
gray = binary ^ (binary >> 1);
例如:
Binary = 0110
右移:
0110
0011
----
XOR
0101
所以:
0110 Binary
↓
0101 Gray
Verilog 一般这样:
gray_next = bin_next ^ (bin_next >> 1);
为什么还需要"两级同步器"?
即使使用 Gray Code,也仍然存在一个问题:
那唯一变化的 bit 也可能产生亚稳态。
所以:
Gray Pointer
↓
DFF
↓
DFF
↓
同步完成
例如把读指针同步到写时钟域:
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
rptr_gray_sync1 <= 0;
rptr_gray_sync2 <= 0;
end
else begin
rptr_gray_sync1 <= rptr_gray;
rptr_gray_sync2 <= rptr_gray_sync1;
end
end
结构:
读时钟域 写时钟域
rptr_gray ────────────> DFF ──> DFF
↑ ↑
wr_clk wr_clk
第一级寄存器可能亚稳。
第二级给它一个完整时钟周期恢复。
数据本身为什么不用 Gray Code?
这里特别容易误解。
异步 FIFO 里:
数据
并不是直接跨 CDC 同步器。
而是写入:
Dual Port RAM
例如:
wr_clk
↓
写 RAM[address]
另一个时钟域:
rd_clk
↓
读 RAM[address]
因此:
数据 → RAM → 数据
而真正需要跨时钟域的是:
写指针
读指针
所以结构实际上是:
数据 ┌──────── RAM ─────────┐
─────────>│ │─────────>
└──────────────────────┘
wptr ─Gray──> rd_clk
rptr ─Gray──> wr_clk
empty 怎么判断?
这个比较简单。
在 读时钟域:
如果:
读指针 == 已同步过来的写指针
说明:
没有未读数据
所以:
EMPTY
即:
rptr_gray_next == wptr_gray_sync
可以理解成:
写到 5
读到 5
那么:
所有写入的数据都已经读完
FIFO 空。
full 怎么判断?
这个稍微复杂。
因为 FIFO 是环形结构。
假设深度 8:
地址:
0
1
2
3
4
5
6
7
然后又回到 0
如果只比较:
write_addr == read_addr
你根本分不清:
FIFO 空
还是:
FIFO 满
所以指针通常比 RAM 地址 多 1 bit。
例如深度:
8 = 2³
RAM 地址是:
3 bit
但 Pointer:
4 bit
即:
Pointer = {wrap_bit, address}
例如:
0000
0001
...
0111
1000
1001
...
最高位相当于记录:
我已经绕 FIFO 一圈了。
Binary 情况下怎么理解 full?
假设:
rptr = 0_010
写指针如果到了:
wptr = 1_010
地址一样:
010
但 MSB 不一样。
表示:
写指针比读指针领先整整一圈
所以:
FIFO FULL
直观上:
rptr
↓
[2][3][4][5][6][7][0][1]
↑
wptr 已经绕了一圈
为什么 Gray Code 判断 full 要反转两位?
这是异步 FIFO 里最经典、也是面试最喜欢问的一点。
比如:
wgray_next ==
{~rgray_sync[ADDRSIZE:ADDRSIZE-1],
rgray_sync[ADDRSIZE-2:0]}
很多人第一次看:
为什么是最高两位取反?
原因是:
Binary 指针相差:
FIFO_DEPTH
对应到 Gray Code 之后,并不是简单:
最高位取反
而是 Gray 编码特性导致:
最高两位发生反转
所以 full 的常见判断就是:
assign full_val =
wgray_next ==
{~rgray_sync[PTR_WIDTH-1:PTR_WIDTH-2],
rgray_sync[PTR_WIDTH-3:0]};
这个公式建议记住。
一个完整的数据过程
假设:
写时钟 = 100 MHz
读时钟 = 65 MHz
FIFO = 8 deep
初始:
wptr = 0
rptr = 0
所以:
EMPTY = 1
FULL = 0
现在写入:
A
B
C
写指针:
0 → 1 → 2 → 3
RAM:
addr 0 = A
addr 1 = B
addr 2 = C
此时:
wptr = 3
rptr = 0
读端开始读。
第一次:
读 addr 0 → A
rptr = 1
第二次:
读 addr 1 → B
rptr = 2
第三次:
读 addr 2 → C
rptr = 3
最终:
wptr == rptr
于是:
EMPTY = 1
为什么写快读慢也没关系?
例如:
写:100 MHz
读:65 MHz
短时间内没关系。
因为 FIFO 就是一个:
数据蓄水池
可以想象:
水龙头 100 MHz
↓
┌─────────────┐
│ │
│ FIFO │
│ │
└─────────────┘
↓
65 MHz 排水
但如果:
长期写入速率 > 长期读取速率
FIFO 最终一定:
FULL
所以 FIFO 只能吸收:
瞬时速率差
不能违反长期平均带宽。
同步 FIFO 和异步 FIFO 区别
| 项目 | 同步 FIFO | 异步 FIFO |
|---|---|---|
| 写时钟 | clk | wr_clk |
| 读时钟 | clk | rd_clk |
| 时钟关系 | 相同 | 可以完全无关 |
| Gray Code | 通常不需要 | 基本需要 |
| CDC | 无 | 有 |
| 指针同步 | 不需要 | 需要 |
| 难度 | 较低 | 较高 |
所以:
同一个 clk
↓
同步 FIFO
而:
clk_A ≠ clk_B
↓
异步 FIFO
下面 Verilog 异步 FIFO RTL。它包含:
-
双口存储
-
写指针 / 读指针
-
Binary → Gray
-
Gray 指针跨时钟域两级同步
-
full -
emptymodule async_fifo #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4
)(
input wire wr_clk,
input wire rd_clk,
input wire rst_n,input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, output wire full, input wire rd_en, output reg [DATA_WIDTH-1:0] rd_data, output wire empty);
// FIFO 深度 = 2^ADDR_WIDTH localparam DEPTH = (1 << ADDR_WIDTH); // ----------------------------- // 1. FIFO 存储空间 // ----------------------------- reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; // 指针需要比地址多 1 bit reg [ADDR_WIDTH:0] wr_bin; reg [ADDR_WIDTH:0] wr_gray; reg [ADDR_WIDTH:0] rd_bin; reg [ADDR_WIDTH:0] rd_gray; wire [ADDR_WIDTH:0] wr_bin_next; wire [ADDR_WIDTH:0] wr_gray_next; wire [ADDR_WIDTH:0] rd_bin_next; wire [ADDR_WIDTH:0] rd_gray_next; // ----------------------------- // 2. 跨时钟同步后的 Gray 指针 // ----------------------------- // 读指针同步到写时钟域 reg [ADDR_WIDTH:0] rd_gray_sync1; reg [ADDR_WIDTH:0] rd_gray_sync2; // 写指针同步到读时钟域 reg [ADDR_WIDTH:0] wr_gray_sync1; reg [ADDR_WIDTH:0] wr_gray_sync2; // ===================================================== // 写时钟域 // ===================================================== // ----------------------------- // 3. 计算下一拍写二进制指针 // ----------------------------- assign wr_bin_next = wr_bin + ((wr_en && !full) ? 1'b1 : 1'b0); // Binary -> Gray assign wr_gray_next = (wr_bin_next >> 1) ^ wr_bin_next; // ----------------------------- // 4. 写数据 // ----------------------------- always @(posedge wr_clk or negedge rst_n) begin if (!rst_n) begin wr_bin <= 0; wr_gray <= 0; end else begin if (wr_en && !full) begin mem[wr_bin[ADDR_WIDTH-1:0]] <= wr_data; end wr_bin <= wr_bin_next; wr_gray <= wr_gray_next; end end // ----------------------------- // 5. 读指针同步到写时钟域 // ----------------------------- always @(posedge wr_clk or negedge rst_n) begin if (!rst_n) begin rd_gray_sync1 <= 0; rd_gray_sync2 <= 0; end else begin rd_gray_sync1 <= rd_gray; rd_gray_sync2 <= rd_gray_sync1; end end // ----------------------------- // 6. Full 判断 // ----------------------------- assign full = (wr_gray_next == {~rd_gray_sync2[ADDR_WIDTH:ADDR_WIDTH-1], rd_gray_sync2[ADDR_WIDTH-2:0]}); // ===================================================== // 读时钟域 // ===================================================== // ----------------------------- // 7. 计算下一拍读二进制指针 // ----------------------------- assign rd_bin_next = rd_bin + ((rd_en && !empty) ? 1'b1 : 1'b0); // Binary -> Gray assign rd_gray_next = (rd_bin_next >> 1) ^ rd_bin_next; // ----------------------------- // 8. 读取数据 // ----------------------------- always @(posedge rd_clk or negedge rst_n) begin if (!rst_n) begin rd_bin <= 0; rd_gray <= 0; rd_data <= 0; end else begin if (rd_en && !empty) begin rd_data <= mem[rd_bin[ADDR_WIDTH-1:0]]; end rd_bin <= rd_bin_next; rd_gray <= rd_gray_next; end end // ----------------------------- // 9. 写指针同步到读时钟域 // ----------------------------- always @(posedge rd_clk or negedge rst_n) begin if (!rst_n) begin wr_gray_sync1 <= 0; wr_gray_sync2 <= 0; end else begin wr_gray_sync1 <= wr_gray; wr_gray_sync2 <= wr_gray_sync1; end end // ----------------------------- // 10. Empty 判断 // ----------------------------- assign empty = (rd_gray_next == wr_gray_sync2);endmodule