ARTICLE DETAIL

资讯详情

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

异步FIFO跨时钟域为何用格雷码?原理、实现与工程踩坑全解析

异步FIFO跨时钟域为何用格雷码?原理、实现与工程踩坑全解析 异步FIFO做了这么多年见过不少人在跨时钟域上栽跟头。格雷码这个知识点理论书上三句话能讲完“用格雷码可以降低亚稳态风险每次只有一位变化空满判断也方便。”但真到了调试现场为什么二进制码一跨时钟域就疯掉为什么格雷码能抗住空满判断那套“高两位取反”到底怎么来的很多人是一头雾水的。这篇文章就把这几个问题彻底拆开结合我实际调过的FIFO案例把格雷码从原理到代码、从设计到踩坑全部捋一遍。适合正在学Verilog的入门者也适合想搞懂异步FIFO内部逻辑的FPGA工程师。1. 异步FIFO到底在解决什么问题1.1 两个时钟域的“时差战争”异步FIFO最常见的应用场景是数据从一个时钟域跨越到另一个时钟域。比如ADC采集端是100MHz的时钟后端处理逻辑是75MHz的时钟两边同时工作但相位、频率完全没关系。数据不可能直接拿一根线连过去就算完因为读方不知道写方什么时候把数据准备好了写方也不知道读方什么时候把数据取走了。FIFO在这里干的事情就是充当一个“中转仓库”。写方只管往仓库里放货读方只管从仓库里取货。仓库剩多少货、满了没有、空了没有这个状态必须实时、可靠地告诉两端。然而问题来了判断空满需要同时知道写指针和读指针的位置而这两个指针分别属于不同的时钟域。写时钟域看自己的写指针没问题但要看读指针的时候读指针是读时钟域的信号不能直接拿过来就用。这就是异步FIFO最核心的矛盾两个独立时钟域之间如何安全地传递多比特的指针信息。1.2 指针为什么是异步FIFO的“七寸”你可能会想跨时钟域传递信号有什么难的直接打两拍同步不就行了吗对于单比特信号确实是这样。但是FIFO的指针是好几根线二进制计数器的指针从0111变成1000的时候四根线全部翻转。如果直接把这四根线送到另一个时钟域去打拍问题就大了。问题不在打拍本身而在“打的这一刻”。读时钟域的寄存器在时钟上升沿对写指针采样采样的一瞬间写指针正处在所有位都翻转的中间状态。由于布线延迟、寄存器建立保持时间不同四个触发器采样到的结果很可能不是同一个时刻的数值。有的位采到了旧值有的位采到了新值组合出来一个从未在正常计数序列里出现过的二进制数。这意味着读方看到的写指针可能比实际超前了也可能比实际落后了而且这个错误不是可预测的固定延迟而是随机的。指针一旦出错空满判断就会跟着出错。满信号提前拉高会导致写方停写吞吐量下降满信号延迟拉高会导致数据覆盖这是致命的。空信号判断出错读方可能读到尚未写入的垃圾数据。所以指针的跨域传递直接决定了FIFO能不能可靠工作。1.3 跨时钟域同步的基本规则再说说跨时钟域同步的通用规则它不光是FIFO的问题也是所有CDC设计的底层逻辑。一个信号从源时钟域出来要进入目的时钟域标准的处理方式就是“打两拍”也就是用两级同步寄存器来接。第一级寄存器采样到源信号输出可能处于亚稳态但经过一个时钟周期的稳定时间信号大概率能收敛到确定的电平第二级寄存器再采样第一级的输出把亚稳态进一步抑制掉。这个处理能解决“亚稳态传播”的问题但不能解决“信号本身变化太快”的问题。如果源信号在一个目的时钟周期内来了两次变化两级同步器也会丢事件。所以在FIFO里跨域传输的信号必须满足一个前提信号在源时钟域必须稳定足够长的时间最好是至少稳定一个源时钟周期这样目的时钟域采样时才能采到确定的电平。这里就埋下了一个伏笔二进制指针在跳变时多个位同时翻转即使满足了“稳定时间”要求多根线在目的时钟域采样时仍然会因为位与位之间的微小偏移导致采样结果不一致。这个问题靠打两拍解决不了必须换一种编码方式。格雷码就是为此而生的。2. 二进制码为什么会在跨时钟域时翻车2.1 多比特翻转的“打包采样”问题把道理说得再直白一点。假设有四位二进制指针当前值是0111下一个时钟周期变成1000。在理想的数字电路里这个过程是瞬时发生的所有位在同一时刻翻转。但真实的电路不是这样的每根信号线的布线长度不同、驱动能力不同、负载不同翻转时间可能有几百皮秒甚至几纳秒的差异。对于高速设计而言这几百皮秒就可能造成大问题。读时钟域的采样沿到来时如果刚好落在整个翻转窗口中间四个触发器看到的情况分别是数据线0已经变成1了数据线1还在1数据线2还在1数据线3也还在0。这个组合是0001还是0101都是有可能的反正不是正常的1000也不是之前的0111。关键是它完全不可预测。有人会问那我把源时钟域的信号先寄存一拍等所有位都稳定了再送往目的时钟域不就行了吗这是治标不治本。寄存器消除了组合逻辑毛刺但无法消除多根线各自翻转时间不同的物理差异。只要这些线是分立的在目的时钟域采样时就可能采到“翻了一半”的状态。而且这种状态会在两级同步器上被“锁存”下来变成数据通路里一个真真实实的错误值后面查都查不出来。2.2 亚稳态可不是小事亚稳态这个词学数字电路的人都知道但很多人意识不到它的破坏力有多大。当触发器的数据输入在时钟沿附近变化违反了建立时间或保持时间的要求触发器输出就会进入一种“中间状态”——既不是0也不是1电压徘徊在阈值附近。理论上这个状态会随着时间推移收敛到一个确定电平但收敛时间是不确定的可能几个纳秒也可能几十个纳秒。在同步器设计中我们通过打两拍来“等”这个收敛过程完成。第一级同步器足够长的时间后第二级采样到的通常是稳定的值。所以从概率上说打两拍能让亚稳态导致的崩溃概率降低到极低水平。但换成多比特的二进制指针时麻烦在于不是一比特亚稳态而是每一根线都可能在某些极端条件下进入亚稳态而且它们各自收敛到的结果——大概率是随机的0或1——组合出来的值完全错误。这就像一群人传话每个人都有可能听错但一个人听错只影响一个词如果所有人都同时听错而且错得五花八门最后合成的消息就是天方夜谭。二进制指针跨时钟域面临的正是这种风险。2.3 用一个例子展示二进制计数器的最大痛点举一个具体的例子。写时钟域写指针从7变成8二进制编码是0111变成1000四位全部翻转。读时钟域打两拍后第一级寄存器采样到的可能是0100第二级看到的是操过一拍后的0100。这个0100既不是7也不是8它代表一个完全不存在的位置。如果把这种错误指针用于满判断极端情况下读方认为写指针到了位置4但实际写指针已经在8于是判定“未满”允许写方继续写。此时FIFO实际深度可能已经不够了继续写入就覆盖了尚未读取的数据。如果用于空判断可能读方认为读指针落后于写指针很远从而判定“非空”但实际上数据还没写进来读方读出的全是无效数据。这两种错误都会让FIFO变成一坨废铁。所以异步FIFO设计里有一条铁律绝对不能把非编码的二进制多比特信号直接跨时钟域。必须换成一种“每次只动一位”的编码让采样结果在任何情况下都只会是合法值之一。3. 格雷码是如何解决同步难题的3.1 格雷码的核心优势每次只变一位格雷码的特点所有学过的人都能背出来任意两个相邻值之间只有一位不同。0到1是0000变成0001只有最低位变1到2是0001变成0011只有第二位变2到3是0011变成0010只有最低位变3到4是0010变成0110只有第二位变。整个序列走完每步都只有一位翻转。这意味着什么当指针从7变成8的时候如果8在这个序列里那么这两个值的编码只有一位不同。目的时钟域采样时最坏情况也就是把这一位采成了旧值或者采成了新值它绝对不会组合出第三个非法值。如果采到旧值只是说明同步器看到的是上一拍的状态如果采到新值说明同步器正确采到了当前状态。两种结果都是合法的。用专业一点的术语说格雷码把“多比特同时变化导致采样不确定”的问题转化成了“单比特变化导致最多一拍延迟”的问题。前者是错误后者是延迟。延迟可以接受错误不能接受。3.2 为什么同步前先转格雷码而不是同步后再转有人问我能不能把二进制指针同步过去之后再转成格雷码做判断答案是绝对不行。因为二进制到格雷码的转换需要看所有位转换逻辑的输入是已经同步过来的多比特信号这个多比特信号在同步过程中已经可能被“污染”了。转换电路只能对错误值再做一次映射得到的还是错误值而且可能把错误值映射成一个合法的格雷码反而更难发现。正确的做法是在源时钟域先把二进制指针转成格雷码再把这组格雷码寄存一拍最后送往目的时钟域打两拍。到目的时钟域之后用同步后的格雷码指针和本地指针做逻辑判断。也就是说转换电路必须放在源时钟域而且转换后要寄存保证从寄存器输出的就是稳定、无毛刺的格雷码信号。这一点在实际工程中特别容易忽略。有些同学图省事直接在组合逻辑里做转换结果转换电路的输入来自RAM地址计数器计数器翻转过程中有组合逻辑毛刺转换后的格雷码也会有毛刺。毛刺送到两级同步器可能在时钟沿附近来回抖动采样结果同样不可靠。所以务必记住跨时钟域的发送端信号必须经过寄存器再发出去。3.3 格雷码与二进制互相转换的实现细节格雷码和二进制码的转换公式是数字设计里最常用的几个公式之一。二进制转格雷码最简单的方式是assign gray (bin 1) ^ bin;格雷码转二进制标准做法是逐位异或integer i; always (*) begin bin[WIDTH-1] gray[WIDTH-1]; for (i WIDTH-2; i 0; i i - 1) begin bin[i] bin[i1] ^ gray[i]; end end后面这段代码在纯组合逻辑综合时会形成一条异或门链从最高位一直往下传递。对于FIFO内部用于寻址RAM的地址总线这个转换链的延迟会影响地址到达时间设计时要格外注意。好在我们通常只在本地时钟域使用转换后的二进制地址路径上没有跨域问题而且地址建立时间一般足够只要做一次时序分析确认没有setup违例即可。还有一个细节转换公式里的格雷码最好是“标准二进制反射格雷码”也就是每相邻两个数恰好差一位的那种。异步FIFO指针用到的是这种。网上有些变种格雷码用于状态机编码或最小化毛刺功耗性质略有不同不能直接套用空满判断公式。4. 空满判断格雷码真正显本事的地方4.1 空满判断为什么不能只靠“相等”先思考一个最简单的方案如果读写指针完全相等就判定为空或满。但问题是一个深度为4的FIFO当读指针追上了写指针指针相等——这时到底是空还是满如果FIFO从来没写过数据读指针当然等于写指针这是空如果FIFO写了4个数据还没读写指针绕了一圈又跟读指针相等了这时是满。光看“相不相等”无法区分这两种情况。解决思路是给指针扩展一位。比如深度为4的FIFO本来只需要2位地址我们给指针增加一位变成3位。这样当写指针绕完一圈回到读指针的位置时虽然低2位相同但最高位不同可能一个是1一个是0就可以判断出是满而不是空。把这个规则推广到格雷码上就是经典的判空判满条件读指针格雷码和写指针格雷码完全相等判空。读指针格雷码的高两位等于写指针格雷码高两位取反其余位完全相同判满。为什么高两位要取反而不是只取最高位原因在于格雷码的回绕特性。以深度4的FIFO为例3位格雷码序列是000001011010110111101100。从地址3010回到地址0110时最高位从0变成1同时低一位从1变成0——有两位变化。如果只用最高位来判断满在回绕瞬间可能需要等两位格雷码都稳定反而多出一拍不确定性。而用高两位来判断当写指针到达“读指针对应位置的回绕一格”时高两位的匹配条件正好成立逻辑上更清晰、更可靠。这是工程实践沉淀下来的经验。4.2 扩展MSB的设计细节扩展MSB这个操作在二进制指针上很简单就是计数器从0一直加到2^N - 1然后再加到2^N即多了一位。转到格雷码后格雷码的序列会从二进制最高位翻转开始又循环一遍整个序列长度是2^(N1)。但要注意实际FIFO的地址寻址只用到低N位高位的额外那一位纯粹是用来区分“绕了几圈”的。设计计数器时常用的是二进制计数然后即时转换成格雷码。因为格雷码本身不是天然支持“加1”操作的你写一个gray_counter的always块每次gray gray 1那就大错特错了它走的是二进制序列不是格雷码序列。所以实践中普遍做法是维护二进制计数器再通过组合逻辑生成格雷码指针。虽然多消耗一点组合逻辑但代码清晰、行为可控。还有一点必须在代码里体现同步器需要分别放在两个方向。写时钟域要把写指针的格雷码打两拍后送到读时钟域用来和本地读指针判断空读时钟域要把读指针的格雷码打两拍后送到写时钟域用来和本地写指针判断满。不能用同一个同步后信号同时判断空满因为同步延迟不同逻辑上也不对称。4.3 用格雷码判断空满的Verilog实现给一个精简但完整的判空判满逻辑示例。假设FIFO深度为2^N指针位宽为N1内部使用二进制计数器对外用格雷码同步// 写时钟域 reg [N:0] w_bin; wire [N:0] w_gray (w_bin 1) ^ w_bin; // 读指针格雷码已同步到写时钟域 reg [N:0] r_gray_sync1, r_gray_sync2; always (posedge wclk or negedge wrst_n) begin if (!wrst_n) begin r_gray_sync1 0; r_gray_sync2 0; end else begin r_gray_sync1 r_gray; r_gray_sync2 r_gray_sync1; end end // 满判断高两位取反其余相同 wire full (w_gray[N] ! r_gray_sync2[N]) (w_gray[N-1] ! r_gray_sync2[N-1]) (w_gray[N-2:0] r_gray_sync2[N-2:0]);读者可能会想为什么满判断里用了“当前写指针”和“同步后的读指针”同步后的读指针已经过了一两拍会不会导致满信号提前或滞后实际上这个设计是偏保守的读指针同步过来有延迟写方看到的读指针是“过去”的位置所以它只会“以为还没满”多写一两个数据。但只要预留了足够的FIFO深度余量这个“多写”恰好被保守的逻辑挡住因为写方看到的读指针比真实位置偏小实际FIFO还没满的时候它可能就已经把full拉高了。这个方向是安全的。空判断相反读方看到的是“过去”的写指针所以只会“以为还没写进来”从而延迟读空不会读空。两个方向都是安全方向这正是格雷码两级同步方案能成为异步FIFO标准做法的原因。5. 实际工程中那些容易踩的坑5.1 同步器打两拍就万事大吉吗很多初学者以为打两拍就能解决一切跨域问题这是最大的误区。打两拍只能解决亚稳态传播的问题不能解决信号逻辑上的错误。如果源信号的建立时间不满足或者源信号本身不稳定第一级同步器采样到的值可能根本就不是源时钟域想要表达的值。所以前面反复强调跨域信号必须是从寄存器同步输出、每拍都稳定的信号。另一个容易忽略的问题是两级同步器本身会引入延迟。写侧用同步后的读指针判满意味着满信号拉高的时刻至少比真实满时刻晚两个目的时钟周期。如果你的FIFO深度非常小比如深度为4甚至为2这两个周期的延迟可能让FIFO在实际写满之前就已经被写爆了。解决办法不是不用格雷码而是要给FIFO增加设计余量或者在高频场景下把判断提前一拍做用组合逻辑提前计算next_w_bin和next_r_gray_sync2的关系。工程上还有一招在综合约束里把这两级同步寄存器标记为异步寄存器防止综合工具将它们合并、重定时或优化掉。Xilinx环境下用(* ASYNC_REG TRUE *)属性Altera环境下设置set_false_path并保证同步器寄存器不被优化。这个细节如果漏掉工具可能会把同步器重排导致设计中实际没有两级同步Race Condition会在批量板卡上随机爆发。5.2 格雷码不能用来做大小比较格雷码最大的特点是相邻编码只有一位变化但它的代价是“编码顺序”和“数值大小”完全没有单调对应关系。比如三位格雷码0000、0011、0112、0103、1104、1115、1016、1007。数值2对应的格雷码是011数值3对应的格雷码是010如果你把010当作2来比较发现它比011小但真正的二进制2确实小于3可这里的010是格雷码不是二进制。直接拿格雷码做大于小于判断结果完全是错的。这带来的工程教训是凡是需要大小比较的场景必须先转回二进制再比较或者用专门的判空判满逻辑而不是幻想“格雷码本身足够聪明”。FIFO的空满判断之所以能用格雷码直接做是因为它用的是“异或高位判断”不是在比较大小而是在检测“读指针追没追上写指针”的回绕关系。5.3 复位与初值问题异步FIFO的复位信号处理也是个经典坑。两个时钟域各自要有自己的复位同步释放电路确保各自域的同步器寄存器和计数器能够同时清零。如果在写时钟域使用的复位信号是读时钟域产生的没有经过同步就可能出现某个寄存器没有复位成功上电后指针处于非法状态。更隐蔽的问题是复位释放的时刻。如果复位释放刚好发生在跨域同步器的采样沿附近同步器第一级寄存器可能采到“复位释放后的第一个合法值”或者“复位释放前的复位电平”两级同步器就会出现在后一拍才跟上来的现象。这通常不影响功能因为本来就是异步复位、同步释放但有一点必须保证复位释放后计数器必须从全0开始走而不是从某个随机值走。在实际代码里我习惯把复位信号同时接到跨域同步器寄存器的复位端这样上电后同步器输出是确定的0指针也是确定的0空满判断逻辑在首个时钟周期就能得到“空”信号这是最安全的初始状态。5.4 综合工具与跨时钟域约束异步时钟域之间没有固定的相位关系综合工具默认情况下会把它当成同步路径来处理试图让跨域路径满足setup和hold约束。这样做的结果不是功能出错而是时序收敛困难或者工具报告一堆冗余的时序违例干扰正常的时序分析。正确的做法是在SDC里声明两个时钟域不同步让工具放宽这些路径的时序把资源留给真正需要约束的路径。常见的SDC写法是set_clock_groups -asynchronous \ -group {clk_wr} \ -group {clk_rd}然后对同步器内部的两级寄存器设置set_false_path。注意false path设的是“从源时钟域寄存器到第一级同步器”这一段以及“第一级到第二级”这一段的跨域路径。第一级到第二级虽然在目的时钟域内但它本来就是为了处理亚稳态不需要满足正常的建立保持时间所以也要设成false path。如果漏设综合工具会认为这两级寄存器之间有时序要求结果可能把它优化掉或拉长延迟。在FPGA上做验证时还能用属性强制保留同步器寄存器。Xilinx的(* ASYNC_REG TRUE *)就能保证综合器将它放在同一片的相邻触发器上缩短布线延迟进一步提升抗亚稳态能力。别小看这个属性高频场景下它可能就是让FIFO稳定工作的最后一根稻草。6. 常见问题速查表FAQ问题原因正确处理二进制指针直接打两拍跨域偶发数据覆盖多比特同时翻转被采成非法值指针先转格雷码再同步格雷码跨域后出现“乱码”转换逻辑没寄存组合毛刺进入同步器在源时钟域先转格雷码并寄存一拍空满判断有时判错直接用相等判断空满未扩展MSB扩展MSB用高两位取反判满完全相等判空full信号迟迟不拉高读指针同步有延迟判满滞后预留FIFO深度余量或提前一拍计算综合后同步寄存器被优化掉未设置ASYNC_REG属性和false path跨域路径设置为异步加保留属性手动把格雷码和二进制混在一起比较大小格雷码不满足数值单调性先转回二进制再比较或用专用判空满公式FIFO深度不是2的幂格雷码序列很别扭格雷码回绕特性依赖2^n序列把实际可用深度设计成2^n或让指针位宽多一位复位瞬间状态不确定复位信号跨域没有同步释放每个时钟域做异步复位同步释放仿真时full/empty信号比预期晚两拍同步器本来就引入2拍延迟仿真时把同步延迟纳入预期不要视为bug高频下偶尔读出了脏数据格雷码信号在源时钟域翻转太快目的域采样窗口抓不到使用源时钟域寄存输出保证信号在目的域采样沿可见这张表基本涵盖了我在项目中遇到过的绝大多数异步FIFO问题。里面每一条我都能对应到一个真实的Debug经历。尤其是“格雷码不能比较大小”这一条当年有个同事图省事把同步后的格雷码直接减一减来判断队列长度结果在边界条件下全线崩溃查了两天才定位到教训十分深刻。7. 关于格雷码的最后一层理解最后再从本质层面把格雷码这件事说透。为什么它能在跨时钟域下成为事实标准因为它把“不确定性”从“错误”变成了“延迟”。二进制多比特同时变化时采样结果存在组合爆炸的可能各种非法值都可能冒出来系统无从判断哪个是真相。格雷码每次只动一位跨时钟域采样最多发生“采到旧值”或“采到新值”两种结果这两种结果都对应真实存在的状态系统即便延迟一拍也永远不会跑到某个不存在的状态上去。这个思想其实超越FIFO本身。任何需要跨时钟域传输的多比特信号如果状态之间能设计成依次翻转的模式都可以借用格雷码的思路来降级风险。比如状态机跨域、计数器的跨域都能套用同样的处理逻辑。我后来做异步握手协议、做异步比较器都保留了这个习惯能单比特翻转就别多比特翻转能编码就别裸传。再补充一个实操中很值得注意的小技巧当FIFO深度很小比如4或8且两个时钟频率比较接近时建议在RTL仿真阶段就用随机化时钟相位和频率抖动来测FIFO不要只在固定频率比下测。因为跨域问题本质是概率问题固定相位差可能恰好避开了所有亚稳态窗口跑一天都测不出来随机化之后稳定性差的FIFO很快就能暴露问题。这个技巧帮我提前抓出过至少两版有隐患的IP核。关于异步FIFO为什么用格雷码技术上的答案就这么多。实际调通一套异步FIFO之后你会对跨时钟域这件事有真正的敬畏感——它不像组合逻辑那样有确定性输出而是一个从“可能出错”到“几乎不会出错”的概率工程。格雷码做的不是消除概率而是把概率从“灾难性错误”迁移到“可接受的延迟”上。用一句话总结我个人的感受格雷码在异步FIFO里不是用来让电路“更快”的而是用来让电路“即使错了也能错得很无害”。
返回列表