4. FIFO空满判断逻辑
好,咱们今天聊聊FIFO里最核心、也最容易出bug的地方——空满判断。说实话,我见过太多工程师在这上面翻车了。我自己刚入行那会儿,也踩过格雷码同步的坑,差点导致芯片功能失效。嗯,咱们一个一个来拆解。
4.1 同步FIFO的空满判断
同步FIFO,读写时钟是同一个。判断空满,说白了就是比较读写指针。
空标志:读指针追上写指针,说明没数据可读了。
满标志:写指针追上读指针,说明没空间可写了。
但有个问题——指针回绕。你想想看,如果FIFO深度是8,指针从0走到7,再走就回到0了。这时候怎么区分空和满?
核心技巧:多用一个bit位
比如深度8的FIFO,指针用4位表示。最高位用来区分回绕次数。
- 读写指针最高位相同 → 在同一圈 → 指针相等就是空
- 读写指针最高位不同 → 写指针多绕了一圈 → 指针相等就是满
// 同步FIFO空满判断示例
wire [3:0] wr_ptr, rd_ptr; // 4位指针,最高位是回绕标志
wire empty = (wr_ptr == rd_ptr);
wire full = (wr_ptr[3] != rd_ptr[3]) &&
(wr_ptr[2:0] == rd_ptr[2:0]);
我在项目中遇到过,有人直接用3位指针判断空满,结果深度8的FIFO,明明还有空间却报满。原因就是没处理回绕。这个坑,踩一次就记住了。
4.2 异步FIFO的空满判断
异步FIFO,读写时钟不同。这时候不能直接比较指针,因为跨时钟域会出问题。
解决方案:格雷码 + 同步器
为什么用格雷码?因为格雷码相邻值只变化1位。跨时钟域同步时,即使出现亚稳态,最多错1位。二进制码就不行,比如从0111变到1000,4位同时跳变,同步后可能得到任何值。
我的习惯做法:
- 写指针转格雷码 → 同步到读时钟域 → 判断空
- 读指针转格雷码 → 同步到写时钟域 → 判断满
// 二进制转格雷码
wire [N:0] gray_wr = (bin_wr >> 1) ^ bin_wr;
// 格雷码转二进制(用于比较)
wire [N:0] bin_rd;
assign bin_rd[N] = gray_rd[N];
genvar i;
generate
for(i = N-1; i >= 0; i = i-1) begin
assign bin_rd[i] = bin_rd[i+1] ^ gray_rd[i];
end
endgenerate
这里有个细节——格雷码判断空满。空标志很简单,同步后的读写格雷码完全相等就是空。满标志就有点讲究了。
我曾经踩过的坑:
格雷码判断满,不能直接比较最高位不同。因为格雷码的最高位和次高位有特殊关系。正确做法是:
- 写指针格雷码的最高位 ≠ 读指针格雷码的最高位
- 写指针格雷码的次高位 ≠ 读指针格雷码的次高位
- 其余位相等
说白了,就是写指针比读指针多绕了一圈,而且是在格雷码空间里判断的。
4.3 指针回绕处理
指针回绕,是FIFO设计的底层逻辑。不管是同步还是异步,都得处理。
回绕的本质:指针从最大值回到0,同时回绕标志位翻转。
| FIFO深度 | 指针位数 | 回绕标志位 | 最大地址 |
|---|---|---|---|
| 8 | 4 (3位地址+1位回绕) | bit[3] | 7 (3'b111) |
| 16 | 5 (4位地址+1位回绕) | bit[4] | 15 (4'b1111) |
| 32 | 6 (5位地址+1位回绕) | bit[5] | 31 (5'b11111) |
你想想看,如果深度不是2的幂次方怎么办?比如深度12。这时候就不能用简单的回绕标志了。我建议的做法是:
- 用计数器记录写入/读取次数
- 比较计数器的差值来判断空满
- 或者用两个指针,一个指向当前地址,一个指向边界
不过说实话,大部分场景下FIFO深度都是2的幂次方。这样硬件实现最简单,面积也最小。
4.4 知识体系总览
下面这张图,是我自己总结的FIFO空满判断的核心逻辑。你看一眼就能明白整体脉络。
嗯,这张图把同步和异步FIFO的核心区别都标出来了。你写代码的时候,对着这张图检查,基本不会漏掉关键点。
避坑指南:
- 同步FIFO:别忘了指针位宽要比地址多1位
- 异步FIFO:格雷码转二进制时,小心组合逻辑环路
- 深度非2的幂次方:别硬套回绕标志,用计数器更靠谱
好了,空满判断这块就聊到这儿。记住一句话:空满判断的本质,就是比较指针的圈数和位置。圈数用回绕标志,位置用地址位。同步FIFO直接比,异步FIFO先转格雷码再比。就这么简单。
可可精品资料库