4、同步FIFO空满判断:指针相等判空、指针差值为深度判满、格雷码指针判空满
同步FIFO的设计,说白了就是两个核心问题:数据怎么存和什么时候能读能写。上一章我们聊了存储体和读写指针,这一章咱们重点攻克第二个问题——空满判断。
空满判断要是搞错了,后果很严重。读空了你还在读,拿到的就是垃圾数据;写满了你还往里写,旧数据就被覆盖了。我在项目中就见过一次这样的bug,仿真跑得好好的,上板就随机挂,查了三天才发现是空满标志出了边界情况。
4.1 指针相等判空——最直观的方法
先说说最简单的判空逻辑。
当写指针和读指针指向同一个位置时,FIFO就是空的。这个很好理解吧?你想想看,写指针追上了读指针,说明所有写进去的数据都已经被读走了。
判空条件:写指针 == 读指针
代码实现也很直接:
assign empty = (wr_ptr == rd_ptr);
嗯,这里要注意一个细节。复位之后,两个指针都指向0,此时FIFO确实是空的。所以复位后empty信号拉高,这个行为是对的。
我个人习惯在判空逻辑里再加一个同步处理,避免跨时钟域的问题。不过同步FIFO的读写时钟是同一个,所以直接比较就行,不用额外操心。
4.2 指针差值为深度判满——小心边界
判满比判空稍微复杂一点。
最朴素的想法是:当写指针比读指针多走了N步(N是FIFO深度),那就满了。用公式表达就是:
判满条件:写指针 - 读指针 == FIFO深度
但这里有个坑——指针是会回绕的。如果FIFO深度是16,写指针从15写到0,读指针还在5,这时候差值怎么算?
我刚开始做设计时就踩过这个坑。直接做减法,15 - 5 = 10,不对啊,实际上写指针已经绕了一圈,差值应该是16 + 0 - 5 = 11。
所以正确的做法是:
assign full = ((wr_ptr - rd_ptr) == DEPTH);
前提是你的指针位宽比深度多一位。比如深度16,指针就用5位。这样减法自然就能处理回绕的情况。
注意:指针位宽必须比深度多1位!否则无法区分「全空」和「全满」——这两种情况下指针数值都可能相等。
4.3 格雷码指针判空满——跨时钟域的利器
上面说的两种方法,在同步FIFO里完全够用。但如果你以后要做异步FIFO(跨时钟域),就得用格雷码了。
格雷码有个特性:相邻两个数之间只有1位不同。这个特性在跨时钟域同步时特别有用——即使采样出现亚稳态,最多也就错一个bit,不会出现大范围错误。
格雷码判空和二进制一样:
assign empty = (gray_wr_ptr == gray_rd_ptr);
判满就有点技巧了。格雷码不能直接做减法,得用另一种方式:
格雷码判满条件:写指针和读指针的最高两位相反,其余位相同。
举个例子,深度8的FIFO,指针用4位格雷码:
- 写指针:1_100(格雷码,对应二进制1_111)
- 读指针:0_100(格雷码,对应二进制0_111)
最高位不同,次高位不同,其余位相同——这就是满了。
代码实现:
assign full = (gray_wr_ptr[WIDTH-1] != gray_rd_ptr[WIDTH-1]) &&
(gray_wr_ptr[WIDTH-2] != gray_rd_ptr[WIDTH-2]) &&
(gray_wr_ptr[WIDTH-3:0] == gray_rd_ptr[WIDTH-3:0]);
我曾经在一个高速数据采集项目里用过格雷码指针。当时时钟频率跑到500MHz,二进制指针的跨时钟域同步老是出问题,换成格雷码之后,一次搞定。
4.4 三种方法对比
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 指针相等判空 | 同步FIFO | 简单、直观 | 只能判空 |
| 指针差值判满 | 同步FIFO | 逻辑清晰 | 需要额外位宽 |
| 格雷码判空满 | 异步FIFO | 跨时钟域安全 | 判满逻辑稍复杂 |
4.5 知识体系总览
下面这张图帮你理清同步FIFO空满判断的完整逻辑:
4.6 避坑指南
最后分享几个我踩过的坑:
- 指针位宽不够:深度16的FIFO,指针只用4位,结果满标志永远拉不高。后来发现是回绕后差值算错了。
- 格雷码转换时机:格雷码要在写入FIFO之前就转换好,不能等到跨时钟域同步时再转。否则中间状态会出问题。
- 空满同时为高:正常情况下空和满不会同时为高。如果仿真中出现这种情况,八成是指针逻辑写错了。
小技巧:调试空满标志时,可以在仿真波形里同时拉出wr_ptr和rd_ptr,手动算一下差值,跟空满信号对比。这个方法帮我抓出过好几个隐蔽的bug。
好了,同步FIFO的空满判断就聊到这儿。说白了就是三种方法,对应不同的场景。你只要记住:同步用二进制,异步用格雷码,指针位宽多留一位,基本就不会出大问题。