FIFO是First Input First Output的英文简写 代表先进的数据先出 ,后进的数据后出。
FIFO存储器是系统的缓冲环节,如果没有FIFO存储器,整个系统就不可能正常工作。
FIFO的功能可以概括为
(1)对连续的数据流进行缓存,防止在进机和存储操作时丢失数据;
(2)数据集中起来进行进机和存储,可避免频繁的总线操作,减轻CPU的负担;
(3)允许系统进行DMA操作,提高数据的传输速度。这是至关重要的一点,如果不采用DMA操作,数据传输将达不到传输要求,而且大大增加CPU的负担,无法同时完成数据的存储工作。
FIFO的写入和读出可以被认为是向一个蓄水池注水和放水,有的时候只是单纯的注水(写入),有的时候是只在放水(读出),有的时候是注水和放水同时进行的,但是由于注水和放水的时间是不同的。如果注水的时间大于放水的时间,那么总有注满的时候,如果再继续注水就会有溢出来的风险,为了防止溢出我们需要使用一个满信号约束注水阀门当在蓄水池蓄满以后就不在注水了。在另一种情况下,如果放水的时间多,那么总有一个时间开始蓄水池会被放空,那么需要一个空信号指示其不再放水了。
时钟:根据FIFO工作的时钟域分为同步/异步FIFO。同步FIFO是指读时钟和写时钟为同一个时钟在时钟沿来临时同时发生读写。异步FIFO读写时钟不一致,读写相互独立。
读写使能:告知FIFO控制器当前是否要进行读/写操作
写满信号:告知外部FIFO已经写满了停止写入数据
读空信号:告知外部FIFO已经读空了停止读数据
读写指针:指向内部的RAM,指向下一个要读取或者写入的位置
FIFO通常用于跨时钟域设计,所以说通常是双时钟设计。即读\写的时钟是分开来的。但是直接设计这样的系统对我们的有很高的要求,我企图由同步FIFO的设计,虽然同步FIFO在实际运用中运用范围有限但是其还是为我们的异步设计带来了启发。本人水平有限,文章有错误的地方希望大家不吝赐教。
1.Vivado 2019.2
2.Modelsim 2019.2
同步FIFO的设计中读写使用了同一时钟源,下面分享一下Vijay A. Nebhrajani大神的Asynchronous FIFO Architectures (Part 1)这一文章中给出的同步FIFO的设计框图:
下面简单得对此框图进行简单得说明
首先DPRAM Array表示的双口RAM模块构成的存储模块,双口RAM可以同时进行数据的读\写,互不干扰。这是为了方便数据的存储和输入,如果我们只使用一个单口RAM模块进行数据的读写,那么我们需要编写一个仲裁机制来确定在当前是哪个数据写入或者是哪个数据读出非常麻烦,而且很难满足同时读写的要求。
状态模块的作用是为了判断当前FIFO的状态,我们在FIFO的通俗解释中也介绍过FIFO中会出现写满和读空的状态。当出现写满状态的时候我们不能在写入了,否则新来的数据会覆盖掉还没有读出的数据,这样的设计是不满足FIFO的设计需求的。同样的,如果出现读空状态的话我们也不能控制FIFO输出数据了。Status模块的主要是通过联合判断写使能、读使能和FIFO内部的信息。给出此时FIFO是否能够输入/输出数据。上文提到的FIFO的满空判断是FIFO设计中比较重要的部分。将在下文中详细介绍。
在状态模块左右的是读写指针模块,读指针指向下一个将要读取的地址,写指针指向下一个将要写入的地址。读操作使得读地址增加,写操作使得写地址增加。值得说明的是当我们利用读写数据的时候一般使用先读取或者写入数据再增加读写指针的操作,这样也符合读写指针指向下一个将要读写地址的设定
FIFO的满空判断是FIFO设计中很重要的一个环节,他决定着FIFO是否会出现输入数据覆盖原有数据以及输出错误的数据情况。FIFO的满空判断主要有两种xia方法。
引入计数器来表示现在FIFO已经存入了多少数据。当写使能信号使能以后,此计数器+1,当读使能信号使能以后此计数器-1。读写使能信号同时使能和都不使能的情况下计数器不做变化。当此计数器为0的时候即为读空状态,当此计数器为FIFO最大深度的时候为写满状态。
通常情况下我们的FIFO读写指针位宽可以用下列公式表达:
log 2 ( m a x c o u n t ) \log_{2}(m a xcount) log2(maxcount)
maxcount表示FIFO可以容纳的最大的数据数量。再此位的基础上额外多添加一个位,也可以实现满空判断下面以一个最大数据量为4的FIFO为例进行说明
刚开始的时候读写指针同时指向第一个地址,此时读写指针的每一位均为一样的,此时是读空的情况,如下图所示:
当写指针开始移动,写指针的值和读指针不相同,此时读空状态解除,FIFO正常的写入
当写指针持续写入直到再次追上读指针的时候,此时应该为写满的情况,如果不采用额外的一位那么此时和第一次的写指针毫无区别,无法判断但是由于映入了额外的位,此时的写指针大小如下图所示:
我们可以发现此时引入的额外的位和原来的位是相反的,这个条件即为写满状态的判别条件。
综上所述:
1.此方法的写满状态判别状态是读写指针最高位相反,其余位相同。
2.读空状态判别条件是读写指针所有位相同。
module syn_fifo #
(
parameter DATA_WIDTH = 32,
parameter MAX_COUNT = 16
)
(
input clk ,
input rst ,
input rd_en , // 读使能
input wr_en , // 写使能
output reg fifo_full , // FIFO写满
output reg fifo_empty , // FIFO读空
output reg [DATA_WIDTH-1:0] data_out , // 数据输出
input [DATA_WIDTH-1:0] data_in // 数据输入
);
reg [DATA_WIDTH-1:0] buffer [MAX_COUNT-1:0]; // buffer 模拟双口RAM
parameter addr = $clog2(MAX_COUNT);
reg [addr:0] rd_addr;
reg [addr:0] wr_addr;
wire we;
wire re;
assign we = !fifo_full && wr_en;
assign re = rd_en && !fifo_empty;
always @(*)
begin
if(rd_addr == wr_addr)
fifo_empty = 1;
else
fifo_empty = 0;
end
always @(*)
begin
if(rd_addr[addr] != wr_addr[addr] && rd_addr[addr-1:0] == wr_addr[addr-1:0])
fifo_full = 1;
else
fifo_full = 0;
end
always @(posedge clk or negedge rst)
begin
if(~rst)
begin
rd_addr <= 0;
data_out <= 0;
end
else if(re)
begin
data_out <= buffer[rd_addr[addr-1:0]];
rd_addr <= rd_addr + 1;
end
end
always @(posedge clk or negedge rst)
begin
if(~rst)
wr_addr <= 0;
else if(we)
begin
buffer[wr_addr[addr-1:0]] <= data_in;
wr_addr <= wr_addr + 1;
end
end
`timescale 1ns / 1ps
module syn_fifo_TB();
// syn_fifo Parameters
parameter PERIOD = 20;
parameter max_count = 16;
parameter DATA_WIDTH = 32;
reg clk=0 ;
reg reset=0 ;
reg rd_en ;
reg wr_en ;
reg [DATA_WIDTH-1:0] data_in ;
wire fifo_full ;
wire fifo_empty ;
wire [DATA_WIDTH-1:0] data_out ;
syn_fifo #(
.DATA_WIDTH ( DATA_WIDTH ),
.MAX_COUNT ( max_count ))
u_syn_fifo(
//ports
.clk ( clk ),
.rst ( reset ),
.rd_en ( rd_en ),
.wr_en ( wr_en ),
.fifo_full ( fifo_full ),
.fifo_empty ( fifo_empty ),
.data_out ( data_out ),
.data_in ( data_in )
);
reg [5:0] cnt;
always@(posedge clk or negedge reset)
begin
if(~reset)
begin
wr_en <= 0;
rd_en <= 0;
data_in <= 0;
cnt <= 0;
end
else if(cnt < 10)
begin
//#2
wr_en <= 1;
rd_en <= 0;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else if(cnt < 12)
begin
//#2
wr_en <= 1;
rd_en <= 1;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else if(cnt < 18)
begin
//#2
wr_en <= 1;
rd_en <= 0;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else if(cnt < 23)
begin
//#2
wr_en <= 1;
rd_en <= 1;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else if(cnt < 39)
begin
//#2
wr_en <= 0;
rd_en <= 1;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else if(cnt < 50)
begin
//#2
wr_en <= 1;
rd_en <= 1;
data_in <= data_in + 1;
cnt <= cnt + 1;
end
else
begin
wr_en <= 0;
rd_en <= 0;
data_in <= 0;
cnt <= 0;
$stop;
end
end
initial
begin
forever #(PERIOD/2) clk=~clk;
end
initial
begin
#(PERIOD*2) reset = 1;
end
上一节中我们使用了双口RAM尝试构建了同步FIFO,在这一节中我将要将其推广到如何具有相互独立的时钟的读写场景下。上文中介绍了同步FIFO中的status模块,只有它同时操作读指针和写指针。当着两个指针工作在不同的时钟域上时,当我们打算用写时钟来采样读指针或用读时钟来采样写指针,将不可避免的遇到一个问题:亚稳态。亚稳态将导致空/满标志的计算错误,并导致设计的失败。
在数字电路中,每一位数据不是1(高电平)就是0(低电平)。当然对于具体的电路来说,并非1(高电平)就是1V,0(低电平)就是0V,对于不同的器件它们都有不同的对应区间。比方说对于某个器件来说,2.25-2.5V可以识别出来是高电平,0-0.25V可以识别出来是低电平,但是如果信号的电压处于0.25~2.25V之间,器件也就无法识别是高电平还是低电平(最终的结果可能是高电平也可能是低电平,无法预测),这种状态也就是亚稳态。如上述所说,亚稳态是一种设计上比较危险的状态。
亚稳态的在数字电路中的出现的主要原因是应为建立时间和保持时间不满足要求。
下面介绍一下建立时间和保持时间的概念:
建立时间:时钟沿到来之前数据所要保持稳定的时间。
保持时间:时钟沿到来之后数据所要保持稳定的时间。
如下图展示
整个亚稳态的形成可以用下列的例子说明
有以下的采样电路
实际的电路图如下:
在这个例子里,观察出现亚稳态的节点,clock1的上升沿对DATA采样,但是Q2在时钟clock2的情况下为Q1采样的时候由于Q1没有满足建立和保持时间所以就出现了亚稳态。
由于亚稳态的情况无法被避免,那么在设计电路时一定要:
让我们重新回到FIFO的设计上来,如果要用读时钟取样写计数器的值,这相对于写时钟来说是异步的。因此,到最后不得不考虑计数器的值到底在哪个范围变化,假定从 FFFF 到 0000。每个单独的位(bit)都处于亚稳态。这种变化意味着有可能读数为 0000 到FFFF之间(包含两者)的任何可能的值。当然这也说明该情况下 FIFO 将无法工作。同步可以保存处于亚稳态时的计数器取样,尽管
看似很离谱但仍然可以得到取样值。换句话说,仅靠同步是不够的。
重要的是我们必须确保不是所有的计数器位(bits)同时改变。所以我们引入格雷码,格雷码是最小汉明距离码,每一次变换只会变化1位bit。
让我们来分析一下格雷码(GRAY)对于 FIFO 的指针设计有什么作用。首先,同步意味着计数器的取样值很少处于亚稳态,其次,我们取样的值最多只会有一位发生错误。这就是说计数器的真实值从 N-1 变到 N,那么无论是否发生错误读取的数不是 N-1 就是 N,而不会是其它的值。由于在变化的那一时刻,必须确定输出的值是多少,这对于读出计数器值来说是完全正确的举动。只要能够确定读出的值是旧还是新就可以了。出现其它值则是不对的。如果进一步考虑,将会发现如果在改变值的瞬间取样计数器的值,两个答案(N-1,N)对于计数器的值都是正确的。
了解了这么多,接下来分析一下怎样将这些知识用于 FIFO 的读写指针操作。人们通常希望知道 FIFO 是否为满。如果它满了,必须阻止写操作再次发生。这很关键,因为当 FIFO 已满时,必须停止写指针加 1。将(格雷码的)读指针与写时钟同步。因为每当同步读指针的时候,实际的读指针可能会变为不同的值。这意味着读指针可能会是一个失效的值。如果是这样,从写操作的角度考虑会发生少读现象(相比实际情况),如果条件吻合,FIFO 为满。实际上,FIFO 可能未满,因为有可能读操作发生,而从写操作的角度是“看不到”的。然而,我们只要阻止额外的写操作就 OK 了。如当 FIFO 真的满了时我们不去阻止写操作将会出现错误。同样的从读操作的角度看——实际上当 FIFO 中还有一些数据时,读操作一方看到“被延迟的”写操作,可能会认为 FIFO 为空。这种情况读操作被阻止直到写操作“变得可被读操作一方所看见”,它将不允许进一步的读操作。使用格雷码和同步的方式处理读写指针可能会牺牲一些FIFO的性能,但是不会有危险的指针操作。
assign grey = bin^ (bin>> 1);//二进制转格雷码
// 格雷码转二进制
integer i;
always @ (grey)
begin
bin[addr]=grey[addr];
for(i=addr-1;i>=0;i=i-1)
bin[i]=grey[i+1]^grey[i];//行为级描述,综合成组合逻辑
end
可以看到下方的时钟同步结构,其余的和同步FIFO的设计大同小异
module asy_fifo
#
(
parameter DATA_WIDTH = 16,
parameter FIFO_DEPTH = 16
)
(
input rst_n , // 复位,低电平有效
// FIFO 写端口
input wclk , // 写时钟
input [DATA_WIDTH-1:0] w_data , // 写数据
input w_en , // 写使能
output w_full , // 写满标志位
// FIFO 读端口
input rclk , // 读时钟
input r_en , // 读使能
output reg [DATA_WIDTH-1:0] r_data , // 读数据
output r_empty // 读空标志位
);
parameter addr = $clog2(FIFO_DEPTH);
reg [DATA_WIDTH-1:0] buffer [FIFO_DEPTH-1:0];
reg [addr:0] w_ptr ; // 写指针寄存器
reg [addr:0] w_ptr_grey_wq ; // 写指针格雷码,写时钟打一拍
reg [addr:0] w_ptr_grey_rq1 ; // 写指针格雷码,读时钟打一拍
reg [addr:0] w_ptr_grey_rq2 ; // 写指针格雷码,读时钟打两拍
reg [addr:0] w_ptr_bin_rq ; // 写指针二进制码,读时钟域
wire [addr:0] w_ptr_grey ; // 写指针格雷码
reg [addr:0] r_ptr ; // 读指针寄存器
reg [addr:0] r_ptr_grey_rq ; // 读指针格雷码,读时钟打一拍
reg [addr:0] r_ptr_grey_wq1 ; // 读指针格雷码,写时钟打一拍
reg [addr:0] r_ptr_grey_wq2 ; // 读指针格雷码,写时钟打两拍
reg [addr:0] r_ptr_bin_wq ; // 读指针二进制码,写时钟域
wire [addr:0] r_ptr_grey ; // 读指针格雷码
assign w_ptr_grey = w_ptr ^ (w_ptr >> 1); // 产生写指针的格雷码
integer rst_ram_cnt;
always @( *)
begin
if(~rst_n)
begin
for(rst_ram_cnt=0;rst_ram_cnt<FIFO_DEPTH;rst_ram_cnt=rst_ram_cnt+1)
buffer[rst_ram_cnt] = 0;
end
end
integer i;
always @ (r_ptr_grey_wq2) //读指针格雷码转写域读指针
begin
r_ptr_bin_wq[addr]=r_ptr_grey_wq2[addr];
for(i=addr-1;i>=0;i=i-1)
r_ptr_bin_wq[i]=r_ptr_grey_wq2[i+1]^r_ptr_grey_wq2[i];//行为级描述,综合成组合逻辑
end
assign r_ptr_grey = r_ptr ^ (r_ptr >> 1); // 产生读指针的格雷码
integer j;
always @ (w_ptr_grey_rq2) //写指针格雷码转读域写指针
begin
w_ptr_bin_rq[addr]=w_ptr_grey_rq2[addr];
for(j=addr-1;j>=0;j=j-1)
w_ptr_bin_rq[j]=w_ptr_grey_rq2[j+1]^w_ptr_grey_rq2[j];//行为级描述,综合成组合逻辑
end
// 写跨时钟域打拍
always @(posedge wclk or negedge rst_n)
begin
if(~rst_n)
w_ptr_grey_wq <= 0;
else
w_ptr_grey_wq <= w_ptr_grey;
end
always @(posedge rclk or negedge rst_n)
begin
if(~rst_n)
w_ptr_grey_rq1 <= 0;
else
w_ptr_grey_rq1 <= w_ptr_grey;
end
always @(posedge rclk or negedge rst_n)
begin
if(~rst_n)
w_ptr_grey_rq2 <= 0;
else
w_ptr_grey_rq2 <= w_ptr_grey_rq1;
end
// 读跨时钟域打拍
always @(posedge rclk or negedge rst_n)
begin
if(~rst_n)
r_ptr_grey_rq <= 0;
else
r_ptr_grey_rq <= r_ptr_grey;
end
always @(posedge wclk or negedge rst_n)
begin
if(~rst_n)
r_ptr_grey_wq1 <= 0;
else
r_ptr_grey_wq1 <= r_ptr_grey;
end
always @(posedge wclk or negedge rst_n)
begin
if(~rst_n)
r_ptr_grey_wq2 <= 0;
else
r_ptr_grey_wq2 <= r_ptr_grey_wq1;
end
// 写满标志位判断
assign w_full = (w_ptr[addr] == ~r_ptr_bin_wq[addr] && w_ptr[addr-1:0] == r_ptr_bin_wq[addr-1:0]);
// 读空标志位判断
assign r_empty = (r_ptr == w_ptr_bin_rq);
// 写地址以及写数据操作 ------写时钟域
always @(posedge wclk or negedge rst_n)
begin
if(~rst_n)
w_ptr <= 0;
else if(w_en & ~w_full)
begin
w_ptr <= w_ptr + 1;
buffer[w_ptr[addr-1:0]] <= w_data;
end
end
// 读地址变化以及读数据操作 ------读时钟域
always @(posedge rclk or negedge rst_n)
begin
if(~rst_n)
r_ptr <= 0;
else if(r_en & ~r_empty)
begin
r_ptr <= r_ptr + 1;
r_data <= buffer[r_ptr[addr-1:0]];
end
end
endmodule //asy_fifo
`timescale 1ns/1ps
module asy_fifo_TB();
parameter DATA_WIDTH = 16;
parameter WRITE_FRE = 100; //unit MHz
parameter READ_FRE = 50; //unit MHz
reg sys_rst = 0;
reg write_clk = 1;
reg read_clk = 1;
always begin
#(500/WRITE_FRE) write_clk = ~write_clk;
end
always begin
#(500/READ_FRE) read_clk = ~read_clk;
end
//Instance
wire w_full;
wire [DATA_WIDTH-1:0] r_data;
wire [3:0] r_use;
wire [3:0] w_use;
wire r_empty;
reg [DATA_WIDTH-1:0] w_data=0;
reg w_en;
reg r_en;
asy_fifo #(
.DATA_WIDTH ( DATA_WIDTH ),
.FIFO_DEPTH ( 16 ))
u_asy_fifo(
//ports
.rst_n ( sys_rst ),
.wclk ( write_clk ),
.w_data ( w_data ),
.w_en ( w_en ),
.w_full ( w_full ),
.rclk ( read_clk ),
.r_en ( r_en ),
.r_data ( r_data ),
.r_empty ( r_empty )
);
initial
begin
#20;
sys_rst = 0;
w_en= 0;
r_en= 0;
#20;
sys_rst=1;
w_en=1;
r_en=1;
#100;
r_en=0;
#400;
w_en=0;
r_en=1;
#400;
$stop;
end
always @(posedge write_clk)
begin
if(~sys_rst)
w_data <= 0;
else
begin
w_data <= {$random} % 100;
end
end
endmodule //TOP
1.读写同时
可以看到由于时钟同步的关系读出的数据是滞后的
2.写满
3.读空
1.链接:亚稳态与跨时钟域
2.Asynchronous FIFO Architectures (Part 1) By Vijay A. Nebhrajani
3.Asynchronous FIFO Architectures (Part 2) By Vijay A. Nebhrajani
4.Asynchronous FIFO Architectures (Part 3) By Vijay A. Nebhrajani
强烈推荐Vijay A. Nebhrajani 所著的Asynchronous FIFO Architectures系列建议观看全文