3、空满判断的黄金法则:读指针追上写指针为空,写指针追上读指针为满,多一位标志位的设计思想
做FIFO设计,最核心的问题是什么?
我个人觉得,就是空满判断。你想想看,如果空满都判断错了,那数据要么读丢了,要么写覆盖了,整个系统就乱套了。我在刚入行那会儿,就因为这个吃过亏——一个异步FIFO的仿真怎么跑都过不了,查了三天,最后发现是空满标志的逻辑写反了。嗯,从那以后,我对这块就特别较真。
3.1 空满判断的本质:追与被追
先说个最直观的理解方式。你把FIFO想象成一个环形跑道,写指针在前面跑,读指针在后面追。
- 空:读指针追上了写指针。说明读已经把写的数据全读走了,FIFO里没东西了。
- 满:写指针追上了读指针。说明写已经把整个FIFO写满了,读还没来得及拿走。
说白了,就是「谁追上谁」的问题。读追上写是空,写追上读是满。这个逻辑很朴素,但实际实现时有个大坑——你怎么区分「空」和「满」?
核心矛盾:当两个指针相等时,到底是空还是满?
你想想看,如果指针只有N位,那写指针绕了一圈回来,和读指针碰上了,这时候你没法判断是「刚追上(空)」还是「绕了一圈追上(满)」。这就是问题的根源。
3.2 多一位标志位的设计思想
怎么解决?我习惯的做法是:给指针多加一位。
举个例子,假设FIFO深度是8,地址位只需要3位(0~7)。但我把指针设计成4位。高1位用来标记「圈数」,低3位是实际的地址。
- 写指针从 0000 → 0111(第0圈,地址0~7)→ 1000(第1圈,地址0)→ 1111(第1圈,地址7)
- 读指针同理
判断规则就变得非常清晰:
| 条件 | 判断结果 | 说明 |
|---|---|---|
| 写指针 == 读指针 | 空 | 所有位都相等,说明在同一圈,读追上了写 |
| 写指针高1位 != 读指针高1位 且低3位相等 |
满 | 高1位不同,说明写比读多绕了一圈,且地址相同 |
我的小技巧:判断满的时候,别只比高1位。我习惯写成 (wptr[4] != rptr[4]) && (wptr[3:0] == rptr[3:0])。这样逻辑更严谨,综合出来也不容易出问题。
3.3 为什么多一位就够了?
你可能会问:为什么只多一位?多两位行不行?
其实多一位就够了。因为FIFO的深度是固定的,写指针最多比读指针多绕一圈(满的时候)。再多绕,那就已经溢出了,属于设计错误。所以一位标志位刚好能区分「同一圈」和「多一圈」这两种状态。
我曾经在一个项目中看到有人用了两位标志位,问了一下,说是「为了保险」。其实没必要,反而增加了比较逻辑的复杂度。设计上讲究「够用就好」,多一位是黄金法则,多两位就是画蛇添足了。
3.4 代码实现示例
下面给一个简单的同步FIFO空满判断代码,重点看指针比较的部分:
module fifo_empty_full #(
parameter DEPTH = 8,
parameter PTR_WIDTH = 4 // 实际地址3位 + 1位标志位
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire rd_en,
output wire empty,
output wire full
);
reg [PTR_WIDTH-1:0] wptr, rptr;
// 空:所有位相等
assign empty = (wptr == rptr);
// 满:高1位不同,低3位相等
assign full = (wptr[PTR_WIDTH-1] != rptr[PTR_WIDTH-1]) &&
(wptr[PTR_WIDTH-2:0] == rptr[PTR_WIDTH-2:0]);
// 写指针更新(略)
// 读指针更新(略)
endmodule
注意:异步FIFO里,指针比较前要先做同步处理。这个我们后面章节会详细讲。这里先记住核心思想——多一位标志位,空满判断就清清楚楚。
3.5 本章知识体系图
下面这张图帮你理清空满判断的核心逻辑:
这张图把核心逻辑都串起来了。你记住一句话就行:多一位标志位,空满永不相混。
可可精品资料库