3、同步FIFO架构设计:双端口RAM + 读写指针 + 空满判断逻辑的经典架构
同步FIFO,说白了就是同一个时钟域下的数据缓冲器。我刚开始做设计那会儿,总觉得FIFO不就是个先进先出的队列嘛,有啥难的?直到第一次流片回来,发现数据读出来全是乱的……嗯,从那以后我再也不敢小看这个经典架构了。
今天咱们就聊聊这个经典的三件套:双端口RAM、读写指针、空满判断逻辑。这三样东西组合在一起,就是同步FIFO的骨架。
3.1 双端口RAM——FIFO的肚子
双端口RAM,顾名思义,有两个端口。一个专门写,一个专门读。这两个端口互不干扰,可以同时操作。
我个人习惯用真双端口RAM(True Dual-Port RAM),也就是两个端口都有独立的地址、数据和控制信号。这样读写可以完全并行,效率最高。
来看看一个典型的双端口RAM接口长什么样:
module dpram #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4,
parameter DEPTH = 16
)(
// 写端口
input wire wr_clk,
input wire wr_en,
input wire [ADDR_WIDTH-1:0] wr_addr,
input wire [DATA_WIDTH-1:0] wr_data,
// 读端口
input wire rd_clk,
input wire rd_en,
input wire [ADDR_WIDTH-1:0] rd_addr,
output reg [DATA_WIDTH-1:0] rd_data
);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
// 写操作
always @(posedge wr_clk) begin
if (wr_en)
mem[wr_addr] <= wr_data;
end
// 读操作
always @(posedge rd_clk) begin
if (rd_en)
rd_data <= mem[rd_addr];
end
endmodule
这里有个细节要注意:读操作是时钟沿触发的。我在项目中遇到过有人把读做成组合逻辑,结果时序一塌糊涂。你想想看,RAM的读延迟本来就大,再搞组合逻辑,setup violation能让你怀疑人生。
3.2 读写指针——FIFO的导航员
读写指针,其实就是两个计数器。写指针指向下一个要写入的位置,读指针指向下一个要读出的位置。
我习惯把指针设计成格雷码计数器。为什么?因为指针要跨时钟域传递(异步FIFO),格雷码每次只变一位,能有效减少亚稳态风险。虽然同步FIFO不需要跨时钟域,但保持这个习惯,以后做异步FIFO时直接复用。
指针的宽度要比地址多一位。比如深度16的FIFO,地址需要4位,但指针要用5位。多出来的这一位用来判断空满。
// 写指针示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en && !full)
wr_ptr <= wr_ptr + 1;
end
// 读指针示例
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= 0;
else if (rd_en && !empty)
rd_ptr <= rd_ptr + 1;
end
注意看,写操作只有在非满时才进行,读操作只有在非空时才进行。这是最基本的保护机制,少了它,FIFO就会溢出或读空。
3.3 空满判断逻辑——FIFO的交通灯
空满判断,这是整个FIFO设计里最容易出错的地方。我见过太多人在这里翻车了。
判断逻辑其实很简单:
- 空标志: 读写指针相等(包括最高位)
- 满标志: 读写指针的最高位不同,但低位全部相等
为什么会这样?因为指针多了一位,所以当写指针比读指针多绕了一圈时,最高位就会不同。这时候虽然地址位相同,但实际上写指针已经领先了一圈,FIFO是满的。
空:wr_ptr == rd_ptr
满:(wr_ptr[WIDTH] != rd_ptr[WIDTH]) && (wr_ptr[WIDTH-1:0] == rd_ptr[WIDTH-1:0])
来看看完整的空满判断代码:
// 空满判断
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[WIDTH] != rd_ptr[WIDTH]) &&
(wr_ptr[WIDTH-1:0] == rd_ptr[WIDTH-1:0]);
// 格雷码转二进制(用于比较)
wire [WIDTH:0] wr_ptr_bin, rd_ptr_bin;
gray2bin #(.WIDTH(WIDTH+1)) u_gray2bin_wr (
.gray (wr_ptr_gray),
.bin (wr_ptr_bin)
);
gray2bin #(.WIDTH(WIDTH+1)) u_gray2bin_rd (
.gray (rd_ptr_gray),
.bin (rd_ptr_bin)
);
3.4 经典架构总览
把上面三部分拼起来,就是完整的同步FIFO了。我画了个结构图,方便你理解:
3.5 完整代码框架
最后,我把整个同步FIFO的框架代码贴出来。你直接拿去用,稍微改改参数就能跑:
module sync_fifo #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4,
parameter DEPTH = 16
)(
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
);
// 指针定义(多一位)
localparam PTR_WIDTH = ADDR_WIDTH + 1;
reg [PTR_WIDTH-1:0] wr_ptr, rd_ptr;
wire [PTR_WIDTH-1:0] wr_ptr_next, rd_ptr_next;
// 双端口RAM实例化
dpram #(
.DATA_WIDTH(DATA_WIDTH),
.ADDR_WIDTH(ADDR_WIDTH),
.DEPTH(DEPTH)
) u_dpram (
.wr_clk (clk),
.wr_en (wr_en && !full),
.wr_addr(wr_ptr[ADDR_WIDTH-1:0]),
.wr_data(wr_data),
.rd_clk (clk),
.rd_en (rd_en && !empty),
.rd_addr(rd_ptr[ADDR_WIDTH-1:0]),
.rd_data(rd_data)
);
// 写指针更新
assign wr_ptr_next = wr_ptr + {{PTR_WIDTH-1{1'b0}}, 1'b1};
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en && !full)
wr_ptr <= wr_ptr_next;
end
// 读指针更新
assign rd_ptr_next = rd_ptr + {{PTR_WIDTH-1{1'b0}}, 1'b1};
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_ptr <= 0;
else if (rd_en && !empty)
rd_ptr <= rd_ptr_next;
end
// 空满判断
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[PTR_WIDTH-1] != rd_ptr[PTR_WIDTH-1]) &&
(wr_ptr[PTR_WIDTH-2:0] == rd_ptr[PTR_WIDTH-2:0]);
endmodule
好了,同步FIFO的经典架构就聊到这儿。说白了就是三个模块各司其职:RAM存数据,指针指位置,逻辑判空满。你把这个架构吃透了,异步FIFO也就是加个同步器的事。
可可精品资料库