FIFO是英文First In First Out的缩写,是一种先进先出的数据缓存器,他与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据。
FIFO被广泛运用于跨时钟域的数据传输中,在实际使用时,我们往往直接调用XILINX的IP核,或者使用已经被验证过的FIFO而非自己实现。为何不建议大家自己写一个FIFO使用呢,我认为主要有以下几点。
1.经过功能仿真验证正确的一个FIFO,在更高的频率下,面临更严格的时序要求,能不能正常工作?
2.在读写时钟频率相差极大的情况下,FIFO能不能正常工作?
3.我们对FIFO下的时序约束是否合理?
以上几点都是不确定的,而使用官方提供的IP核可以尽可能避免因自己考虑不周导致的设计错误,因此不推荐使用自己写FIFO。在这里我想从自身的理解出发尝试解释如何更完整的实现一个FIFO,仅仅用于探讨,如有错误欢迎大家指正。而关于FIFO原理和实现的介绍已经有很多博客珠玉在前,在这里我们不做赘述。
2 实现 2.1 端口及参数的定义
关于FIFO功能的实现我们借鉴了前人博客异步FIFO原理与代码实现,第一部分是确定FIFO的参数和端口,在这里我们建立的是16x256的FIFO,因此地址位宽需要8位,为了判断空满指针则需要九位,同时我们例化了一个双口ram用于存储数据。
//**************************************************************************
// *** 名称 : async_fifo.v
// *** 作者 : 吃豆熊
// *** 日期 : 2021-4-20
// *** 描述 : 异步fifo模块
//**************************************************************************
module async_fifo
//========================< 参数 >==========================================
#(
parameter DATA_WIDTH = 16, //数据位宽
parameter DATA_DEPTH = 256, //FIFO深度
parameter ADDR_WIDTH = 8 //FIFO地址位宽
)
//========================< 端口 >==========================================
(
input wire rst,
//写端口
input wire wr_clk, //写时钟
input wire wr_en, //写使能
input wire [DATA_WIDTH-1:0] din, //写入数据 位宽为16位
//读端口
input wire rd_clk, //读时钟
input wire rd_en, //读使能
//输出
output reg dout_vld, //输出数据有效信号
output reg [DATA_WIDTH-1:0] dout, //输出以太网数据
output empty, //读空信号
output full //写满信号
);
//========================< 信号 >==========================================
reg [DATA_WIDTH:0] wr_addr_ptr; //写地址指针 比写地址多一位MSB用于检测是否同一圈
reg [DATA_WIDTH:0] rd_addr_ptr; //读地址指针
wire [DATA_WIDTH-1:0] wr_addr; //写地址
wire [DATA_WIDTH-1:0] rd_addr; //读地址
wire [ADDR_WIDTH:0] wr_addr_ptr_gray; //写指针对应格雷码
reg [ADDR_WIDTH:0] wr_addr_ptr_gray_d1; //写指针对应格雷码同步第一拍
reg [ADDR_WIDTH:0] wr_addr_ptr_gray_d2; //写指针对应格雷码同步第二拍
wire [ADDR_WIDTH:0] rd_addr_ptr_gray; //读指针对应格雷码
reg [ADDR_WIDTH:0] rd_addr_ptr_gray_d1; //读指针对应格雷码同步第一拍
reg [ADDR_WIDTH:0] rd_addr_ptr_gray_d2; //读指针对应格雷码同步第二拍
reg [DATA_WIDTH-1:0] fifo_ram [DATA_DEPTH-1:0]; //例化双口ram
2.2 初始化双口ram及数据读写
这里我们需要做的是在复位有效时,将ram内数据清空,同时在FIFO未写满的情况下写入数据,在未读空的情况下读出
//==========================================================================
//== 初始化双口ram及写入数据
//==========================================================================
genvar i;
generate
for(i = 0; i < DATA_DEPTH; i = i + 1 )
begin:fifo_init
always@(posedge wr_clk or posedge rst)
begin
if (rst == 1'b1) begin
fifo_ram[i] <= 'h0;
end
else if ((wr_en == 1'b1) && (full == 1'b0)) begin
fifo_ram[wr_addr] <= din;
end
else begin
fifo_ram[wr_addr] <= fifo_ram[wr_addr];
end
end
end
endgenerate
//==========================================================================
//== 生成输出数据及有效
//==========================================================================
//复位无效情况下,若读有效且未读空,生成读出数据及有效信号
always @(posedge rd_clk or posedge rst) begin
if (rst == 1'b1) begin
dout <= 'h0;
dout_vld <= 1'b0;
end
else if ((rd_en == 1'b1) && (empty == 1'b0)) begin
dout <= fifo_ram [rd_addr];
dout_vld <= 1'b1;
end
else begin
dout <= 'h0;
dout_vld <= 1'b0;
end
end
2.3 读写地址及指针生成
这里读写地址好理解,而读写指针则比地址多一位,最高位MSB用于判断读空写满,若读写指针最高位相同,则代表读空;当不同时,读比写多经过了一轮,代表写满。
在这里需要注意的是,当读写时钟域不一致时,我们需要将指针进行同步才能比较,而我们都知道多比特的控制信号是不能通过打拍来同步的,会出现错位等功能错误。在这里我们将指针转化为格雷码,此时每次只会变化一位,可以通过打拍来同步,而且不会出现功能错误,具体原因我们会在后面进行解释。
而在码制转化后,读空条件仍是格雷码指针完全相同,而写满则转变为需要写时钟域的格雷码写指针和被同步到写时钟域的格雷码读指针高两位不相同,其余各位完全相同。
//==========================================================================
//== 生成空满信号
//==========================================================================
//写满需要写时钟域下,写指针和同步过来的读指针,高两位不同,其他完全相同
assign full = (wr_addr_ptr_gray == {~rd_addr_ptr_gray_d2[ADDR_WIDTH:ADDR_WIDTH-1],rd_addr_ptr_gray_d2[ADDR_WIDTH-2:0]});
//读空需要读时钟域下,读指针和同步过来的写指针完全相同
assign empty = (rd_addr_ptr_gray == wr_addr_ptr_gray_d2);
//==========================================================================
//== 生成读写地址
//==========================================================================
//写地址
assign wr_addr = wr_addr_ptr[ADDR_WIDTH-1:0];
//读地址
assign rd_addr = rd_addr_ptr[ADDR_WIDTH-1:0];
//==========================================================================
//== 生成指针及跨时钟域同步
//==========================================================================
//写地址指针生成
always @(posedge wr_clk or posedge rst) begin
if (rst == 1'b1) begin
wr_addr_ptr <= 'h0;
end
else if ((wr_en == 1'b1) && (full == 1'b0)) begin
wr_addr_ptr <= wr_addr_ptr + 1'b1;
end
else begin
wr_addr_ptr <= wr_addr_ptr;
end
end
//写地址指针格雷码转换
assign wr_addr_ptr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;
//写地址指针同步
always @(posedge rd_clk or posedge rst) begin
wr_addr_ptr_gray_d1 <= wr_addr_ptr_gray;
wr_addr_ptr_gray_d2 <= wr_addr_ptr_gray_d1;
end
//读地址指针生成
always @(posedge rd_clk or posedge rst) begin
if (rst == 1'b1) begin
rd_addr_ptr <= 'h0;
end
else if ((rd_en == 1'b1) && (empty == 1'b0)) begin
rd_addr_ptr <= rd_addr_ptr + 1'b1;
end
else begin
rd_addr_ptr <= rd_addr_ptr;
end
end
//读地址指针格雷码转换
assign rd_addr_ptr_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;
//读地址指针同步
always @(posedge wr_clk or posedge rst) begin
rd_addr_ptr_gray_d1 <= rd_addr_ptr_gray;
rd_addr_ptr_gray_d2 <= rd_addr_ptr_gray_d1;
end
3 仿真
这里我们给出仿真的代码如下,同时为了方便大家的仿真,我也给出了我常用的一份脚本,脚本命名为run.do,只需要在modelsim里输入do run.do即可完成仿真,相信大家都可以通过这两份文件轻松完成异步FIFO的仿真。
//**************************************************************************
// *** 名称 : tb_async_fifo.v
// *** 作者 : 吃豆熊
// *** 日期 : 2021-4-20
// *** 描述 : 异步fifo testbench
//**************************************************************************
`timescale 1ns/1ns
module tb_async_fifo();
//========================< 端口 >==========================================
reg rst ;
reg wr_clk ;
reg wr_en ;
reg [15:0] din ;
wire full ;
reg rd_clk ;
reg rd_en ;
wire [15:0] dout ;
wire dout_vld ;
wire empty ;
//==========================================================================
//== 变量赋值
//==========================================================================
//时钟、复位信号
initial
begin
wr_clk = 1'b1 ;
rd_clk = 1'b1 ;
rst <= 1'b1 ;
wr_en <= 1'b0 ;
rd_en <= 1'b0 ;
#200
rst <= 1'b0 ;
wr_en <= 1'b1 ;
#20000
wr_en <= 1'b0 ;
rd_en <= 1'b1 ;
#20000
rd_en <= 1'b0 ;
$stop;
end
always #10 wr_clk = ~wr_clk;
always #30 rd_clk = ~rd_clk;
always@(posedge wr_clk or negedge rst)begin
if(rst == 1'b1)
din <= 'd0;
else if(wr_en)
din <= din + 1'b1;
else
din <= din;
end
//==========================================================================
//== 模块例化
//==========================================================================
async_fifo async_fifo_inst
(
.rst (rst ),
.wr_clk (wr_clk ),
.wr_en (wr_en ),
.din (din ),
.full (full ),
.rd_clk (rd_clk ),
.rd_en (rd_en ),
.dout (dout ),
.dout_vld (dout_vld ),
.empty (empty )
);
endmodule
# 退出当前仿真
quit -sim
vlib work
# 分别编译设计文件和testbench。
vlog "../src/*.v"
vlog "../tb/*.v"
# 开始仿真,根据版本不同进行选择
vsim -voptargs=+acc work.tb_async_fifo
#vsim -gui -novopt work.tb_async_fifo
# 添加指定信号
# 添加顶层所有的信号
# Set the window types
# 打开波形窗口
view wave
view structure
# 打开信号窗口
view signals
# 添加波形
add wave -position insertpoint sim:/tb_async_fifo/*
add wave -position insertpoint sim:/tb_async_fifo/async_fifo_inst/*
.main clear
# 运行一定时间
run 10us
4 约束
因为我们的异步FIFO内就涉及到了CDC的部分,因此大家容易想到的就是直接set_false_path from wr_clk to rd_clk,或者是set_clock_group,通过异步处理的方式来忽略这方面的检查。但是这是不完善的,随着时钟频率的提升,电路对时序的要求会变高,可能同样的布局布线,在低频下不会有问题在高频下时序就违例了,在这里我们给出一种可行的约束方式。
首先是读写指针的第一级同步,也就是wr_addr_ptr_gray to wr_addr_ptr_gray_d1和rd_addr_ptr_gray to rd_addr_ptr_gray_d1,在第一级同步时是有可能不能成功满足的而且这也是允许的,不然我们也不需要第二级同步了,所以我们直接设定为set_false_path。
而在读写指针的第二级同步,也就是wr_addr_ptr_gray_d1 to wr_addr_ptr_gray_d2和rd_addr_ptr_gray_d1 to rd_addr_ptr_gray_d1,为了防止各bit到达时间不一致,或者出现过高的延迟,首先是不能有任何的logic块,在这里我们肯定是可以满足的,但是同时要考虑布局布线的延时影响,因此我们可以设定set_max_delay为一个clk,实际中,我们可以设置的更紧,比如0.4-0.5个clk。
就目前来看,我们已经做了比较恰当的约束了,当然也有其他的约束方式,但是我仍然不推荐将这个FIFO运用于实际工程中。因为我们注意到,在vivado2019.1版本中,实际上在我们综合IP核时,工具会自动帮我们添加合适的约束,包括时钟以及FIFO约束等等,因此在要求不高的情况下时序往往可以得到满足,而当要求很高时,没有经过验证的FIFO同样不靠谱,因此我们还是是推荐使用工程自带的基础IP核。
---
原创教程,转载请注明出处吃豆熊-如何更完整地实现一个异步FIFO-功能实现,仿真及时序约束
参考资料:IC基础(一):异步FIFO原理与代码实现