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读写指针知识体系 读写指针 写指针 (wr_ptr) 读指针 (rd_ptr) 二进制计数 格雷码计数 二进制计数 格雷码计数 同步FIFO用二进制 | 异步FIFO用格雷码 核心:格雷码跨时钟域只有1位变化,降低亚稳态风险

说白了,指针设计就是FIFO的命脉。二进制简单但跨时钟域危险,格雷码安全但转换逻辑稍复杂。我个人建议:异步FIFO永远用格雷码,别偷懒。

好,这一章就到这里。记住:指针搞定了,FIFO就成功了一半。

本章要点回顾:

  • 写指针指向下一个写入地址,读指针指向下一个读出地址
  • 二进制计数简单,但多位同时变化不适合跨时钟域
  • 格雷码相邻值只有1位变化,是异步FIFO的标准选择
  • 指针位宽 = 地址位宽 + 1(多出的1位用于空满判断)

可可精品资料库