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的核心数据流和判断逻辑:
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的空满判断,核心就是扩展一位的指针比较。空标志看完全相等,满标志看最高位不同且低位相等。就这么简单,但细节决定成败。