Skip to content

同步 FIFO:空满标志

Verilog
Contents

一、为什么需要 FIFO?

两个模块之间数据的产生速率和消费速率不匹配,需要一个”蓄水池”来平滑。

  • 速率不匹配: 比如 CPU 以 100MB/s 向外设写,但外设只能 60MB/s 消费——需要 FIFO 缓冲突发。
  • 位宽不匹配: 上游 32 位、下游 8 位,需要 FIFO 做宽度转换(这种叫 width-converting FIFO)。
  • 时钟域隔离: 写侧和读侧在不同时钟域——这是异步 FIFO,课件留到下一章讲。

二、核心要素

一个同步 FIFO 最小配置有 5 个端口 + 2 个状态 + 内部 3 件套:

对外端口:

  • clk, rst_n: 时钟和复位
  • wr_en, din: 写使能 + 写数据
  • rd_en, dout: 读使能 + 读数据

状态输出:

  • full: 写满,此时 wr_en 应被忽略(或上游别发)
  • empty: 读空,此时 rd_en 应被忽略(或 dout 无效)

内部 3 件套:

  • 存储体(RAM/Reg array): 实际存数据的地方
  • 写指针 wr_ptr: 下一次要写入的地址
  • 读指针 rd_ptr: 下一次要读出的地址

上图先连续写入A、B、C:do_wr拉高后的下一个时钟沿,wr_ptr才加1。buf_out是组合读(assign buf_out = mem[rd_ptr]),一直显示rd_ptr指向的那一格,所以A写进去后马上就出现在buf_out上;do_rd拉高后的下一个时钟沿rd_ptr加1,buf_out依次换成B、C。

两个关键参数:

  • DEPTH (深度): 能存多少个数据,决定了指针宽度 ADDR_WIDTH = log2(DEPTH)
  • WIDTH (宽度): 每个数据多少 bit

DEPTH和WIDTH写成module的parameter,例化时再给具体的值,同一份RTL就能做出不同大小的FIFO,这叫参数化。比如用#(.DEPTH(16), .WIDTH(32))例化出来的FIFO,能存16个32位的数据。

Note

处理器取指和译码之间的instruction queue就是一个FIFO。SystemVerilog里的队列([$])是另一回事:它是testbench里用的数据类型,不能综合成电路,见SystemVerilog笔记「2. 队列 queue」。

三、空满判断——FIFO 设计的灵魂

这是全章最重要的内容,必须掌握。课件给了两种方法

方法 1:用 counter 判断(课件第 8 页)

优点: 直观,容易理解,不容易错 缺点: fifo_cnt 宽度需要 log2(DEPTH) + 1 位(因为要能表示 DEPTH 这个值本身),而且 counter 的加减逻辑在大深度 FIFO 里会成为关键路径。

判空判满:

assign buf_empty = (cnt == 0);
assign buf_full = (cnt == DEPTH);

上方波形假设 DEPTH = 4,cnt 最大值 = 4 时 full 拉高。

同时读写(cnt 不变) do_wr 和 do_rd 同一周期都为高 → 一进一出,cnt 保持不变,full/empty 不受影响。

方法 2:指针扩展 1 bit(课件作业扩展要求)

这是业界更常用的写法,也最值得吃透。核心思想:

读写指针都比地址多一位,用高位区分”是否多转了一圈”。

举例,DEPTH = 8,地址宽度需要 3 bit(0~7),但指针用 4 bit:

地址宽度为什么是 3 bit

“地址”是用来指定访问哪一个格子的编号。

有 8 个格子,就要给每个格子一个唯一的编号,所以会发现二进制下,最少需要3位即000到111来表示这8个不同格子的编号。

数一下二进制列——每个地址都是 3 位,所以”地址宽度 = 3 bit”。

  • wr_ptr[3] 和 rd_ptr[3] 是”圈数”标志位
  • wr_ptr[2:0] 和 rd_ptr[2:0] 才是真正访问 RAM 的地址

DEPTH=8:do_wr 拉高后下一 posedge wr_ptr 才更新,8 次写(t3-t10)填满,读 2 次(t12-t13)

判空判满规则:

assign empty = (wr_ptr == rd_ptr);

assign full  = (wr_ptr[3]    != rd_ptr[3]) && 
               (wr_ptr[2:0]  == rd_ptr[2:0]);

边界场景:从满读空(rd_ptr 追上 wr_ptr,高位也相同) 初始状态:DEPTH=8,wr_ptr=4’b1000,rd_ptr=4’b0000,full=1。连续读 8 次后 rd_ptr[3] 翻转为 1,rd_ptr==wr_ptr,empty 拉高。

第一次读(t3)后 rd_ptr[2:0]=001 ≠ wr_ptr[2:0]=000,full 立刻清零。8 次读后 rd_ptr=1000=wr_ptr,empty 拉高。

