3、FIFO的读写指针:写指针、读指针、二进制计数与格雷码计数
好,咱们今天聊聊FIFO的核心——读写指针。
说实话,我刚入行那会儿,觉得FIFO不就是个存储嘛,有啥难的?后来做异步FIFO时踩了坑,才明白指针设计才是真正的灵魂。你想想看,如果指针都搞不清楚,那FIFO的空满判断肯定要出问题。
3.1 写指针(wr_ptr)与读指针(rd_ptr)
先说说最基本的定义。
写指针,指向下一个要写入数据的地址。每写入一个数据,写指针就加1。
读指针,指向下一个要读出数据的地址。每读出一个数据,读指针也加1。
说白了,这两个指针就像两个人在赛跑。写指针在前面跑,读指针在后面追。它们之间的距离,就是FIFO里当前存储的数据量。
核心关系:
- 写指针追上读指针 → FIFO满(不能再写)
- 读指针追上写指针 → FIFO空(不能再读)
我在项目中遇到过一个问题:有个同事把读写指针的初始值设成了0,结果复位后FIFO状态就乱了。嗯,这里要注意——复位时,读写指针通常都指向0,但空标志要立即拉高。
3.2 指针的二进制计数
最简单的实现方式,就是用二进制计数器。
// 二进制写指针示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr_bin <= 0;
else if (wr_en && !full)
wr_ptr_bin <= wr_ptr_bin + 1'b1;
end
// 二进制读指针示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr_bin <= 0;
else if (rd_en && !empty)
rd_ptr_bin <= rd_ptr_bin + 1'b1;
end
二进制计数有什么问题?
你想想看,当指针从7(0111)跳到8(1000)时,所有位都变了。在异步FIFO里,跨时钟域传递这种信号,很容易出现亚稳态。我曾经调试过一个bug,就是二进制指针跨时钟域时采样到了错误值,导致空满判断出错,数据直接丢了。
注意:二进制指针不适合直接用于异步FIFO的跨时钟域传递。多位同时变化,亚稳态风险极高。
3.3 格雷码计数
那怎么办?用格雷码。
格雷码有个特点:相邻两个值之间,只有1位不同。比如从7(格雷码0100)到8(格雷码1100),只有最高位变了。
我个人的习惯是,在异步FIFO里,读写指针都用格雷码。这样跨时钟域传递时,即使出现亚稳态,最多错1位,不会出现灾难性错误。
二进制转格雷码的公式很简单:
// 二进制转格雷码
gray = (bin >> 1) ^ bin;
// 格雷码转二进制(用于比较)
bin[MSB] = gray[MSB];
for (i = MSB-1; i >= 0; i = i-1)
bin[i] = bin[i+1] ^ gray[i];
举个例子:
| 二进制 | 格雷码 | 变化位数 |
|---|---|---|
| 0000 (0) | 0000 | - |
| 0001 (1) | 0001 | 1位 |
| 0010 (2) | 0011 | 1位 |
| 0011 (3) | 0010 | 1位 |
| 0100 (4) | 0110 | 1位 |
| 1000 (8) | 1100 | 1位 |
看到了吗?每次加1,格雷码只有1位变化。这就是它适合跨时钟域的原因。
3.4 格雷码指针的生成
实际设计中,我通常这样生成格雷码指针:
// 格雷码写指针生成
reg [ADDR_WIDTH:0] wr_ptr_gray;
reg [ADDR_WIDTH:0] wr_ptr_bin;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr_bin <= 0;
wr_ptr_gray <= 0;
end
else if (wr_en && !full) begin
wr_ptr_bin <= wr_ptr_bin + 1'b1;
wr_ptr_gray <= (wr_ptr_bin + 1'b1) ^ ((wr_ptr_bin + 1'b1) >> 1);
end
end
注意这里指针的位宽是 ADDR_WIDTH+1。为什么多1位?
嗯,这是为了判断空满。举个例子,深度为8的FIFO,地址位宽3位就够了。但指针用4位,最高位用来区分空和满。当写指针和读指针的格雷码完全相等时,FIFO空;当写指针比读指针多走了一圈(最高位不同,低位相同)时,FIFO满。
小技巧:格雷码判断空满时,不能直接比较数值。要先把格雷码转回二进制,或者用专门的格雷码比较逻辑。我习惯的做法是:空标志用格雷码直接相等判断,满标志用格雷码的次高位取反后比较。
3.5 二进制 vs 格雷码:怎么选?
我总结一下:
- 同步FIFO:读写时钟相同,直接用二进制指针。简单、高效。
- 异步FIFO:读写时钟不同,必须用格雷码指针。这是业界标准做法。
曾经有个项目,为了省几个寄存器,有人坚持在异步FIFO里用二进制指针。结果呢?仿真没问题,上板就随机出错。查了三天,最后发现是跨时钟域采样导致指针值跳变。从那以后,我再也不敢在异步FIFO里用二进制指针了。
3.6 知识体系图
下面这张图,帮你理清本章的核心逻辑:
说白了,指针设计就是FIFO的命脉。二进制简单但跨时钟域危险,格雷码安全但转换逻辑稍复杂。我个人建议:异步FIFO永远用格雷码,别偷懒。
好,这一章就到这里。记住:指针搞定了,FIFO就成功了一半。
本章要点回顾:
- 写指针指向下一个写入地址,读指针指向下一个读出地址
- 二进制计数简单,但多位同时变化不适合跨时钟域
- 格雷码相邻值只有1位变化,是异步FIFO的标准选择
- 指针位宽 = 地址位宽 + 1(多出的1位用于空满判断)
可可精品资料库