文章目录
背景
目前关于FIFO,我还没有实战用到,不过这个也是一名FPGA工程师必须要掌握的,这里来恶补一下,以方便以后哪天万一用到,况且最近笔试、面试,FIFO都会被问到。
1、FIFO的介绍
FIFO(FIRST Input First Output) 即先进先出队列,在计算机中先进先出队列是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。
FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端是采样速率比较慢的一个接口,另一端是擦痒速度比较快的一个接口,如果我们直接将这两个接口相连接,那么就会出现各种问题。如何解决呢?我们就可以在这两个不同的时钟域之间采用FIFO来作为数据缓冲。
另外对于不同宽度的数据接口,也可以用FIFO,比如一端的接口输出数据是8位,而另一端输出数据可能是16位,我们就可以在这两个不同宽度的数据接口中使用FIFO来达到数据匹配。
这里以同步FIFO IP核为例进行讲解。(注意:同步FIFO的读写时钟是同一个时钟)
FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端时AD数据采集,另一端时计算机的PCI总线,假设其AD采集的速率为16位 100K SPS,那么每秒的数据量为100K×16bit=1.6Mbps,而PCI总线的速度为33MHz,总线宽度32bit,其最大传输速率为1056Mbps,在两个不同的时钟域间就可以采用FIFO来作为数据缓冲。另外对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
读指针:指向下一个读出地址。读完后自动加1。
写指针:指向下一个要写入的地址的,写完自动加1。
读写指针其实就是读写的地址,只不过这个地址不能任意选择,而是连续的。
2、IP核的配置
从配置的第二个界面,我们可以看到支持的类型;
前三个代表使用共同的时钟,后面三个代表使用独立的读写时钟。
上面第三个页面,有两个标签需要我们配置:“Read ”模式和端口参数
在read mode中:standard FIFO(标准FIFO):FIFO的读数据延迟读请求一个时钟周期输出;
First word fall through:FIFO的读数据在读请求的同一个时钟周期内输出。
另外,可以设置读取深度和读取宽度;
在optional flags栏中:
Almost FULL flag (几乎满了标志位):表示FIFO里存的数据还差一个就满了;
Almost Empty Flag(几乎空了标志位):表示FIFO里面存的数据只剩一个了;
W rite Port Handshaking 栏中:
write Acknowledga Flag:表示一次成功的写操作标志位;
Overflow Flag:表示一次无效的写操作标志位(溢出了)
在“Read Port Handshaking”栏中;
“valid flag”表示该端口的数据是有效的;
“underflow flag”表示一次无效的读操作标志位
在这个页面中,有七个标签徐雅我们配置。
“Reset Pin”:给FIFO IP核设置一个复数引脚。
Synchrnous Reset(同步复位):复位信号在时钟的上升呀且复位信号为低电平时有效;
Asynchronous Reset(异步复位):复位信号在复位信号的下降沿时有效。
“Full Flags Reset Value”设置全部标志位信号的复位值
“Use Dout Reset Value”:设置读端口数据的复位值,默认为0;
最终形成FIFO的详细报告单
另外,我们可以看到,其实FIFO IP核和我们的RAM IP核使用方法是差不多的,在RAM IP核中我们需要写使能、写数据、读使能、读数据以及时钟和地址,在我们的FIFO IP核中,我们则需要写使能、写数据、读使能、读数据、时钟、空表示、满表示、以及数据计数等等,如果我们去掉这些标志信号,我么你可以看到我们的FIFO IP 核剩下的信号是和RAM IP核一样的,唯一不同的是我们的RAM IP核读写操作需要地址,我们的 FIFO核就不需要使用地址,只要使能信号打开,时钟到来,数据就可以读出和写入,比我们的RAM IP核使用起来简单。
3、同步FIFO工程实例
下面我以计数器产生数据写入,只需要控制wr_en和re_en即可
`timescale 1ns / 1ns
//
module FIFO_top(
input clk,
input rst,
output [7:0] dout, // output [7 : 0] dout
output full, // output full
output almost_full, // output almost_full
output wr_ack, // output wr_ack
output overflow, // output overflow
output empty, // output empty
output almost_empty, // output almost_empty
output valid, // output valid
output underflow, // output underflow
output [4:0]data_count // output [4 : 0] data_count
);
reg [7:0] timecnt=0;
reg [7:0] din=0;
wire wr_en;
wire rd_en;
reg wr_en_reg=0;
reg rd_en_reg=0;
always @ (posedge clk or negedge rst)
begin
if(!rst)
din<=0;
else
din<=timecnt;
end
always @ (posedge clk or negedge rst)
begin
if(!rst)
timecnt<=0;
else if(timecnt==8'd69)
timecnt<=1'b0;
else
timecnt<=timecnt+1;
end
//用于产生FIFO IP核的写使能信号
//组合电路,用于产生写使能信号
always @ (posedge clk or negedge rst)
begin
if(!rst)
begin
wr_en_reg<=0;
rd_en_reg<=0;
end
else
begin
wr_en_reg <=(timecnt>=8'd4&&timecnt<=8'd35)?1'b1:1'b0;
rd_en_reg <=(timecnt>=8'd38&&timecnt<=8'd69)?1'b1:1'b0;
end
end
assign wr_en=wr_en_reg;
assign rd_en=rd_en_reg;
FIFO_IP FIFO_IP_inst (
.clk(clk), // input clk
.rst(rst), // input rst
.din(din), // input [7 : 0] din
.wr_en(wr_en), // input wr_en
.rd_en(rd_en), // input rd_en
.dout(dout), // output [7 : 0] dout
.full(full), // output full
.almost_full(almost_full), // output almost_full
.wr_ack(wr_ack), // output wr_ack
.overflow(overflow), // output overflow
.empty(empty), // output empty
.almost_empty(almost_empty), // output almost_empty
.valid(valid), // output valid
.underflow(underflow), // output underflow
.data_count(data_count) // output [4 : 0] data_count
);
endmodule
从上图中可以看到虽然已经开启了写使能,但是在复位后,需要4个时钟的恢复时间,即使写了,写没写进去。
然后data_count会延迟一个周期,empty也是延迟一个周期。所以写进去的数开始应该是4.
但是如果没复位,直接打开write写使能,是只需要一个时钟的;
从上图,可以看出第一次读取数据是4,并且在输出4的同时,data_count也计数为31(原本为32(即0,0就是32(因为只有5个bit位宽)))
full拉低,almost full在被读走一个数据后拉低;
工程链接:
以上是同步FIFO,也就是读写使用的是一个时钟
下面介绍异步FIFO,也就是读写非一个时钟
4、异步FIFO
1、读写指针的概念
- 读指针:
总是指向下一个将要被写入的单元,复位时指向第一个单元。 - 写指针:
总是指向当前要被读出的数据,复位时指向第一个单元。
也就是说,复位时读写指针都指向第一个单元。并且向FIFO写入一个数据,写指针加1。从FIFO中读出一个数据,都指针加1。
FIFO设计的关键是如何产生可靠的读写指针和满空信号。FIFO读写指针的工作原理如上第四点所述。
2、FIFO满空标志的产生
那么剩下的就是要讨论如何产生FIFO的满空信号了。FIFO什么时候为空呢?我们来思考一下,假设我从第一个单元写入数据,那么写指针从地址0—>1,读指针不变,此时FIFO中有一个数据。接着我把这个数据读出来,读指针从0—>1。此时写指针为1,读指针也为1,FIFO中没有数据了,因此FIFO为空。从中可以发现,判断FIFO为空很简单,只要读写指针相等就是空。但是事情好像也没那么简单,再想一下,假设一开始就复位,读写指针都在0地址,然后一直王FIFO中写入数据,当写满FIFO的时候,写指针刚好转了一圈回到了0地址,此时读写指针也相等,但是这时候FIFO是满的。因此得到下面的判空和判满条件。
判空:读指针追上写指针的时候,两者相等,为空。
判满:写指针追上读指针的时候,两者相等,为满。
突然发现两者相等的话不是空就是满,区别就是谁追上谁而已了。那么如何来区别是谁追上谁呢?
3、如何判断读写指针相等时候,为空还是为满呢?
答案就是在表示读写指针的数据位宽上再加1位来区分是满还是空。比如FIFO的深度位8,那么需要3位二进制数来表地址,则需要再最高之前再加一位,变成4位。一开始读写都是0000,FIFO为空。当写指针增加并越过最后一个存储单元的时候,就将这个最高位取反,变成1000。这时是写地址追上了读地址,FIFO为满。同理,当读地址越过最后一个存储单元的时候把读地址的最高位也取反。可以看到,当最高位相同,并且剩下的位也相同的时候FIFO为空;
4、异步时钟域下如何判断是空还是满?
上述已经解释了如何按断FIFO的满空,是根据地址是否相等的方法,就是深度比如有8位,那么指针应该有4位
如果读的指针等于写的指针(同时读同时写),那么二者相等,为空。
如果读的指针为0000,写的指针为1000,那么写追上读。此时为满。
如果写的指针为0000,读的指针为1000,那么读追上写,此时为空。
-----------------------我是分界符-----------------------------
但是在上述问题中,读写时钟需要一致同步,才能判断。如果是在不同的时钟域下,显然需要将读写指针进行同步化才可以进行判断。具体就是在判断空的时候,需要将写地址同步到读时钟域下进行判断。同理,在进行判断满的时候需要将读时钟域下的读指针同步到写时钟域进行判断。
5、使用格雷码来表示指针
其实在读时钟域中读指针的增加仍然是自然二进制,同理,在写时钟域中写地址的增加也是按照自然二进制变化的。但是在将读指针发送到写时钟域下进行同步时,如果仍然采用自然二进制,那么就会面临地址同时有多位变化的情况,这样会容易引起亚稳态或者毛刺。