FIFO的使用非常广泛,一般用于不同时钟域之间的数据传输,比如FIFO的一端是AD数据采集,另一端是计算机的PCI总线,假设其AD采集的速率为16位100K SPS,那么每秒的数据量为100K×16bit=1.6Mbps,而PCI总线的速度为33MHz,总线宽度32bit,其最大传输速率为33*32=1056Mbps,在两个不同的时钟域间就可以采用FIFO来作为数据缓冲。另外对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
本文就讲通过ISE软件生成一个FIFO,并对其进行一些操作以求更加了解FIFO中各个信号的作用以及控制方法(有些简单步骤将省略不提)。
在这步中,由于现在做的不是soc工程,选择Native.
Next后,需选择时钟和存储器类型。1,时钟,由FIFO的作用可知大部分都是读写不同步的,这里我们也选择异步模式,即读写的时钟不同。2,存储器类型,这里主要是block RAM和distribute RAM之间的区别。简而言之,block RAM是FPGA中定制的ram资源,而distribute RAM则是由LUT构成的RAM资源。由此区别表明,当FIFO较大时应选择block RAM,当FIFO较小时,选择distribute RAM.另外一个很重要的就是block RAM支持读写不同宽度,而distribute不支持。在这里为了更全面的了解FIFO,选择block RAM以拥有非对称方向速率的特性。
读模式有两种选择,一般选择标准模式,至于First-Word Fall-Fhrough的含义请查看FIFO手册。写数据宽度定义为8位,写深度定义为256.读宽度定义为4位,而读深度将根据以上几个参数自动计算。但我们需要注意的是,在data port parameters处,有actual write depth和actual read depth,他们都比我们设置的要小,其意义以及原因将在例程中说明。
之后便是添加信号,信号越多越难操作,但同时也能让我们更加准确的控制FIFO,这里为了更好的了解FIFO,把所有能选的信号都选上。
接下来是复位信号以及可编程信号的配置。虽然在block RAM和distribute RAM中,复位信号不是必需的,但根据习惯,还是启用rst端口,且配置成同步复位,即整个FIFO共用一个复位信号,而不是读写不同的复位。至于编程信号,看选项就很容易理解了。配置如图,FIFO中数据达到200时,programmable full有效,数据为10时,programmable empty有效。
之后是写计数和读计数,都使之有效,由于写深度是256,读深度是512.因此写计数器的宽度定义为8,读计数器的狂度定义为9.其实不一定计数器一定要比深度大,当计数器计数最大值小于数据深度时,例如数据深度为512,而计数器大小为256,则每两个数据计数器计数一次。
最后可以看到我们配置的FIFO信息如下(注意几个关键信息),最后生成IP就好了。
通过点击View HDL Instantiation Template,我们可以看到所有需要例化的信号,以及格式。
在该FIFO例程中,首先是将1-255写入FIFO中,此时不读取,观察各个信号,然后再从FIFO中读出FIFO中存储的数据,此时不再写入,观察各信号。
代码如下:
module FIFO_top(
input clk,rst,
output wire [3:0] dout
);
wire clk_50M_wire;
wire [7:0] din_wire;
wire valid,wr_ack;
wire overflow,underflow;
wire almost_empty,almost_full;
wire [8:0] rd_data_count;
wire [7:0] wr_data_count;
wire prog_full,prog_empty;
wire wr_en,rd_en;
wire full,empty;
///////////////////////////////////////////////////
//二分频电路
//100M为读时钟,50M为写时钟
reg clk_50M;
assign clk_50M_wire = clk_50M ;
always @(posedge clk or posedge rst) begin
if (rst) clk_50M<=0;
else clk_50M<=~clk_50M;
end
///////////////////////////////////////////////
////////////////////////////////////////////////
reg [2:0] cnt;
assign wr_en =(full==0 && rd_en==0 && cnt==5)?1:0 ;//非满时写,且满后就不再写了,即便之后数据被读取导致非满
assign rd_en = (empty==0 && wr_en==0)?1:0 ;//写时不读取,写完再读取
reg [7:0] din;
assign din_wire = din ;
always @(posedge clk_50M or posedge rst) begin
if (rst) begin
din<=1;
end
else begin
if(wr_en) din<=din+1;
else din<=din;
end
end
always @ (posedge clk_50M or posedge rst)
if(rst) cnt<=0;
else begin
if(cnt==3'd5) cnt<=cnt;
else cnt<=cnt+1;
end
FIFO FIFO (
.rst(rst), // input rst
.wr_clk(clk_50M_wire), // input wr_clk 50M
.rd_clk(clk), // input rd_cFIFOlk 100M
.din(din_wire), // input [7 : 0] din
.wr_en(wr_en), // input wr_en
.rd_en(rd_en), // input rd_en
.dout(dout), // output [3 : 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
.rd_data_count(rd_data_count), // output [8 : 0] rd_data_count
.wr_data_count(wr_data_count), // output [7 : 0] wr_data_count
.prog_full(prog_full), // output prog_full 200
.prog_empty(prog_empty) // output prog_empty 10
);
endmodule
1,复位信号rst: 由结果可知,其为高电平有效,且复位后其他信号的初始值是可以在产生FIFO中配置的,之前配置为0;很重要的一点是,复位后的几个写周期内(2,3个周期)是无法进行写操作的,所以在本例程中,复位一段时间后再拉高wr_en以确保首先写入的是1.
2,写使能信号wr_en与写响应信号wr_ack:关于该信号,很重要的一点是,写入的值是wr_en拉高时的值;还是说当wr_en拉高后,下一周期才能进行写操作?如果是前者,由波形所示,写入的第一个值应该是1;如果是后者,写入的应该是2。这就需要根据读取的第一个值来判断了。而经查看,读取的第一个值为1,也就是说,只要wr_en拉高,立马就进行写操作。
从下图还能明白wr_ack的工作模式,即写入成功时,wr_ack将在下一周期拉高。也就是说,wr_ack反映的是上一周期的写操作。
3,读使能信号rd_en与读响应信号valid:在读操作中,第一个读取的数据应该是0,第二个是1(原因之后解释)。由波形可知,当rd_en有效的那个上升沿,并没有进行读操作,而是在下一个周期才真正读取了数据,同时valid被拉高,这是与写操作所不同的地方。
4,写计数wr_data_count和rd_data_count:因为写数据会有256个,读数据会有512个,一旦count的大小不够,count从一开始就会失效,成为高阻态,所以应该给wr_data_count设置成8位,rd_data_count设置成9位。当把wr_data_count设置成7位,rd_data_count设置成8位时,结果见图。
正常设置时,即wr_data_count设置成8位,rd_data_count设置成9位。
在写的过程中,可以看到,wr_data_count正常计数,每次加一,但是其值滞后2个周期。而由于读操作是每次4位,写操作是每次8位,即每次写操作都意味着需要读两次才能读出数据,所以每次写操作,rd_data_count都是加2。
在读过程中,rd_data_count是每次减1。同理,wr_data_count则是每2次读操作才减1。
对于这两个信号,也有不太正常的地方。如下图,当进行了读操作的时候,wr_data_count依旧保持在255不变,rd_data_count则在505和504之间切换,且其最大值不是预期的510.
可能的原因在于wr_data_count是属于写时钟域的,读操作进行后需要一段时间才能反映到写时钟域的各个参数,这在之后的empty等信号也可以得出类似结论。
至于rd_data_count应该是受读操作以及full或者wr_data_count等信号的共同影响,导致其在505和504之间不停变换。
官方文档也提到说,wr_data_count以及rd_data_count是大概的,不是非常准确。
5,prog_full;almost_full;full:Prog_full在之前的设置中是200,该值是根据wr_data_count判定的,即当wr_data_count为200时,prog_full置一。但是由于wr_data_count滞后2个周期,所以真正写入到FIFO中的值应该有202个了。
full以及almost_full由图可知,在数据数满足要求后的下一个周期被拉高。而且当读操作进行后,这两个信号并不是立刻被拉低,和之前所提到的一样,这两个信号属于写时钟域,读操作反映到这两个信号上需要一定的时间
6,prog_empty;almost_empty;empty:
Prog_empty之前设置的是10,由波形图知,其信号还是比较准确的。
Almost_empty以及empty则提早了一个周期被拉高
也可以看到,当写操作进行时,empty等信号也不是立刻变低的,其原因也应该是属于不同时钟域。
7,underflow;overflow: 改动程序,使写信号一直有效,可以看到当full变高后的下一周期因为继续进行写操作,使得overflow也被拉高。
再改动程序,写操作一段时间后,读信号一直有效。可以看到当empty有效后,继续读操作,underflow将在下一周期被拉高。
8,关于FIFO实际读写深度的问题:在之前的设置中可以看到,我们原本设置的写深度是256,读深度是512;但是边上显示的实际读写深度分别是255,510.从结果中我们也可以看到:当写到255时(数据是1~255),full被拉高。读数据以及读数据深度是根据写数据和写深度来的,自然也就是510了。也就是说实际写深度会比设置的小1,这是在工程中需要注意的地方。
9,关于读写不对称的问题:所谓的读写不对称,即读写的位大小以及读写速率不一样。在本例程中,写入的数据是8位的,而读出的数据是4位的。那么读操作的时候是怎么样一个读出法呢?在写操作中,我们是顺序写入1~255的。通过观察读数据可知,读出的数据为:0,1,0,2,0,3……这就说明当读写非对称时,是先读取数据的高位的。
10,关于FIFO的深度计算问题:在很多笔试面试中,都会问的FIFO的深度计算问题。因此,了解这方面也是很有必要的。
网上有很多关于这方面的公式,在此就不做讨论了。其实很简单,只要先计算单位时间内读写数据量的差值,然后乘以持续时间,就是FIFO的最小深度了。但是这里还需要注意几点:
1,背靠背问题,假设写数据100个wr_clk内写入80个数据,这时就需要考虑最坏的情况,就是前20个时钟不写入,接着80个时钟写入,再后来的80个时钟继续写入,最后的20个时钟不写入。这样写入数据最集中的情况就有160个时钟写入160个数据了。
2,需要留有深度裕量,即实际的深度会比设置的小,因此应该多设置点FIFO深度,以避免丢失数据。