异步FIFO_包含将满和将空判断信号
参考文章:
https://www.cnblogs.com/xuqing125/p/8337586.html
https://blog.csdn.net/darknessdarkness/article/details/104726798
一、FIFO简介
FIFO(First In First Out,即先入先出),是一种数据缓冲器,用来实现数据先入先出的读写方式。FIFO存储器主要是作为缓存,应用在同步时钟系统和异步时钟系统中,在很多的设计中都会使用,如多比特数据做跨时钟域处理,前后带宽不同步等都用到了FIFO。FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。
1.FIFO分类
同步FIFO(sync_fifo),读写时钟相同。它的作用一般是做交互数据的一个缓冲,也就是说它的主要作用就是一个buffer。
异步FIFO(async_fifo),读写时钟不同,它有两个主要的作用,一个是实现数据在不同时钟域进行传递,另一个作用就是实现不同数据宽度的数据接口。异步FIFO的实现其实本质上和双口RAM是一样的,其实现思路就是将数据在wr_clk的时钟下写入自己设定大小的ram中,然后通过读时钟rd_clk从ram中将数据读出来即可。
2.异步FIFO的主要参数
- 位宽:用参数DATA_WIDTH表示,也就是FIFO存储的数据位宽(例如DATA_WIDTH=8,能表示0~255的数);
- 指针位宽:用参数PTR_WIDTH表示,例如指针地址位宽为8,地址位宽为9(定义9为进位),FIFO深度则为2^8=256;
- 深度:用参数DATA_DEPTH表示,也就是地址的大小,也就是说能存储多少个数据,由PTR_WIDTH计算得到;
- 满标志:full,当FIFO中的数据满了以后将不再能进行数据的写入;
- 将满标志:almost_full,当FIFO中的数据将要满了的时候将满标志跳变为1,将满跳变的间隔阈值大小由ALMOST_FULL_GAP决定;
- 空标志:empty,当FIFO为空的时候将不能进行数据的读出;
- 将空标志:almost_empty,当FIFO中的数据将要空了的时候将满标志跳变为1,将空跳变的间隔阈值大小由ALMOST_EMPTY_GAP决定;
- 写地址:wr_addr,由自动加一生成,将数据写入该地址;
- 读地址:rd_addr,由自动加一生成,将该地址上的数据读出;
- 时钟:wr_clk,rd_clk,读写应用不同的时钟;
- 复位信号:rd_rst_n,wr_rst_n,复位信号低电平有效;
- 指针:wr_ptr,rd_ptr,比rd_addr多出来一位作为空满判断位。wr_ptr_gray,rd_ptr_gray,用格雷码指针来判断空满标识。wr_almost_ptr,rd_almost_ptr,用二进制码指针来判断将空将满标识;
- 同步指针:r2w_r_ptr_gray为读时钟域同步到写时钟域的读指针的格雷码,w2r_w_ptr_gray为写时钟域同步到读时钟域的写指针的格雷码,指针的同步操作,用来做对比产生空满标志符;
3.异步FIFO的逻辑图
4.异步FIFO模块划分
异步FIFO将模块划分为4个部分,fifo_mem(RAM)、wr_full、rd_empty、sync(synchronization)。RAM模块根据读写地址进行数据的写入和读出,wr_full模块根据wr_clk产生写地址,full满信号以及almost_full将满信号,rd_empty模块根rd_clk产生读地址,empty空信号以及almost_empty将空信号,sync模块用于同步wr_ptr_gray到读时钟域或者同步rd_ptrr_gray到写时钟域。
二、异步FIFO设计关键技术问题
异步 FIFO 设计过程中存在两个关键技术问题:1)亚稳态;2)空/满状态标志位判断及产生。在处理空/满标志位问题上,目前最常用的方案是增加一位读写指针附加位,当读写指针最高位相同其余位也相同时,认为读空,当读写指针最高位不相同其余位相同时,认为写满。文中以宽度为16位、深度为8位的异步 FIFO 为例,介绍亚稳态产生的原因以及降低亚稳态出现概率的方法,分析利用格雷码和同步转换来产生空/满标志位的方法。
1.亚稳态产生的原因及解决方法
在所有数字器件中,寄存器都定义了一个信号时序要求,满足时序要求的寄存器才能正确地在输入端获取数据、在输出端产生数据。为确保操作可靠,输入数据在时钟沿之前必须稳定一段时间(建立时间),并且在时钟沿之后保持一段时间(保持时间),触发器经过一个特定时钟至输出延时后有效。如果一个数据信号在变化之前不满足触发器建立和保持时间要求,触发器输出可能会进入亚稳态。亚稳态触发器输出值会在高低电平之间徘徊不定,如下图第二个采样时钟来到时所示。
虽然亚稳态在异步电路中无法避免,但是1)两级同步器和2)格雷码计数器可以降低亚稳态概率到可以接受的程度。
1.1两级同步寄存器
图中两级寄存器对不同时钟域的输入数据锁存两拍。一般情况下,两级锁存同步器是一级同步器出现亚稳态概率的平方,在大部分同步设计中,两级同步器可以大大降低亚稳态出现概率,下图为通过两级寄存器消除亚稳态的示例图(当 clk1 和 clk2 上升沿很近时,data0 在变化时,此时 clk2 上升沿采集到一个正在变化的数值,data1 是个不确定值,FF1触发器输出处于亚稳态,经过1个时钟延时,data1 值趋于稳定,FF2在 clk2 上升沿对 data1 稳定值采样,输出 data2 为确定值。虽然data1 在被 clk2 上升沿采样时也有处于亚稳态的可能,但是这种概率很小,经过两级同步器能大大降低亚稳态概率)。
两拍延时的数据同步对空满标志产生的影响:
由此信号rd_ptr_gray经过两级D触发器,就会有两拍的延时形成r2w_r_ptr_gray信号,所以在进行比较的时候就不是实时的rd_ptr_gray与wr_ptr_gray进行比较,而是两拍之前的rd_ptr_gray即r2w_r_ptr_gray与此刻的wr_ptr_gray进行比较。那么问题就来了这与我们的本意其实是不相符的,其实是这样的,这是一种最坏情况的考虑,将r2w_r_ptr_gray与wr_ptr_gray相比较是为了产生full信号,在用于数据同步的这两拍里面有可能再进行读操作,所以用于比较时的读地址一定小于或等于当前的读地址,就算此刻产生full信号,其实FIFO有可能还没有满。这也就为设计留了一些设计的余量。同理,就算有empty信号的产生,FIFO有可能还有数据。这种留余量的设计在实际的工程项目中是很常见的。
1.2格雷码计数器
格雷码是一种误差最小化的可靠性编码,可以极大地减少由一个状态变化到下一个状态时电路产生的误差。这种编码方式是两个相邻码之间只有一位变化,缺点是格雷码是无权码,不能直接用于计算、比较,计算将满将空信号时需要转换为二进制代码计算。亚稳态出现的原因是数据变化时建立和保持时间不够,数据地址经过二级同步器后,地址指针采用格雷码编码,地址指针一次只能变化一位,通过这种可以有效减少亚稳态出现概率。下图为格雷码与二进制代码相互转换的算法及代码。
二进制转格雷码:
//写指针格雷码转换
assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
//读指针格雷码转换
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);
格雷码转二进制:
//格雷码转二进制
assign rd_almost_ptr[PTR_WIDTH] = w2r_w_ptr_gray[PTR_WIDTH];
genvar i;
generate
for(i=PTR_WIDTH-1;i>=0;i=i-1)begin:rdgray2bin // <-- example block name
assign rd_almost_ptr[i] = rd_almost_ptr[i+1] ^ w2r_w_ptr_gray[i];
end
endgenerate
//格雷码转二进制
assign wr_almost_ptr[PTR_WIDTH] = r2w_r_ptr_gray[PTR_WIDTH];
genvar i;
generate
for (i=PTR_WIDTH-1;i>=0;i=i-1) begin:wrgray2bin // <-- example block name
assign wr_almost_ptr[i] = wr_almost_ptr[i+1] ^ r2w_r_ptr_gray[i];
end
endgenerate
在不同时钟域进行数据交换的时候我们一般采用格雷码的数据形式进行数据传递,这样能很大程度上降低出错的概率。
引入格雷码同时也引入一个问题,就是数据空满标志的判断不再是二进制时候的判断标准。
2.空/满状态标志位判断及产生
我们知道FIFO的状态是满还是空,他们的相同的判断条件都是wr_addr=rd_addr,但到底是空还是满我们还不能确定。在这里介绍一种方法来判断空满状态。我们设定一个指针rd_ptr,wr_ptr,宽度为[PTR_WIDTH:0],也就是说比传统的地址rd_addr和wr_addr多一位,我们就用这多出来的一位做空满判断。
- 如果是满状态的话,也就是说wr_ptr比rd_ptr多走了一圈,反应在二进制数值上就是wr_ptr和rd_ptr的最高位不相同,反应在格雷码上就是wr_ptr_gray(rd_ptr_gray)和同步过来的r2w_r_ptr_gray(w2r_w_ptr_gray)最高位和次高位不相同,其余位相同。
- 如果是空状态的话,也就是说w_pointer_bin和r_pointer_bin的路径相同,反应在二进制数值和格雷码上均为wr_ptr和rd_ptr的每一位相等。
3.将空/将满状态标志位判断及产生
将满(almost_full)和将空信号(almost_empty)实际上表示更加保守的满和空信号。基本思路是,设定一个间隔值ALMOST_FULL_GAP(ALMOST_EMPTY_GAP),当读写地址之间的间隔小于或等于该间隔就产生将空或将满信号。
对于异步FIFO而言,由于同步过来的地址信号都是格雷码表示的,我们不能直接用格雷码去判断上述的这个间隔,所以需要先对接受到的格雷码进行解码变为二进制,再和当前时钟域下的另一个地址进行将满和将空的生成。
对于将空的判断和空一样,只需要检查写地址与读地址的差是否小于等于间隔
//产生将空信号 fifo_almost_empty
assign almost_empty = ((rd_almost_ptr - rd_ptr) <= ALMOST_EMPTY_GAP) ? 1'b1 : 1'b0;
对将满的判断则需要分两种情况:
- 最高位不同时:此时表示写地址有一个回卷,直接将读写地址除去符号位的部分做差与间隔比较。
- 最高位相同时:需要在差值上再加上FIFO深度(数据的总个数)。
//若最高位不相同则直接相减,最高位相同则需在此基础上加一个数据深度DATA_DEPTH
assign wr_almost_val = (wr_almost_ptr[PTR_WIDTH] ^ wr_ptr[PTR_WIDTH]) ? wr_almost_ptr[PTR_WIDTH-1:0] - wr_ptr[PTR_WIDTH-1:0] : DATA_DEPTH + wr_almost_ptr - wr_ptr;
//产生将满信号
assign almost_full = (wr_almost_val <= ALMOST_FULL_GAP) ? 1'b1 : 1'b0;
三、源代码
1.顶层文件
顶层文件代码如下:
module asyn_fifo #(
parameter DATA_WIDTH = 16,
parameter PTR_WIDTH = 8 ,
parameter ALMOST_EMPTY_GAP = 3 ,
parameter ALMOST_FULL_GAP = 3
)(
input wr_clk,
input wr_rst_n,
input wr_en,
input [DATA_WIDTH-1:0] wr_data,
input rd_clk,
input rd_rst_n,
input rd_en,
output [DATA_WIDTH-1:0] rd_data,
output full,
output almost_full,
output wire empty,
output wire almost_empty
);
wire [PTR_WIDTH:0] r2w_r_ptr_gray;
wire [PTR_WIDTH:0] w2r_w_ptr_gray;
wire [PTR_WIDTH:0] wr_ptr_gray;
wire [PTR_WIDTH:0] rd_ptr_gray;
wire [PTR_WIDTH-1:0] wr_addr;
wire [PTR_WIDTH-1:0] rd_addr;
wr_full #(
.DATA_WIDTH(DATA_WIDTH),
.PTR_WIDTH(PTR_WIDTH),
.ALMOST_FULL_GAP(ALMOST_FULL_GAP)
) wr_full_inst(
.wr_clk(wr_clk),
.wr_rst_n(wr_rst_n),
.wr_en(wr_en),
.r2w_r_ptr_gray(r2w_r_ptr_gray),
.wr_addr(wr_addr),
.wr_ptr_gray(wr_ptr_gray),
.full(full),
.almost_full(almost_full)
);
fifo_mem #(
.DATA_WIDTH(DATA_WIDTH),
.PTR_WIDTH(PTR_WIDTH)
) fifo_mem_inst(
.wr_clk(wr_clk),
.wr_rst_n(wr_rst_n),
.wr_en(wr_en),
.wr_addr(wr_addr),
.wr_data(wr_data),
.full(full),
.rd_clk(rd_clk),
.rd_rst_n(rd_rst_n),
.rd_en(rd_en),
.rd_addr(rd_addr),
.empty(empty),
.rd_data(rd_data)
);
rd_empty #(
.DATA_WIDTH(DATA_WIDTH),
.PTR_WIDTH(PTR_WIDTH),
.ALMOST_EMPTY_GAP(ALMOST_EMPTY_GAP)
) rd_empty_inst(
.rd_clk(rd_clk),
.rd_rst_n(rd_rst_n),
.rd_en(rd_en),
.w2r_w_ptr_gray(w2r_w_ptr_gray),
.rd_addr(rd_addr),
.rd_ptr_gray(rd_ptr_gray),
.empty(empty),
.almost_empty(almost_empty)
);
sync #(
.DATA_WIDTH(DATA_WIDTH),
.PTR_WIDTH(PTR_WIDTH)
) sync_inst(
.wr_clk(wr_clk),
.wr_rst_n(wr_rst_n),
.rd_clk(rd_clk),
.rd_rst_n(rd_rst_n),
.wr_ptr_gray(wr_ptr_gray),
.rd_ptr_gray(rd_ptr_gray),
.w2r_w_ptr_gray(w2r_w_ptr_gray),
.r2w_r_ptr_gray(r2w_r_ptr_gray)
);
endmodule
2.fifo_memory(RAM)
RAM模块根据读写地址进行数据的写入和读出,代码如下:
module fifo_mem #(
parameter DATA_WIDTH = 16, //RAM存储器位宽
parameter PTR_WIDTH = 8 //指针地址位宽为8,地址位宽为9(定义9为进位),fifo深度则为2^8=256
//parameter DATA_DEPTH = 256 //数据深度(数据个数)
)(
input wr_clk,
input wr_rst_n,
input wr_en,
input [PTR_WIDTH-1:0] wr_addr, //写地址
input [DATA_WIDTH-1:0] wr_data, //写数据
input full,
input rd_clk,
input rd_rst_n,
input rd_en,
input [PTR_WIDTH-1:0] rd_addr, //读地址
input empty,
output reg [DATA_WIDTH-1:0] rd_data //读数据
);
localparam DATA_DEPTH = 1 << PTR_WIDTH;//相当于将二进制数1在二进制中的表示向左移动PTR_WIDTH位,也就是将这个数乘以2的PTR_WIDTH次方
//开辟存储空间
reg [DATA_WIDTH-1:0] mem[DATA_DEPTH-1:0];
integer i;
always @(posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n) begin
for(i=0;i<DATA_DEPTH;i=i+1) begin
mem[i] <= 1'b0;
end
end
else if(wr_en && !full) begin
mem[wr_addr] <= wr_data;
end
end
always @(posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_data <= 'b0;
end
else if(rd_en && !empty) begin
rd_data <= mem[rd_addr];
end
end
endmodule
3.wr_full
wr_full模块根据wr_clk产生写地址,full满信号以及almost_full将满信号,代码如下:
module wr_full #(
parameter DATA_WIDTH = 16,
parameter PTR_WIDTH = 8,
parameter ALMOST_FULL_GAP = 3
)
(
input wr_clk,
input wr_rst_n,
input wr_en,
input [PTR_WIDTH:0] r2w_r_ptr_gray,
output full,
output [PTR_WIDTH-1:0] wr_addr,
output [PTR_WIDTH:0] wr_ptr_gray,
output wire almost_full
);
localparam DATA_DEPTH = 1 << PTR_WIDTH;//相当于将二进制数1在二进制中的表示向左移动PTR_WIDTH位,也就是将这个数乘以2的PTR_WIDTH次方
reg [PTR_WIDTH:0] wr_ptr;
wire [PTR_WIDTH:0] wr_almost_ptr;
wire [PTR_WIDTH:0] wr_almost_val;
//fifo_mem的地址
assign wr_addr = wr_ptr[PTR_WIDTH-1:0];
//写指针格雷码转换
assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
//产生full, 最高位和此高位不同而其他位均相同则判断为full
assign full = ({~wr_ptr_gray[PTR_WIDTH:PTR_WIDTH-1],wr_ptr_gray[PTR_WIDTH-2:0]} ==
{r2w_r_ptr_gray[PTR_WIDTH:PTR_WIDTH-1],r2w_r_ptr_gray[PTR_WIDTH-2:0]}) ? 1'b1:1'b0;
//格雷码转二进制
assign wr_almost_ptr[PTR_WIDTH] = r2w_r_ptr_gray[PTR_WIDTH];
genvar i;
generate
for (i=PTR_WIDTH-1;i>=0;i=i-1) begin:wrgray2bin // <-- example block name
assign wr_almost_ptr[i] = wr_almost_ptr[i+1] ^ r2w_r_ptr_gray[i];
end
endgenerate
//若最高位不相同则直接相减,最高位相同则需在此基础上加一个数据深度DATA_DEPTH
assign wr_almost_val = (wr_almost_ptr[PTR_WIDTH] ^ wr_ptr[PTR_WIDTH]) ? wr_almost_ptr[PTR_WIDTH-1:0] - wr_ptr[PTR_WIDTH-1:0] : DATA_DEPTH + wr_almost_ptr - wr_ptr;
//产生将满信号
assign almost_full = (wr_almost_val <= ALMOST_FULL_GAP) ? 1'b1 : 1'b0;
//产生写指针
always@(posedge wr_clk or negedge wr_rst_n)
if (!wr_rst_n)begin
wr_ptr <= 'b0;
end
else if (~full && wr_en)begin
wr_ptr <= wr_ptr + 1'b1;
end
endmodule
4.rd_empty
rd_empty模块根据rd_clk产生读地址,empty空信号以及almost_empty将空信号,代码如下:
module rd_empty #(
parameter DATA_WIDTH = 16,
parameter PTR_WIDTH = 8,
parameter ALMOST_EMPTY_GAP = 3 //将空信号间隔阈值为3
)
(
input rd_clk,
input rd_rst_n,
input rd_en,
input [PTR_WIDTH:0] w2r_w_ptr_gray, //写时钟域同步到写时钟域的读指针的格雷码
output [PTR_WIDTH-1:0] rd_addr, //fifo_mem的地址
output [PTR_WIDTH:0] rd_ptr_gray, //格雷码形式的读指针
output wire empty,
output wire almost_empty
);
reg [PTR_WIDTH:0] rd_ptr; //比rd_addr多出来一位作为空满判断位
wire [PTR_WIDTH:0] rd_almost_ptr; //用来判断将空信号的读指针
//直接作为存储实体的地址,比如连接到RAM存储实体的读地址端
assign rd_addr = rd_ptr[PTR_WIDTH-1:0];
//读指针格雷码转换
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);
//产生读空信号empty
assign empty = (rd_ptr_gray == w2r_w_ptr_gray) ? 1'b1:1'b0;
//格雷码转二进制
assign rd_almost_ptr[PTR_WIDTH] = w2r_w_ptr_gray[PTR_WIDTH];
genvar i;
generate
for(i=PTR_WIDTH-1;i>=0;i=i-1)begin:rdgray2bin // <-- example block name
assign rd_almost_ptr[i] = rd_almost_ptr[i+1] ^ w2r_w_ptr_gray[i];
end
endgenerate
//产生将空信号 fifo_almost_empty
assign almost_empty = ((rd_almost_ptr - rd_ptr) <= ALMOST_EMPTY_GAP) ? 1'b1 : 1'b0;
//读指针产生
always @(posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_ptr <= 'b0;
end
else if(rd_en && !empty) begin
rd_ptr <= rd_ptr + 1'b1;
end
end
endmodule
5.sync
sync模块用于同步wr_ptr_gray到读时钟域或者同步rd_ptrr_gray到写时钟域,代码如下:
module sync #(
parameter DATA_WIDTH = 16,
parameter PTR_WIDTH = 8
)(
input wr_clk,
input wr_rst_n,
input rd_clk,
input rd_rst_n,
input [PTR_WIDTH:0] wr_ptr_gray,
input [PTR_WIDTH:0] rd_ptr_gray,
output wire [PTR_WIDTH:0] w2r_w_ptr_gray,
output wire [PTR_WIDTH:0] r2w_r_ptr_gray
);
reg [PTR_WIDTH:0] wr_ptr_gray_r[1:0];
reg [PTR_WIDTH:0] rd_ptr_gray_r[1:0];
assign w2r_w_ptr_gray = wr_ptr_gray_r[1];
assign r2w_r_ptr_gray = rd_ptr_gray_r[1];
//写指针同步到读时钟域
always @(posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n) begin
wr_ptr_gray_r[0] <= 'b0;
wr_ptr_gray_r[1] <= 'b0;
end
else begin
wr_ptr_gray_r[0] <= wr_ptr_gray;
wr_ptr_gray_r[1] <= wr_ptr_gray_r[0];
end
end
//读指针同步到写时钟域
always @(posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n) begin
rd_ptr_gray_r[0] <= 'b0;
rd_ptr_gray_r[1] <= 'b0;
end
else begin
rd_ptr_gray_r[0] <= rd_ptr_gray;
rd_ptr_gray_r[1] <= rd_ptr_gray_r[0];
end
end
endmodule
6.Testbench测试文件
代码如下:
`timescale 1ns/1ns
module asyn_fifo_tb;
localparam DATA_WIDTH = 16;
reg wr_clk;
reg wr_rst_n;
reg wr_en;
reg [DATA_WIDTH-1:0] wr_data;
reg rd_clk;
reg rd_rst_n;
reg rd_en;
wire [DATA_WIDTH-1:0] rd_data;
wire full;
wire almost_full;
wire empty;
wire almost_empty;
asyn_fifo #(
.DATA_WIDTH(16),
.PTR_WIDTH(8)
)
asyn_fifo_inst
(
.wr_clk(wr_clk),
.wr_rst_n(wr_rst_n),
.wr_en(wr_en),
.wr_data(wr_data),
.rd_clk(rd_clk),
.rd_rst_n(rd_rst_n),
.rd_en(rd_en),
.rd_data(rd_data),
.full(full),
.almost_full(almost_full),
.empty(empty),
.almost_empty(almost_empty)
);
initial wr_clk = 0;
always #10 wr_clk = ~wr_clk;
initial rd_clk = 0;
always #30 rd_clk = ~rd_clk;
//写数据
always @(posedge wr_clk or negedge wr_rst_n)
begin
if (!wr_rst_n)
begin
wr_data <= 1'b0;
end
else if(wr_en)
begin
wr_data <= wr_data + 1'b1;
end
else
begin
wr_data <= wr_data;
end
end
initial begin
wr_rst_n = 0;
rd_rst_n = 0;
wr_en = 0;
rd_en = 0;
#200;
wr_rst_n = 1;
rd_rst_n = 1;
wr_en = 1;
#200
rd_en = 1;
#8000;
wr_en = 0;
#20000
rd_en = 0;
#2000
wr_en = 1;
#3000
$stop;
end
endmodule
四、仿真测试