4、同步FIFO设计:单时钟域下的空满标志生成逻辑、Verilog代码实现

同步FIFO,说白了就是读写都在同一个时钟域里。你可能会问:那它还有什么难的?

嗯,难点不在时钟域同步,而在空满标志的精准判断。我刚开始做FIFO设计时,就吃过这个亏——空满标志判断错了,数据要么读空了还在读,要么写满了还在写,整个系统直接崩掉。

4.1 空满标志的核心思想

同步FIFO的空满判断,本质上就是比较读写指针。但这里有个坑:指针是循环的,你得处理回绕问题。

我个人习惯用格雷码指针,但同步FIFO其实不需要。因为读写在同一时钟域,直接用二进制指针就行,简单粗暴。

判断逻辑其实就两条:

  • 空标志:读写指针相等时,FIFO为空
  • 满标志:写指针追上读指针,且多绕了一圈

等等,为什么满标志要多绕一圈?你想想看,如果读写指针相等,到底是空还是满?

这就是经典问题。解决办法是:把指针宽度扩展1位,用最高位区分绕圈次数。

关键点:指针宽度 = 地址宽度 + 1位绕圈标志位

比如深度16的FIFO,地址需要4位,指针就用5位。最高位相同表示在同一圈,不同表示写指针多绕了一圈。

4.2 空满标志生成逻辑

有了扩展指针,判断就清晰了:

条件 标志 说明
wptr == rptr 空 读写指针完全相等,包括最高位
wptr[n] != rptr[n] 且 低n位相等 满 最高位不同,低位相同,说明写指针多绕了一圈

我在项目中遇到过一个问题:满标志判断时,如果只比较最高位,可能会误判。比如写指针刚绕了一圈,读指针也刚好绕了一圈,这时候最高位又相同了,但FIFO其实没满。

所以正确的做法是:最高位不同 + 低位全部相等,两个条件同时满足才算满。

4.3 Verilog代码实现

下面给出一个完整的同步FIFO设计。深度16,数据宽度8位。

module sync_fifo #(
    parameter DATA_WIDTH = 8,
    parameter ADDR_WIDTH = 4,
    parameter DEPTH = 1 << ADDR_WIDTH
)(
    input  wire                clk,
    input  wire                rst_n,
    input  wire                wr_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    input  wire                rd_en,
    output reg  [DATA_WIDTH-1:0] rd_data,
    output wire                empty,
    output wire                full
);

    // 指针宽度 = 地址宽度 + 1
    localparam PTR_WIDTH = ADDR_WIDTH + 1;

    reg [PTR_WIDTH-1:0] wptr, rptr;
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];

    // 写指针逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            wptr <= 0;
        else if (wr_en && !full)
            wptr <= wptr + 1;
    end

    // 读指针逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            rptr <= 0;
        else if (rd_en && !empty)
            rptr <= rptr + 1;
    end

    // 写数据
    always @(posedge clk) begin
        if (wr_en && !full)
            mem[wptr[ADDR_WIDTH-1:0]] <= wr_data;
    end

    // 读数据
    always @(posedge clk) begin
        if (rd_en && !empty)
            rd_data <= mem[rptr[ADDR_WIDTH-1:0]];
    end

    // 空标志:指针完全相等
    assign empty = (wptr == rptr);

    // 满标志:最高位不同,低位相同
    assign full = (wptr[PTR_WIDTH-1] != rptr[PTR_WIDTH-1]) &&
                  (wptr[ADDR_WIDTH-1:0] == rptr[ADDR_WIDTH-1:0]);

endmodule

个人经验:代码里我用了组合逻辑直接生成空满标志。这样做的好处是延迟小,但要注意:如果时钟频率很高,组合路径可能成为瓶颈。高频设计建议用寄存器打一拍。

4.4 核心逻辑流程图

下面这张图展示了同步FIFO的核心数据流和判断逻辑:

写指针 (wptr) 读指针 (rptr) 双端口存储器 深度16,宽度8 写端口 | 读端口 指针比较器 empty full 空:wptr == rptr | 满:最高位不同且低位相同

4.5 避坑指南

我曾经在一个项目中,因为空满标志判断延迟,导致数据覆盖。具体来说,满标志拉高后,写使能已经发出了,结果多写了一个数据。

这个问题怎么解决?

  • 方案一:满标志用寄存器打一拍,但会损失一个时钟周期的性能
  • 方案二:提前判断,比如深度还剩2个位置时就拉高满标志(这叫almost full)
  • 方案三:写使能直接和满标志做与门,从源头堵住

注意:同步FIFO虽然简单,但空满标志的时序一定要仔细检查。我见过有人把空满标志放在always块里用非阻塞赋值,结果仿真和综合结果不一致,查了两天才找到原因。

记住:空满标志用assign组合逻辑生成,或者用阻塞赋值在always块里生成。非阻塞赋值会引入一个时钟周期的延迟,这在空满判断中是致命的。

4.6 深度扩展

上面的代码深度是16,如果你想改成其他深度,只需要改ADDR_WIDTH参数。比如深度32,ADDR_WIDTH=5,指针宽度=6。

但要注意:深度必须是2的幂次方。如果不是,你得用其他方法处理回绕,那就复杂了。

我个人建议:同步FIFO的深度尽量用2的幂次方。这样指针回绕逻辑最简单,也最不容易出错。如果非要用非2的幂次方深度,那就得在指针比较时做特殊处理,比如用计数器代替指针。

好了,同步FIFO的空满标志就讲到这里。代码可以直接拿去用,但建议你根据实际项目需求做调整。比如数据宽度、深度、是否要almost full/empty标志等。

总结一句话:同步FIFO的空满判断,核心就是扩展一位的指针比较。空标志看完全相等,满标志看最高位不同且低位相等。就这么简单,但细节决定成败。