一、FIFO的简介
FIFO( First Input First Output)指的是一种数据结构,数据以先进先出的顺序进行处理。FIFO分为同步FIFO和异步FIFO,应用场景非常广泛,例如:跨时钟域传输、数据缓存、数据位宽转换等等。
Xilinx FIFO支持自定义宽度、深度、状态标志、内存类型和写入 (读取)端口宽高比。FIFO 还可以进行定制,以利用某些 FPGA 系列中提供的块 RAM、分布式 RAM 或内置 FIFO 资源来创建高性能、面积优化等 FPGA 设计。Xilinx支持 Native、AXI4-Stream、AXI4、AXI3 和 AXI4-Lite 接口 本文主要讲解Native接口的FIFO,AXI接口的后续再讲解。其中Native接口异步FIFO结构如下:
二、Native FIFO 特点
- FIFO 数据宽度为 1 至 1024 位
- 对称或非对称宽高比(读写端口比范围为 1:8 至 8:1)
- 同步或异步复位选项
- 可选存储器类型(块 RAM、分布式 RAM) 、移位寄存器或内置 FIFO
- 选择在标准或第一个字下降模式 (FWFT) 下运行
- 满和空状态标志,以及用于指示左字的几乎满和几乎空标志
- 可编程满和空状态标志,由用户定义的常量或专用输入端口设置
- 可配置的握手信号
- 支持 Block RAM 和内置 FIFO 配置的汉明错误注入和纠正检查 (ECC)
三、同步FIFO的不同配置以及仿真测试
3.1 打开 FIFO Generrator
- 用户给FIFO的命名,这里测试同步FIFO,因此命名为scfifo_test
- 接口种类:正如上文所述,Xilinx支持Native和AXI接口,本文主要讲解Native接口
- 组成FIFO的方式,同步还是异步,由块 RAM、分布式 RAM 还是内置 FIFO 资源构成
- 各种FIFO支持的特性列表
- 每种特性的是什么意思:
(1) Non-symmetric aspect ratios:非对称纵横比(读写的数据端口位宽可以不一致,范围从 1:8 到 8:1)
(2) First-Word Fall-Through:第一个字读出功能,当 FIFO 中存在数据时,第一个字将通过 FIFO自动出现在输出总线 dout上
(3) Uses Built-in FlFO primitives:使用内置的FIFO单元,从而可以通过在宽度和深度上级联内置 FIFO 来创建大型 FIFO
(4) ECC support:纠正检查支持
(5) Dynamic Error Injection:汉明错误注入
我们就选择Native接口,由块Block RAM组成的同步FIFO
3.2 端口设置
- 读FIFO模式,标准的FIFO是读使能来临后,延迟一个时钟周期输出数据。这里选择标准,后面会测试FWFT。
- 读写数据位宽以及深度,默认都是同位宽,后面会测试不同读写位宽。
- 输出寄存器,如果打开,输出的数据会延迟一拍出来
- 复位管脚
- 复位模式,分为同步复位和异步复位
- 复位时,dout数据显示什么数值
- 读数据延迟的时钟周期
3.3 标志信号设置
- 可以操作的标准信号,几乎满标志和几乎空标志
- 写确认确认信号和读有效信号,和读写溢出信号。可以打开看是否数据正确的读写进FIFO
3.3 数据数量信号设置
可以显示FIFO内还剩多少数量,因为这里选择的是同步FIFO,因此只有一个data_count。如果是异步FIFO,将会有读写数量。
3.4 标准读模式,读写位宽一致的FIFO仿真测试以及信号分析
3.4.1 控制代码编写
`timescale 1ns / 1ps
module my_scfifo_test(
input sys_clk , //输入时钟
input rst_n //系统复位
);
wire [17:0] dout ; //FIFO读数据
wire full ; //FIFO满信号
wire empty ; //FIFO空信号
wire almost_full ; //FIFO几乎满信号
wire almost_empty ; //FIFO几乎空信号
wire [9:0] data_count ; //FIFO数据数量
reg srst ; //FIFO复位
reg [17:0] din ; //FIFO写数据
reg wr_en ; //FIFO 写使能
reg rd_en ; //FIFO读使能
reg [6:0] wait_cnt ; //等待复位完成计数器
reg [2:0] state ;
always @(posedge sys_clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
wait_cnt <= 'd0;
state <= 'd0;
srst <= 1'b1;
din <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
end
else case (state)
0: if(wait_cnt[6] == 1'b1) begin //复位完后,等待一段时间再进行读写操作
state <= 'd1;
wait_cnt <= 'd0;
end
else begin
state <= 'd0;
wait_cnt <= wait_cnt +1'b1;
srst <= 1'b0;
end
1: if(full == 1'b0)begin //如果数据没满,则一直写累加的数据
wr_en <= 1'b1;
din <= din +1'b1;
state <= 'd1;
end
else begin
wr_en <= 1'b0;
din <= din;
state <= 'd2;
end
2: if(empty == 1'b0)begin //如果数据没空,就一直读数据
rd_en <= 1'b1;
state <= 'd2;
end
else begin
rd_en <= 1'b0;
state <= 'd3;
end
3: state <= 'd3;
default: state <= 'd0;
endcase
end
scfifo_test u_scfifo_test(
.clk (sys_clk ),
.srst (srst ),
.din (din ),
.wr_en (wr_en ),
.rd_en (rd_en ),
.dout (dout ),
.full (full ),
.almost_full (almost_full ),
.empty (empty ),
.almost_empty (almost_empty ),
.data_count (data_count )
);
endmodule
3.4.2 FIFO写数据端仿真结果分析
由仿真可以看出,在1510ns处FIFO写入第一个数据,在下一拍1530ns处data_count累加,同时拉低empty。再过一拍1550ns处almost_empty拉低(一旦 FIFO 中出现两个或更多字,almost_empty 标志就会失效)。data_count相对于FIFO内实际数量有一拍的延迟。
但是为什么在1530ns和1550ns处empty和almost_empty拉低但是我们采集到的数据依然是高电平。我们将1530ns处的上升沿不断地放大,我们将看到如下:
我们局部放大后可以看到,在1530ns处时钟上升沿时,过了0.1ns的延迟才将empty信号拉低,这是仿真工具根据实际信号变化的延迟预设的0.1ns 具体为什么请看关于Verilog中判断语句执行时序和modelsim时标取值的问题。结论就是:凡是在功能仿真中,左侧取样的信号一定是在时钟沿右侧翻转,仿真工具以0.1ns的延迟填充。 同理,almost_empty信号在1550ns处的上升沿处也有0.1ns的延迟翻转。
在21970ns处实际写入1024个数据后,由于data_count和空满信号会延迟一拍,因此在21990ns处的数据1025并没有写进去。由于data_count位宽为10,所以最大只能显示1023。当内部数据有1024个时,因为数据1024是11位宽,低10位都为0,所以data_count显示为0
3.4.3 FIFO读数据端仿真结果分析
在22030ns处拉高rd_en,dout在下一拍才出来,同时full拉低,data_count从1024减1等于1023。如果放大局部来看,依然也延迟了0.1ns 。实际上,在22030ns拉高的读使能,信号变化不是瞬时的,也会有一定延时。因此实际上,在22030ns拉高的读使能,在22050ns才能采集到读使能为高电平,同时经过0.1ns延迟后,再22050.1ns出数据,和拉低full信号以及data_count减1。具体为什么依然请看关于Verilog中判断语句执行时序和modelsim时标取值的问题。结论就是:凡是在功能仿真中,右侧取样的信号一定是在时钟沿时刻翻转,仿真工具以0延迟填充。
由于rd_en比data_count早一拍,因此在42490ns处的rd_en是读取的最后一个数据1024。1024在42510ns处出来,在42510ns的读使能将读不出任何数据,所以没有1025 出来。因此在写使能端,1025没有被写入到FIFO里。
3.5 标准读模式,打开output Register的FIFO仿真测试
打开了输出寄存器,我们用同样的程序进行仿真观看结果,写数据端和上面仿真一模一样,不一样的是读数据端,结果如下:
在22030ns时拉高rd_en,由于打开了输出寄存器,因此读出来的数据还会延迟有一拍。但是为什么data_count和full信号还是只延迟了一拍才变化呢?因为这个输出寄存器存在FIFO内部外侧,数据读出FIFO后再经过的寄存器,因此data_count和full属于FIFO内部的信号,所以只延迟了一拍。
3.6 FWFT模式下,FIFO仿真测试以及信号解析
上面我们可以看到 Read Latency 变成了0 ,data_count变成了11位。实际上的FIFO深度为1026个。用同样的程序,打开仿真:
从仿真中我们可以看到,在1510ns时写入的第一个数据,延迟3拍后,输出到dout上。同时拉低了empty和almost_empty。
由于实际FIFO深度为1026个,所以在22030ns处的1027数据没有被写进去。在后面的22070ns处rd_en拉高,延迟一拍后,dout输出FIFO内部的第二个数据。符合预期。
3.7 标准读模式,读写位宽不一致的FIFO仿真测试以及信号分析
根据PG057手册,对称纵横比允许 FIFO 的输入和输出深度不同。支持以下写入读取宽高比:1:8、1:4、1:2、1:1、2:1、4:1、8:1,并且 FIFO 的输出深度根据输入深度和写入和读取宽度自动计算。
仅当可以写入或读取一个完整字时,满标志和空标志才有效。 FIFO 不允许访问部分字。例如,假设 FIFO 已满,如果写入宽度为 8 位且读取宽度为 2 位,则必须先完成四个有效的读取操作,然后才能完全置低并接受写入操作。写入数据计数根据写入端口比率显示 FIFO 字数,读取数据计数根据读取端口比率显示 FIFO 字数。
3.7.1 写读宽度1:4的FIFO测试
上图是长宽比为 1:4 的 FIFO (写入宽度 = 2,读取宽度 = 8)。由图可以看出在执行读操作之前执行了四个连续的写操作。第一个写入操作是 01,然后是 00、11,最后是 10。存储器从左到右(MSB 到 LSB)填满。执行读操作时,接收到的数据为01_00_11_10。时序图如下:
修改FIFO为写宽度为2,读宽度为8。
由上图可以看出,设置写读宽度为1:4后,写深度为1024,读深度变为为256,因为每写入4个数据后,才能读一个数出来。
在1510ns时开始写入01、10、11、00;在1570ns时刻写入第四个数据后,后一拍rd_count开始累加。后续写数据端每写入四个,rd_count累加1,符合预期。接下来我们看读出端:
在22030ns时开始拉高读使能,后一拍出的数据为01101100,符合预期。
3.7.2 写读宽度4:1的FIFO测试
上图显示写读宽度为 4:1 的 FIFO(写入宽度为 8,读取宽度为 2)。写操作是11_00_01_11。执行读操作时,数据从左到右(MSB 到 LSB)接收。如上图所示,第一次读取结果为数据 11,随后是 00、01,最后是 11,时序图如下:
修改FIFO为写宽度为8,读宽度为2。
由上图可以看出,设置写读宽度为4:1后,写深度为256,读深度变为为1024,因为每写入1个数据后,可以读4个数出来。
在1510ns时开始写入00000001后,后一拍rd_count开始累加4。后续写数据端每写入1个,rd_count就累加4,符合预期。接下来我们看读出端:
在6670ns拉高rd_en,后面读出来的数据是00、00、00、01 符合预期。
四、异步FIFO的不同配置以及仿真测试
4.1 打开 FIFO Generrator
各选项的解释与同步FIFO一致,这里不再赘述。
4.2 端口设置
4.3 标志信号设置
4.4 读写数据数量信号设置
4.5 关于FIFO异步复位需要注意的问题
根据《PG057》数据手册,在复位时,读写时钟必须都存在。如果出于任何原因,读或者写时钟在复位时丢失,则必须在读写时钟都存在时再次复位。否则可能会导致FIFO的意外行为。有时rst_busy信号可能会被卡住,可能需要重新配置 FPGA。
如果异步复位是一个最慢时钟宽度,并且断言发生在非常接近最慢时钟的上升沿时,则复位检测可能无法正确发生,从而导致意外行为。为了避免这种情况,始终建议将异步复位置位至少 3 个最慢的时钟周期
。以下是官方给出的推荐复位时序图:
No Access Zone期间的所有 FIFO 输出均应被视为无效。
由以上时序图可以看出:
- 在拉高RST后延迟了7个WR_CLK周期,FIFO将WR_RST_BUSY信号拉高;
- 再延迟1个WR_CLK周期将FULL信号拉高;
- 从WR_RST_BUSY拉高开始直到FULL信号拉低后一个周期内,wr_en信号应该为低电平。
- 在拉高RST后延迟了7个RD_CLK周期,FIFO将RD_RST_BUSY信号拉高;
- 再延迟1个RD_CLK周期将EMPTY信号拉高;
- 从RD_RST_BUSY拉高开始直到FULL信号拉低后一个RD_CLK周期内rd_en信号应该为低电平;
- DOUT信号在RD_RST_BUSY拉高开始直到EMPTY信号拉低后一个RD_CLK内都无效。
- 在复位周期加上60个慢时钟周期后,FIFO可以正常读写
4.5.1 FIFO复位时,读或者写时钟丢失的情况仿真
按照上面的步骤配置好异步FIFO后,例化,写入控制程序如下:
`timescale 1ns / 1ps
module my_dcfifo_test(
input wr_clk , //输入写时钟
input rd_clk , //输入写时钟
input rst_n //系统复位
);
wire [17:0] dout ; //FIFO读数据
wire wr_rst_busy ; //写复位安全信号
wire rd_rst_busy ; //读复位安全信号
wire full ; //FIFO满信号
wire empty ; //FIFO空信号
wire almost_full ; //FIFO几乎满信号
wire almost_empty ; //FIFO几乎空信号
wire [9:0] rd_data_count ; //FIFO读数据数量
wire [9:0] wr_data_count ; //FIFO写数据数量
reg srst ; //写时钟端给的FIFO复位
reg drst ; //读时钟端给的FIFO复位
wire fifo_rst ;
reg [17:0] din ; //FIFO写数据
reg wr_en ; //FIFO写使能
reg rd_en ; //FIFO读使能
reg [2:0] wr_state ; //写FIFO状态机
reg [2:0] rd_state ; //读FIFO状态机
reg [7:0] wr_cnt ; //写数据的计数器
reg [7:0] wait_cnt ;
always @(posedge wr_clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
wr_state <= 'd0;
srst <= 1'b1;
din <= 'd0;
wr_en <= 1'b0;
wr_cnt <= 'd0;
end
else case (wr_state)
0: if(wr_rst_busy == 1'b0) begin //等待复位完成
wr_state <= 'd1;
end
else begin
wr_state <= 'd0;
srst <= 1'b0;
end
1:
if(wr_cnt == 'd1033)begin// 如果写到某个数后,突然给个复位信号
srst <= 1'b1;
wr_state <= 'd0;
wr_en <= 1'b0;
wr_cnt <= wr_cnt + 1'b1;
end
else if(full == 1'b0)begin //如果数据没满,则一直写累加的数据
wr_en <= 1'b1;
din <= din +1'b1;
wr_cnt <= wr_cnt+ 1'b1;
wr_state <= 'd1;
end
else begin
wr_en <= 1'b0;
din <= din;
wr_state <= 'd2;
srst <= 1'b0;
end
2: wr_state <= 'd2;
default: wr_state <= 'd0;
endcase
end
always @(posedge rd_clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
rd_state <= 'd0;
rd_en <= 1'b0;
drst <= 1'b0;
wait_cnt <= 'd0;
end
else case (rd_state)
0: if(wait_cnt == 'd10) begin //如果读时钟存在,则复位FIFO 10个周期
rd_state <= 'd1;
wait_cnt <= 'd0;
drst <= 1'b0;
end
else begin
drst <= 1'b1;
wait_cnt <= wait_cnt +1'b1;
rd_state <= 'd0;
end
1: if(rd_rst_busy == 1'b0) begin //等待复位完成
rd_state <= 'd2;
end
else begin
rd_state <= 'd1;
end
2: if(wait_cnt[7] == 1'b1) begin //等待一段时间,写数据
rd_state <= 'd3;
wait_cnt <= 'd0;
end
else begin
wait_cnt <= wait_cnt + 1'b1;
rd_state <= 'd2;
end
3: if(empty == 1'b0 )begin //如果数据没空,则一直读
rd_en <= 1'b1;
rd_state <= 'd3;
end
else begin
rd_en <= 1'b0;
rd_state <= 'd3;
end
default: rd_state <= 'd0;
endcase
end
assign fifo_rst = srst | drst;
dc_fifo_test your_instance_name (
.rst (fifo_rst),
.wr_clk (wr_clk),
.rd_clk (rd_clk),
.din (din),
.wr_en (wr_en),
.rd_en (rd_en),
.dout (dout),
.full (full),
.almost_full (almost_full),
.empty (empty),
.almost_empty (almost_empty),
.rd_data_count (rd_data_count),
.wr_data_count (wr_data_count),
.wr_rst_busy (wr_rst_busy),
.rd_rst_busy (rd_rst_busy)
);
endmodule
仿真结果如下:
由上图仿真结果可知,在FIFO复位期间,如果读写时钟没有同时存在的话,导致FIFO一直处于异常状态,此时的FIFO无法进行读写操作。根据上述官方手册所说,后续在读写时钟都存在时候,再给FIFO进行复位,FIFO就能恢复正常了,结果如下:
由上图仿真结果可知,在415ns以内,没有读时钟导致FIFO复位异常;在415ns后,读写时钟都有时再次复位FIFO,在1170ns以后就能正确的读写FIFO了。
4.5.2 FIFO复位时,RST复位周期小于3个慢时钟周期的情况仿真
我们修改一下写状态机代码,使其读写时钟都有,然后快时钟只给一个周期的复位:
always @(posedge wr_clk or negedge rst_n) begin
if(rst_n == 1'b0)begin
wr_state <= 'd0;
srst <= 1'b0;
din <= 'd0;
wr_en <= 1'b0;
wr_cnt <= 'd0;
end
else case (wr_state)
0: if(wr_rst_busy == 1'b0) begin //等待复位完成
wr_state <= 'd1;
srst <= 1'b0;
end
else begin
wr_state <= 'd0;
srst <= 1'b0;
end
1:
if(wr_cnt == 'd133)begin// 如果写到某个数后,突然给个复位信号
srst <= 1'b1;
wr_state <= 'd0;
wr_en <= 1'b0;
wr_cnt <= wr_cnt + 1'b1;
end
else if(full == 1'b0)begin //如果数据没满,则一直写累加的数据
wr_en <= 1'b1;
din <= din +1'b1;
wr_cnt <= wr_cnt+ 1'b1;
wr_state <= 'd1;
end
else begin
wr_en <= 1'b0;
din <= din;
wr_state <= 'd2;
srst <= 1'b0;
end
2: wr_state <= 'd2;
default: wr_state <= 'd0;
endcase
end
仿真结果如下:
在4690ns时,写入端以及写了133个数据,突然给了一个快时钟周期的复位,后续FIFO也出现了异常。
4.6 标准读模式,读写位宽一致的FIFO仿真测试以及读写域信号分析
按照上述的配置,可控制代码。我们打开仿真观察:
从仿真可以看出,当FIFO复位完成后,状态机开始写FIFO操作,为什么读写count不一致呢?我们一步步分析:
-
在①时刻2450ns,写入了第一个数据1,然后在延迟wr_clk两个周期后,wr_count开始累加,后面以此类推。
-
在②时刻2505ns,rd_clk上升沿采集到wr_data_count等于1,然后延迟rd_clk三个周期后,rd_count输出1。
-
在③时刻2535ns,rd_clk上升沿采集到wr_data_count等于3,然后延迟rd_clk三个周期后,rd_count输出3。
-
在④时刻2565ns,rd_clk上升沿采集到wr_data_count等于4,然后延迟rd_clk三个周期后,rd_count输出4。
-
在2595ns,rd_clk上升沿采集到wr_data_count等于6,然后延迟rd_clk三个周期后,rd_count输出6。
后续以此类推,因此单靠rd_data_count和wr_data_count来判断FIFO内还剩多少数是不准确的,只能估计一个大概。如果想要使用更准确的计数来作为逻辑判断,我们可以打开可编程的空满信号来作为判断的标准,如下:
4.6.1 programmable flag仿真测试
打开可编程满信号设置后,我们打开仿真:
例如我想要在FIFO内有512个数据时开始读操作,如果用rd_clk去判断rd_data_count=512,由图可以看出,rd_data_count并没有=512,因此该条件就不会触发。所以,在自定义空满标准信号后,在wr_data_count=512时,拉高prog_full,rd_clk只需要判断此信号是否为高,就能够操作后续逻辑了。
我们设置的当FIFO内部wr_data_dount小于300时候,拉低prog_full信号,因此用户可以通过自定义prog_full和prog_empty信号的范围,就能很好的更准确的判断。
五、使用FIFO的注意事项总结
- 在使用小容量FIFO时,可以选择分布式ram构成的FIFO,这样比较节省block ram的资源。大容量的FIFO优先使用block ram;
- 读写宽度不一致时,注意先出高位,再出低位;
- FIFO异步复位时,读写时钟必须都存在;
- 异步复位至少复位三个慢时钟周期;
- 复位期间不能进行读写操作;
- 复位拉低后,等待60个周期或者读写安全信号拉低后,才进行读写操作;
- FIFO的读写数量不准,建议自己对写入的数据进行计数,或者使用可编程的空满信号加以判断;
参考
《PG057》