4、异步FIFO设计:格雷码指针、空满标志生成、深度计算与仿真

异步FIFO,这玩意儿在跨时钟域设计里,可以说是「兵家必争之地」。我做了这么多年验证,几乎每个多时钟域的项目都离不开它。说白了,它就是两个不同时钟域之间的数据缓冲池——一边写,一边读,互不干扰。

但问题来了:怎么判断这个池子满了?怎么判断它空了?指针怎么跨时钟域传递?深度选多少才合适?嗯,今天咱们就把这些硬骨头一块儿啃了。

4.1 格雷码指针:为什么非它不可?

先问个问题:二进制指针直接跨时钟域,行不行?

我告诉你,不行。为什么?因为二进制计数时,多位同时翻转太常见了。比如从 0111 跳到 1000,4个bit全变。跨时钟域采样时,可能采到 0000、0100、1000……任何中间值。这会导致空满判断彻底乱掉。

格雷码就解决了这个问题。它的特点是:相邻两个值之间,只有1个bit变化。你想想看,跨时钟域采样时,就算采样点刚好落在变化沿上,最多也就一个bit不确定。这个风险,比二进制小太多了。

核心要点:格雷码指针的唯一目的——降低跨时钟域传递时的亚稳态风险。

我个人习惯,在RTL里用二进制计数,然后转格雷码再跨时钟域。这样内部运算方便,外部传递安全。

二进制转格雷码的公式

gray = (binary >> 1) ^ binary;

就这么一行。格雷码转二进制稍微麻烦点,但咱们在空满判断时,其实不需要转回来——直接比较格雷码就行。

小技巧:我在项目中遇到过,有人把格雷码指针的位宽多留1位,用来区分满和空。比如深度为16的FIFO,指针用5位。最高位用来标记「绕了一圈」。这个做法很实用。

4.2 空满标志生成:最容易被坑的地方

空满标志,是异步FIFO的灵魂。判断错了,要么丢数据,要么读脏数据。我曾经因为一个空标志延迟了一个周期,导致整个系统死锁,查了三天才找到原因。

空标志

当读指针追上写指针时,FIFO为空。注意,这里说的「追上」,是在格雷码域比较。如果读指针和写指针完全相等,就是空。

assign empty = (rd_ptr_gray == wr_ptr_gray);

简单吧?但要注意:读指针是在读时钟域,写指针是在写时钟域。比较之前,必须把写指针同步到读时钟域。同步会引入延迟,所以空标志不会立刻反映写操作——这是正常的。

满标志

满标志稍微绕一点。当写指针比读指针多走了一圈(深度值),FIFO就满了。在格雷码域怎么判断?看最高位和次高位。

assign full = (wr_ptr_gray[WIDTH] != rd_ptr_sync[WIDTH]) &&
              (wr_ptr_gray[WIDTH-1:0] == rd_ptr_sync[WIDTH-1:0]);

什么意思?最高位不同,说明写指针多绕了一圈;低位相同,说明刚好差一个整圈。这就是满。

注意:满标志判断时,读指针需要同步到写时钟域。同步延迟会导致「假满」——明明还有空间,但满标志已经拉高了。这是安全的,只是性能会受一点影响。千万别反过来搞成「真满但没拉高」,那数据就丢了。

4.3 深度计算:选大了浪费,选小了完蛋

FIFO深度怎么算?很多人直接拍脑袋定个16、32。其实有公式的。

核心思路:在写使能连续有效的情况下,读使能也在工作,FIFO要能吸收掉最坏情况下的数据积压。

公式长这样:

depth > (wr_clk_freq / rd_clk_freq) * (rd_latency + 1) * burst_length

其中:

  • wr_clk_freq / rd_clk_freq:写时钟比读时钟快多少倍
  • rd_latency:读使能到数据输出的延迟(通常1~2周期)
  • burst_length:连续写入的最大数据量

举个例子:写时钟200MHz,读时钟100MHz,读延迟2周期,一次突发写16个数据。

depth > (200/100) * (2+1) * 16 = 2 * 3 * 16 = 96

所以深度至少96。我一般会再加20%余量,取128。2的幂次,格雷码也好处理。

我的经验:深度选2的幂次,不是为了好看,而是格雷码的循环特性刚好匹配。非2的幂次也能做,但格雷码的「相邻只变1位」特性会破坏,跨时钟域风险就大了。

4.4 仿真验证:别光看波形,要看边界

仿真异步FIFO,我一般会关注这几个点:

  1. 空满标志的时序:在写时钟域看满标志,在读时钟域看空标志。同步延迟是否合理?
  2. 指针跨时钟域:用$random注入随机延迟,模拟跨时钟域采样。看看有没有出现非法值。
  3. 边界压力:连续写满、连续读空、写一半读一半、读写速率极端差异。
  4. 数据完整性:写进去的数据,读出来是不是一模一样?这个用scoreboard比对最靠谱。

我记得有一次,仿真跑了一百万个随机事务都没问题,结果上板就挂了。后来发现是复位时序的问题——复位释放时,格雷码指针没有回到全0。从那以后,我每次都在验证计划里加一条:复位后检查指针初始值。

避坑指南:我曾经因为格雷码指针的位宽少定义了一位,导致满标志永远拉不高。仿真时没注意,因为数据量小。后来上板跑大数据量,直接溢出。所以,指针位宽 = log2(深度) + 1,这个+1千万别省。

4.5 小结

异步FIFO设计,说难不难,说简单也不简单。核心就三件事:

  • 格雷码指针——解决跨时钟域传递的安全问题
  • 空满标志——同步比较,注意延迟和位宽
  • 深度计算——根据读写速率和突发长度,留足余量

仿真时,别只盯着正常流程。边界条件、复位、随机延迟,这些才是暴露问题的关键。嗯,今天就聊到这儿。下一章咱们聊聊异步FIFO的另一种实现——双口RAM + 握手协议,那个又是另一番天地了。