为什么这样就能区分空和满? 不扩展 1 bit 的话,wr_ptr == rd_ptr 有两种情况无法区分:

  • 刚复位,都指向 0 → 空
  • 写了 8 次,写指针从 0 → 8,但 3 bit 溢出回 0 → 满

扩展 1 bit 后,这两种情况的高位不同,就能区分了。

优点: 判空判满是纯组合逻辑,没有 counter 的加减链,时序更好 缺点: 理解起来比 counter 绕,新手容易写错。

🔑 小结: “同步 FIFO 怎么判空判满?两种方法有什么区别?” —— 答案就是上面这两种,能同时说出、并讲清楚 trade-off,就算真正掌握了。

四、完整 RTL 代码

fifo_counter.sv

module fifo_counter #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 8,
    parameter ADDR_WIDTH = 3    // log2(DEPTH)
)(
    input                    clk,
    input                    rst_n,
    input  [DATA_WIDTH-1:0]  buf_in,
    output [DATA_WIDTH-1:0]  buf_out,
    input                    wr_en,
    input                    rd_en,
    output                   buf_empty,
    output                   buf_full,
    output [ADDR_WIDTH:0]    fifo_cnt    // extra bit to represent DEPTH
);

    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];

    reg [ADDR_WIDTH-1:0] wr_ptr, rd_ptr;
    reg [ADDR_WIDTH:0]   cnt;              // ADDR_WIDTH+1 bits: range 0~DEPTH

    assign buf_empty = (cnt == 0);
    assign buf_full  = (cnt == DEPTH);
    assign fifo_cnt  = cnt;

    // guarded write/read enable
    wire do_wr = wr_en && !buf_full;
    wire do_rd = rd_en && !buf_empty;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)       wr_ptr <= 0;
        else if (do_wr)   wr_ptr <= wr_ptr + 1'b1;   // wraps naturally
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)       rd_ptr <= 0;
        else if (do_rd)   rd_ptr <= rd_ptr + 1'b1;
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)                    cnt <= 0;
        else begin
            case ({do_wr, do_rd})
                2'b10: cnt <= cnt + 1'b1;
                2'b01: cnt <= cnt - 1'b1;
                default: cnt <= cnt;
            endcase
        end
    end

    always @(posedge clk) begin
        if (do_wr) mem[wr_ptr] <= buf_in;
    end

    // combinational read — same-cycle output
    assign buf_out = mem[rd_ptr];

endmodule

版本 A:用 fifo_counter(匹配课件 TB)

fifo_cnt 这个 +1 bit 不”标识”任何东西,纯粹是数学需要 —— 因为要能存下 DEPTH 这个数值本身。下面用具体数字看就明白了。
计数器要表达多少个不同的值?
DEPTH = 8 时,fifo_cnt 的合法范围是:
0, 1, 2, 3, 4, 5, 6, 7, 8 ← 一共 9 个值
^ ^
空 满

判断空满核心逻辑:

版本 B:指针扩展 1 bit(课件扩展要求)

sync_fifo.sv

module sync_fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 8,
    parameter ADDR_WIDTH = 3
)(
    input                    clk,
    input                    rst_n,
    input  [DATA_WIDTH-1:0]  buf_in,
    output [DATA_WIDTH-1:0]  buf_out,
    input                    wr_en,
    input                    rd_en,
    output                   buf_empty,
    output                   buf_full
);

    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    
    // pointers are ADDR_WIDTH+1 bits; MSB used for full/empty detection
    reg [ADDR_WIDTH:0] wr_ptr, rd_ptr;

    assign buf_empty = (wr_ptr == rd_ptr);
    assign buf_full  = (wr_ptr[ADDR_WIDTH]     != rd_ptr[ADDR_WIDTH]) &&
                       (wr_ptr[ADDR_WIDTH-1:0] == rd_ptr[ADDR_WIDTH-1:0]);

    wire do_wr = wr_en && !buf_full;
    wire do_rd = rd_en && !buf_empty;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)     wr_ptr <= 0;
        else if (do_wr) wr_ptr <= wr_ptr + 1'b1;
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)     rd_ptr <= 0;
        else if (do_rd) rd_ptr <= rd_ptr + 1'b1;
    end

    // only lower ADDR_WIDTH bits index into mem
    always @(posedge clk) begin
        if (do_wr) mem[wr_ptr[ADDR_WIDTH-1:0]] <= buf_in;
    end

    assign buf_out = mem[rd_ptr[ADDR_WIDTH-1:0]];

endmodule

判断空满核心逻辑:利用高位来识别是否多走了一圈

参考

  • C. E. Cummings, “Simulation and Synthesis Techniques for Asynchronous FIFO Design,” SNUG San Jose, 2002,见Paradigm Works技术文库

Discussion

Loading comments…

Checking sign-in…