异步 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 GrayVerilog 一般这样:
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_clkempty 怎么判断?
这个比较简单。
在读时钟域:
如果:
读指针 == 已同步过来的写指针说明:
没有未读数据所以:
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 → 3RAM:
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 指针跨时钟域两级同步
fullempty
module 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