ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

异步 FIFO 为什么使用格雷码

异步 FIFO 为什么使用格雷码 前言异步 FIFO 中写数据和读数据分别工作在两个没有固定相位关系的时钟域中。例如写时钟wclk读时钟rclk写指针在wclk域更新读指针在rclk域更新为了判断 FIFO 是否为空或已满写时钟域需要知道读指针的位置读时钟域也需要知道写指针的位置。问题就在这里读写指针是多位信号不能直接用普通二进制形式跨时钟域。异步 FIFO 使用格雷码不是因为格雷码更节省资源也不是因为格雷码不会产生亚稳态而是因为格雷码相邻两个计数值之间只变化一个 bit可以降低多位指针跨时钟域时被采样成错误组合值的风险。一、异步 FIFO 到底有哪些信号需要跨时钟域一个典型异步 FIFO 可以简化为下面的结构写时钟域 wclk 读时钟域 rclk --------------------- --------------------- wdata -| 写地址、写指针 | | 读地址、读指针 |- rdata | | | | | 生成 full | | 生成 empty | -------------------- -------------------- | | | 双口 RAM | -------- [ FIFO Memory ] ------ | | | | 同步读指针 同步写指针 rgray - wclk wgray - rclkFIFO 中的数据通常存放在双口 RAM 中写端口由wclk控制读端口由rclk控制写指针决定写地址读指针决定读地址。数据本身并不是简单地经过两级触发器同步到另一个时钟域。真正需要跨时钟域的是写指针写时钟域 - 读时钟域 读指针读时钟域 - 写时钟域同步后的指针主要用于生成empty读时钟域判断 FIFO 是否为空 full 写时钟域判断 FIFO 是否已满二、为什么不能直接同步二进制指针假设 FIFO 的写指针是一个 3 bit 二进制计数器。它的部分变化如下十进制 二进制 2 010 3 011 4 100 5 101观察从 3 增加到 4011 - 100三个 bit 全部发生变化。在 RTL 仿真中这三个 bit 看起来像是同时变化的但在真实 FPGA 或 ASIC 电路中不同 bit 的路径延迟不可能完全相同。因此真实变化过程可能类似原值011 bit2 先变化111 bit1 再变化101 bit0 最后变化100 目标值100也可能出现其他中间组合例如011 - 001 - 000 - 100这些中间值不是计数器真正想发送的值只是多个 bit 传播延迟不一致造成的瞬间状态。如果目标时钟刚好在这个过程中采样就可能采到错误值。例如写指针实际只是3 - 4读时钟域却可能短暂采到0 1 5 7具体采到什么取决于各 bit 的传播延迟和目标时钟采样位置。这会直接影响 FIFO 的空满判断。三、每个 bit 都加两级同步器不行吗很多新人会想到二进制指针有多个 bit那我给每个 bit 都加两级触发器不就可以了吗例如binary_pointer[0] - 两级同步器 binary_pointer[1] - 两级同步器 binary_pointer[2] - 两级同步器这样做仍然有问题。两级同步器主要解决的是降低单个异步信号的亚稳态传播概率。它不能保证多个 bit 在目标时钟域的同一个周期完成同步。例如二进制指针从011 - 100三个 bit 都可能在目标时钟边沿附近变化。同步后可能出现bit2 已经同步为 1 bit1 仍然是旧值 1 bit0 已经同步为 0目标时钟域看到的就是110虽然每一个 bit 都经过了同步器但组合起来的多位数据仍然可能不是旧值011也不是新值100。所以需要明确两级同步器可以降低亚稳态传播风险但不能保证普通多位总线的一致性。四、格雷码解决了什么问题格雷码最重要的特点是相邻两个编码之间只有一个 bit 发生变化。3 bit 二进制计数和对应的格雷码如下十进制二进制格雷码00000001001001201001130110104100110510111161101017111100观察格雷码的连续变化000 001 011 010 110 111 101 100任意两个相邻值之间只变化一个 bit。例如从十进制 3 增加到 4二进制011 - 100 三个 bit 变化 格雷码010 - 110 只有一个 bit 变化当格雷码指针跨时钟域时只有一个 bit 处于变化过程中。目标时钟域可能采到旧值010或者新值110某一个变化 bit 也可能进入亚稳态但经过两级同步器后最终通常只会表现为这次采到旧指针或者下一次才采到新指针不会像二进制计数器那样因为多个 bit 同时变化而轻易拼出一个相差很远的错误指针。这就是异步 FIFO 使用格雷码的核心原因。五、格雷码并不能消除亚稳态这里需要特别纠正一个常见误区。错误说法使用格雷码以后就不会产生亚稳态。正确说法格雷码不能消除亚稳态。格雷码指针仍然是异步信号。如果某个格雷码 bit 恰好在目标时钟采样边沿附近变化该 bit 仍然可能进入亚稳态。真正的处理方法是源时钟域生成并寄存格雷码指针 ↓ 目标时钟域两级或多级同步器 ↓ 目标时钟域使用同步后的格雷码判断 full 或 empty格雷码和同步器解决的是两个不同问题方法主要作用格雷码减少一次计数变化时同时翻转的 bit 数量两级同步器降低亚稳态继续传播到后级逻辑的概率二者通常需要配合使用。六、为什么还要保留二进制指针既然格雷码适合跨时钟域是否可以完全不用二进制指针一般不这样做。异步 FIFO 中通常同时维护两套指针二进制指针用于地址计算和加一运算 格雷码指针用于跨时钟域同步原因很简单。二进制指针加一非常方便wbin_next wbin 1b1;FIFO RAM 的地址也可以直接取二进制指针的低位waddr wbin[ADDR_WIDTH-1:0]; raddr rbin[ADDR_WIDTH-1:0];而格雷码不适合直接进行普通加法。所以实际结构通常是当前二进制指针 | 1 | 下一个二进制指针 | | binary - gray v 下一个格雷码指针七、二进制转格雷码二进制转格雷码的公式非常简单gray binary ^ (binary 1)对应 Verilog 写法assign gray binary ^ (binary 1);例如二进制值为binary 3b101右移一位binary 1 3b010异或得到101 010 --- 111因此binary 101 - gray 111八、写指针的典型实现下面是写指针生成的核心代码。为了同时判断 FIFO 是否绕回指针通常比 RAM 地址多一位。假设FIFO 深度 16 地址位宽 4 bit 指针位宽 5 bit低 4 bit 用作 RAM 地址额外的最高位用于区分指针是否已经绕回。localparam PTR_WIDTH ADDR_WIDTH 1; reg [PTR_WIDTH-1:0] wbin; reg [PTR_WIDTH-1:0] wgray; wire winc; wire [PTR_WIDTH-1:0] wbin_next; wire [PTR_WIDTH-1:0] wgray_next; assign winc w_en !wfull; assign wbin_next wbin winc; assign wgray_next (wbin_next 1) ^ wbin_next; always (posedge wclk or negedge wrst_n) begin if (!wrst_n) begin wbin {PTR_WIDTH{1b0}}; wgray {PTR_WIDTH{1b0}}; end else begin wbin wbin_next; wgray wgray_next; end end assign waddr wbin[ADDR_WIDTH-1:0];这里有三个关键点。1. 只有允许写入时指针才增加winc w_en !wfull;FIFO 已满时即使外部仍然给出w_en写指针也不能继续增加。2. 先计算二进制指针再转成格雷码wbin_next wbin winc; wgray_next (wbin_next 1) ^ wbin_next;3. 跨时钟域的格雷码必须先寄存跨域同步器的输入应尽量来自寄存器输出而不是直接使用可能产生毛刺的组合逻辑。因此实际跨域的是寄存后的wgray而不是直接把一串复杂组合逻辑接到同步器输入。九、读指针的典型实现读指针与写指针基本对称localparam PTR_WIDTH ADDR_WIDTH 1; reg [PTR_WIDTH-1:0] rbin; reg [PTR_WIDTH-1:0] rgray; wire rinc; wire [PTR_WIDTH-1:0] rbin_next; wire [PTR_WIDTH-1:0] rgray_next; assign rinc r_en !rempty; assign rbin_next rbin rinc; assign rgray_next (rbin_next 1) ^ rbin_next; always (posedge rclk or negedge rrst_n) begin if (!rrst_n) begin rbin {PTR_WIDTH{1b0}}; rgray {PTR_WIDTH{1b0}}; end else begin rbin rbin_next; rgray rgray_next; end end assign raddr rbin[ADDR_WIDTH-1:0];FIFO 为空时rinc 1b0;读指针保持不变避免继续读导致下溢。十、格雷码指针如何跨时钟域写指针需要同步到读时钟域读指针也需要同步到写时钟域。下面给出一个简单的两级同步器。module gray_sync #( parameter WIDTH 5 )( input wire clk, input wire rst_n, input wire [WIDTH-1:0] async_gray, output wire [WIDTH-1:0] sync_gray ); (* ASYNC_REG TRUE *) reg [WIDTH-1:0] sync_ff1; (* ASYNC_REG TRUE *) reg [WIDTH-1:0] sync_ff2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync_ff1 {WIDTH{1b0}}; sync_ff2 {WIDTH{1b0}}; end else begin sync_ff1 async_gray; sync_ff2 sync_ff1; end end assign sync_gray sync_ff2; endmodule读指针同步到写时钟域gray_sync #( .WIDTH(PTR_WIDTH) ) u_rgray_to_wclk ( .clk (wclk), .rst_n (wrst_n), .async_gray (rgray), .sync_gray (rgray_sync) );写指针同步到读时钟域gray_sync #( .WIDTH(PTR_WIDTH) ) u_wgray_to_rclk ( .clk (rclk), .rst_n (rrst_n), .async_gray (wgray), .sync_gray (wgray_sync) );需要注意rgray_sync 是写时钟域信号 wgray_sync 是读时钟域信号同步后的指针只能在对应的目标时钟域中使用。十一、empty 是怎么判断的FIFO 为空的本质是读指针追上了写指针在读时钟域中应该比较下一个读格雷码指针和已经同步到读时钟域的写格雷码指针代码如下wire rempty_next; assign rempty_next (rgray_next wgray_sync); always (posedge rclk or negedge rrst_n) begin if (!rrst_n) rempty 1b1; else rempty rempty_next; end为什么比较的是rgray_next不是当前rgray假设 FIFO 中只剩一个数据。当前读指针还没有移动因此当前读写指针不相等。当本周期执行最后一次读取后rgray_next wgray_sync说明这次读完后 FIFO 将变为空因此应在当前读时钟边沿更新rempty。十二、full 为什么要翻转最高两位FIFO 已满表示写指针比读指针正好多走了一整圈二进制指针通常会增加一个额外最高位用于区分地址相同且圈数相同FIFO 为空 地址相同但相差一圈FIFO 已满但是格雷码的满判断不能简单地只翻转一个最高位。对于常见的反射二进制格雷码满判断通常写成wire [PTR_WIDTH-1:0] rgray_full_value; wire wfull_next; assign rgray_full_value { ~rgray_sync[PTR_WIDTH-1:PTR_WIDTH-2], rgray_sync[PTR_WIDTH-3:0] }; assign wfull_next (wgray_next rgray_full_value);然后在写时钟域寄存always (posedge wclk or negedge wrst_n) begin if (!wrst_n) wfull 1b0; else wfull wfull_next; end也就是同步后的读格雷码最高两位取反 其余低位保持不变 然后与下一个写格雷码比较该写法适用于常见的FIFO 深度 2 的整数次幂 指针位宽 地址位宽 1 ADDR_WIDTH 2不要把这段逻辑简单理解为“格雷码最高位表示圈数”。格雷码各 bit 与二进制位之间存在编码关系因此满判断需要按格雷码序列的规律比较。十三、波形上应该怎么看先看二进制指针从 3 变成 4。源时钟域二进制指针 bin[2] _________/‾‾‾‾‾‾‾ bin[1] ‾‾‾‾‾‾‾‾‾\_______ bin[0] ‾‾‾‾‾‾‾‾‾\_______ 数值 011 - 100三个 bit 都在变化。由于实际路径延迟不同目标时钟采样时可能看到错误组合。再看对应的格雷码十进制 3 - 4 格雷码 010 - 110波形上只有一个 bit 变化gray[2] _________/‾‾‾‾‾‾‾ gray[1] ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ gray[0] _______________ 数值 010 - 110如果目标时钟边沿正好落在变化附近变化的那个 bit 可能进入亚稳态。但另外两个 bit 保持不变。经过同步器后目标时钟域看到的通常是旧值 010或者晚一两个周期看到新值 110对于 FIFO 来说指针晚同步几个周期通常是安全的。十四、为什么指针同步延迟不会让 FIFO 溢出两级同步器会引入延迟。例如读操作已经释放了一个存储位置但写时钟域可能还没有看到更新后的读指针。这时写时钟域可能仍然认为FIFO 是满的于是晚几个周期才撤销full。结果是暂时少写几个数据而不是覆盖尚未读取的数据。同样写端已经写入新数据后读时钟域可能还没有看到更新后的写指针。此时empty可能多保持几个周期。结果是暂时晚几个周期开始读取而不是读取无效数据。这种行为通常称为保守判断或悲观判断full 可能撤销得晚 empty 可能撤销得晚它牺牲少量延迟换取不溢出和不下溢。十五、快时钟域连续变化慢时钟域会不会出问题假设写时钟比读时钟快很多。在两个rclk边沿之间写指针可能增加多次000 - 001 - 011 - 010读时钟域可能没有采到中间状态而是直接看到000 - 010这两个采样值之间可能有多个 bit 不同。这并不代表格雷码失效。格雷码保证的是源时钟域每一次相邻计数变化只翻转一个 bit并不是保证目标时钟域连续两次采到的数值永远只差一个 bit慢时钟域跳过部分指针值是允许的。异步 FIFO 不需要让对面看到每一个中间指针只需要得到一个安全、可能稍微滞后的指针位置。十六、格雷码的使用条件格雷码并不是所有多位 CDC 信号的通用解决方案。要使用这种方法源数据必须满足相邻有效值每次只增加 1 或减少 1异步 FIFO 指针正好满足这个条件不操作保持不变 成功写入写指针加 1 成功读取读指针加 1下面这些情况不能直接套用 FIFO 指针的格雷码同步方法计数器一次跳过多个值 普通数据总线任意变化 地址总线任意跳转 多个字段组成的状态总线例如binary0011 - 1001即使分别转换成格雷码也不能保证两个状态之间只变化一个 bit。这种信号通常应考虑握手机制请求应答同步异步 FIFOToggle 同步其他专用 CDC 结构。十七、FIFO 深度为什么通常是 2 的整数次幂标准反射格雷码的完整循环长度是2^N异步 FIFO 中常见深度为4、8、16、32、64、128……这样二进制指针自然循环转换后的格雷码也能保持首尾连接时只变化一个 bit。例如 3 bit 格雷码000 001 011 010 110 111 101 100最后一个状态回到第一个状态100 - 000同样只变化一个 bit。如果直接截取其中一部分作为非 2 的整数次幂 FIFO计数器回绕时可能出现多个 bit 同时变化破坏格雷码跨域的前提。因此手写经典格雷码异步 FIFO 时通常要求FIFO_DEPTH 2^ADDR_WIDTH非 2 的整数次幂深度不是完全不能实现但空满判断、指针循环和 CDC 处理会更复杂不能直接照搬经典模板。十八、工程中还需要注意什么1. 格雷码必须在源时钟域寄存推荐结构binary next ↓ gray next ↓ 源时钟寄存器 ↓ 跨域同步器不要让带有组合毛刺的信号直接进入 CDC 同步器。2. 同步器应添加工具识别属性在 Vivado 中常见写法为(* ASYNC_REG TRUE *) reg [WIDTH-1:0] sync_ff1; (* ASYNC_REG TRUE *) reg [WIDTH-1:0] sync_ff2;该属性主要帮助实现工具识别同步器并优化同步触发器的布局。它不能代替正确的 RTL 结构。3. 复位释放也要注意时钟域工程中常见策略是异步拉低复位 同步释放复位写时钟域和读时钟域应分别生成各自同步释放的复位信号。不要假设一个异步复位在两个无关时钟域中能够完全同时、安全地释放。4. 不要只看功能仿真理想 RTL 仿真通常不会自动暴露亚稳态和真实路径偏斜问题。除了功能仿真还应检查CDC 报告 同步器识别结果 时序例外 跨域路径约束 复位跨域结构使用 Vivado 时可以重点查看report_cdc确认同步结构是否被正确识别。5. 手写 FIFO 前先评估是否应该使用厂商 IP如果项目目标是快速交付而不是学习异步 FIFO 原理可以优先使用Xilinx XPM_FIFO_ASYNC FIFO Generator Intel FIFO IP成熟 IP 通常已经处理格雷码指针空满判断同步器RAM 推断或实例化CDC 属性部分约束不同读模式数据计数和 almost full 等功能。但即使使用 IP也应该理解格雷码的作用否则遇到full、empty延迟或 CDC 报告时仍然很难定位问题。十九、常见错误总结错误一直接同步二进制指针sync_ff1 binary_pointer; sync_ff2 sync_ff1;问题二进制指针可能同时变化多个 bit 目标域可能采到错误组合错误二认为格雷码不需要同步器assign remote_pointer gray_pointer;问题格雷码仍然是异步信号 变化的那个 bit 仍然可能产生亚稳态错误三先跨域同步二进制再转格雷码错误结构binary pointer ↓ 多位同步器 ↓ binary to gray二进制指针在进入同步器之前已经存在多 bit 同时变化的问题。正确结构应是binary pointer ↓ binary to gray ↓ 源时钟寄存 ↓ 同步器错误四格雷码指针一次增加多个数例如wbin_next wbin 4;这种情况下前后两个格雷码可能变化多个 bit不能再使用“相邻状态只变化一位”的结论。错误五认为同步后的指针一定是最新值同步器一定会引入延迟。写时钟域看到的读指针、读时钟域看到的写指针本来就是滞后的。异步 FIFO 的空满逻辑必须允许这种滞后并保证结果偏向安全。二十、面试时可以怎么回答如果面试官问异步 FIFO 为什么使用格雷码可以这样回答异步 FIFO 的读写指针需要跨时钟域同步。如果直接同步二进制指针指针在进位时可能同时变化多个 bit例如011变成100目标时钟域可能因为各 bit 延迟和同步周期不同采到错误的组合值。格雷码相邻计数值只变化一个 bit可以把多位同时变化的问题限制为单 bit 变化。再配合两级同步器可以降低亚稳态传播风险使目标域通常只会看到旧指针或新指针。格雷码不能消除亚稳态也不能代替同步器。如果继续追问格雷码跨域后 full 和 empty 为什么会有延迟可以回答因为对端指针经过同步器后会滞后。异步 FIFO 的 full 和 empty 通常采用保守判断状态可能晚几个周期撤销但不会因此导致溢出或下溢。总结异步 FIFO 使用格雷码核心不是为了编码方便而是为了安全传递读写指针。需要记住以下几点二进制指针进位时可能同时变化多个 bit。多位二进制信号不能简单地逐位加两级同步器。格雷码相邻状态只变化一个 bit。格雷码仍可能产生亚稳态必须配合同步器。二进制指针用于加一和 RAM 寻址。格雷码指针用于跨时钟域。empty 在读时钟域产生full 在写时钟域产生。同步后的远端指针存在延迟空满状态通常采用保守判断。经典格雷码异步 FIFO 通常要求深度为 2 的整数次幂。格雷码只适合每次加一、减一或保持不变的计数类信号。一句话概括异步 FIFO 使用格雷码是为了让跨时钟域的指针每次最多只变化一个 bit再通过同步器降低亚稳态风险避免二进制指针多 bit 同时变化导致错误的空满判断。
返回列表