一、为什么需要 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位的数据。
处理器取指和译码之间的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:
“地址”是用来指定访问哪一个格子的编号。
有 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…