4、异步FIFO设计:格雷码指针、空满标志生成、深度计算与仿真
异步FIFO,这玩意儿在跨时钟域设计里,可以说是「兵家必争之地」。我做了这么多年验证,几乎每个多时钟域的项目都离不开它。说白了,它就是两个不同时钟域之间的数据缓冲池——一边写,一边读,互不干扰。
但问题来了:怎么判断这个池子满了?怎么判断它空了?指针怎么跨时钟域传递?深度选多少才合适?嗯,今天咱们就把这些硬骨头一块儿啃了。
4.1 格雷码指针:为什么非它不可?
先问个问题:二进制指针直接跨时钟域,行不行?
我告诉你,不行。为什么?因为二进制计数时,多位同时翻转太常见了。比如从 0111 跳到 1000,4个bit全变。跨时钟域采样时,可能采到 0000、0100、1000……任何中间值。这会导致空满判断彻底乱掉。
格雷码就解决了这个问题。它的特点是:相邻两个值之间,只有1个bit变化。你想想看,跨时钟域采样时,就算采样点刚好落在变化沿上,最多也就一个bit不确定。这个风险,比二进制小太多了。
我个人习惯,在RTL里用二进制计数,然后转格雷码再跨时钟域。这样内部运算方便,外部传递安全。
二进制转格雷码的公式
gray = (binary >> 1) ^ binary;
就这么一行。格雷码转二进制稍微麻烦点,但咱们在空满判断时,其实不需要转回来——直接比较格雷码就行。
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的幂次,格雷码也好处理。
4.4 仿真验证:别光看波形,要看边界
仿真异步FIFO,我一般会关注这几个点:
- 空满标志的时序:在写时钟域看满标志,在读时钟域看空标志。同步延迟是否合理?
- 指针跨时钟域:用$random注入随机延迟,模拟跨时钟域采样。看看有没有出现非法值。
- 边界压力:连续写满、连续读空、写一半读一半、读写速率极端差异。
- 数据完整性:写进去的数据,读出来是不是一模一样?这个用scoreboard比对最靠谱。
我记得有一次,仿真跑了一百万个随机事务都没问题,结果上板就挂了。后来发现是复位时序的问题——复位释放时,格雷码指针没有回到全0。从那以后,我每次都在验证计划里加一条:复位后检查指针初始值。
4.5 小结
异步FIFO设计,说难不难,说简单也不简单。核心就三件事:
- 格雷码指针——解决跨时钟域传递的安全问题
- 空满标志——同步比较,注意延迟和位宽
- 深度计算——根据读写速率和突发长度,留足余量
仿真时,别只盯着正常流程。边界条件、复位、随机延迟,这些才是暴露问题的关键。嗯,今天就聊到这儿。下一章咱们聊聊异步FIFO的另一种实现——双口RAM + 握手协议,那个又是另一番天地了。