ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

异步FIFO核心揭秘:格雷码与指针同步

异步FIFO核心揭秘:格雷码与指针同步 异步 FIFO 用来在两个不同、互不相关的时钟域之间安全传输数据。比如写时钟域 读时钟域 clk_wr 100 MHz clk_rd 65 MHz 数据 ── [ 写控制 ] ── RAM ── [ 读控制 ] ── 数据 ↑ ↑ wr_clk rd_clk两个时钟可能频率不同也可能相位完全没关系所以不能直接把多位数据从一个时钟域硬塞给另一个时钟域。假设 FPGA 里面模块 A100 MHz 模块 B65 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读指针rptrGray 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 CodeBinaryGray000000001001010011011010100110101111110101111100例如Binary: 011 → 100变化很多位。而对应 Gray010 → 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但 Pointer4 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 → 3RAMaddr 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写时钟clkwr_clk读时钟clkrd_clk时钟关系相同可以完全无关Gray Code通常不需要基本需要CDC无有指针同步不需要需要难度较低较高所以同一个 clk ↓ 同步 FIFO而clk_A ≠ clk_B ↓ 异步 FIFO下面Verilog 异步 FIFO RTL。它包含双口存储写指针 / 读指针Binary → GrayGray 指针跨时钟域两级同步fullemptymodule 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) ? 1b1 : 1b0); // 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) ? 1b1 : 1b0); // 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
返回列表