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 depthactual 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深度,以避免丢失数据。