数字设计--异步FIFO
异步FIFO
异步FIFO用一种时钟写入数据,而用另外一种时钟读出数据。读写指针的变化动作由不同的时钟产生。因此,对FIFO空或满的判断是跨时钟域的。如何根据异步的指针信号产生正确的空、满标志,是异步FIFO设计成败的关键。
异步FIFO端口
- FIFO 的宽度:即 FIFO一次读写操作的数据位;
- FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)。
- 满标志:FIFO 已满或将要满时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的写操作继续向FIFO 中写数据而造成溢出(overflow)。
- 空标志:FIFO 已空或将要空时由 FIFO 的状态电路送出的一个信号,以阻止 FIFO 的读操作继续从FIFO 中读出数据而造成无效数据的读出(underflow)。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。(异步FIFO读时钟与写时钟一般不同)
- 将满标志(almost full):FIFO将要满时由FIFO的状态电路送出的一个信号。
- 将空信号(almost empty):FIFO将要空时由FIFO的状态电路送出的一个信号。
异步FIFO结构
异步FIFO可以分为对ROM或者寄存器组进行数据的读写,读写指针的增加和空满状态的判断这几各部分。空满状态的判断有很多种方法,根据方法的不同又可以分为很多种结构,其中我们可以将读写指针同步到相对应的写读时钟域去做"同步判断"(#1),也可以直接对读写指针进行比较,做异步判断(#2)。
读写指针
写指针指向数据下一个要写入的地址
读指针指向数据下一个要读出的地址
#1结构
从上面可以看出,对于#1结构来说,包含着存储器部分,fifo满状态判断部分,fifo空状态判断部分,写指针同步到读时钟域,读指针同步到写时钟域,写指针控制和读指针控制几个部分。
读写指针的跨时钟域
对于空满判断,需要比较读指针域写指针的大小关系,但是读指针与写指针存在于不同的时钟域
在#1结构中比较读指针与写指针的方法是将读指针同步到写时钟域,与写指针比较,判断FIFO是否为满,将写指针同步到读时钟域,与读指针进行比较,判断FIFO是否为空。
将读写指针同步到相对的时钟域即跨时钟域,可以采用“打两拍”的方式,来减少出现亚稳态的可能性,如果读写指针采用二进制数采用打两拍的方式进行同步,由于二进制数在增加的过程中相邻的两个数可能有多位同时发生变化(比如1111(15)->0000(0),4位全部发生了变化),那么在打两拍的过程中可能会发生多个位亚稳态的现象,就会导致同步到对应时钟域上的数值发生错误,影响空满状态的判断。所以不能直接将二进制数直接打两拍进行跨时钟域的同步。
握手
这里我们可以采用握手的方法,握手操作是跨时钟域的经典方法,可以设置使能信号,当写时钟域产生新的写指针时,使能信号有效,同步到读时钟域(使能信号只有一位,打两拍就可以),读时钟域在收到使能信号后对写指针进行读取,读时钟域对写指针进行读取之前,写指针不能改变(所以在读时钟沿写指针不会变化就不存在亚稳态的问题),当读时钟域对写指针读取完之后,读时钟域再使能另一个使能信号,这个使能信号同步到写时钟域后,写时钟域接收到使能信号,写指针才能变化。读指针的同步也同理。因为使能信号的缘故,在传递使能信号的过程中指针不能发生变化,所以就不存在各个时钟域在采样指针信号时发生变化导致的亚稳态问题。
格雷码
当然对于读写指针同步到相对的时钟域,还可以采用格雷码。
格雷码具有相邻码字之间只有一位不同的特性,那么对于读写指针在增加的过程中,每次变化只会导致一位信号的变化,所以读写指针采用格雷码来进行跨时钟域的同步时,如果出现采样时发生变化,也只会导致一位信号出现亚稳态,只会导致数据变化的那一位出现错误(比如传输1100->1101,如果发生亚稳态,只会导致最后一位出现错误,最后采样到的结果要么是1100要么是1101,则这个采样的值对于空满状态的判断来说是"保守"的值,如果是1101那正好传输正确,如果是1100,会导致提前判为空或者满,不会使FIFO出现溢出或者空读的现象,只会影响相应读写的效率)。
区别
握手的方式可以将二进制数进行跨时钟域,而采取格雷码的方式却只能用格雷码跨时钟域。如果采取格雷码的方式,为了保证格雷码相邻码元之间只有一位发生变化,FIFO的深度为2的幂次(这么说可能不准确,需要保证格雷码的首尾只有一位变化,所以指针的深度必须是偶数,地址可奇可偶,但是指针的深度需要时偶数,这个问题后面会提到),而对于握手的方式,对FIFO的深度没有要求。握手的方式由于存在反馈的时间,虽然这个反馈的时间不会导致溢出或者空读,但是会影响读写的效率,在速度上要低于采用格雷码的方式。
读写指针与FIFO地址的类型
因为上面我们说跨时钟域可以握手也可以用格雷码,那么指针可以用二进制数也可以用格雷码。
那么到底用二进制还是格雷码呢?
下面的仅是个人思考,如有不对欢迎指正
一种方式是采用格雷码作为FIFO地址,因为格雷码没有重复的部分,所以用格雷码作为地址去进行FIFO的读写(如果不嫌麻烦的话可以再转换为二进制数的形式作为FIFO地址)。举个例子,比如用n位格雷码作为读写指针,那么可以保留低n-2位格雷码,将第n位和第n-1位格雷码进行异或来作为第n-1位组成n-1位宽的FIFO地址。
从上面可以看出,如果单纯的将格雷码的低位截出来作为FIFO地址,上下两部分不相同,不能作为FIFO的地址,因为比如FIFO的深度为8,当走到7这个位置的时候,下一个应该和0的格雷码一样,所以直接截不能作为FIFO地址。将格雷码的最高两位相异或最后得到的结果上下两部分一样,就可以作为FIFO地址。
如果用格雷码作为读写指针和地址,就存在一个问题,那就是格雷码没法加一,所以指针的增加还需要将格雷码转换为二进制码。
那么整体的结构就可以表示为
这种方式将格雷码先转换为二进制码,再将二进制码转换为格雷码,说实话很繁琐,另外包含了很多组合逻辑(二进制转格雷码,格雷码转二进制),相对来说系统的速度会比较慢。但是如果需要判断almost_full和almost_empty,用这种结构就很方便,直接用二进制数去判断FIFO的数据数目。
为了简便
将二进制码作为指针和地址,只有在跨时钟域的时候才将二进制码转换为格雷码。
这样的话只涉及到一次的二进制转换为格雷码,实现起来比较简单,后面我们讨论也采用在这种形式。
这种结构相比较上一种结构多使用了一组寄存器组去存储二进制数,面积增加了但是减少了组合逻辑的数量(只有一个二进制转格雷码的逻辑),所以系统的速度会比较块。
实际上从上面看,这两种方法跨时钟域都采用了格雷码(否则出现格雷码就没有必要了),地址的选取是可以相互转换的,采取哪一种方式,实际上也没那么必要。
怎么简单怎么来,所以推荐还是第二种。
上面是个人思考,不一定对
空满判断
握手
如果通过握手的方式进行跨时钟域的同步,那么我们就要用二进制数进行空满状态的判断。
我们可以在地址数据的前面增加一位数据来对空满状态进行判断。举个例子,FIFO地址的宽度为4位,那么读写指针的宽度设为5位(低四位用作FIFO的地址),如果读写指针的5位全部相等,说明FIFO是空的状态,如果读写指针的低4位相等,最高位即我们加的第五位不同,说明写指针相对于读指针来说已经"套圈",写指针已经绕了一圈追上了读指针,所以这时FIFO的状态是满状态。
格雷码
如果采用格雷码进行跨时钟域同步,同样的需要读写指针的位数比FIFO地址的宽度大一位。
关于空满判断可以将同步到相对应域上的格雷码转换为二进制码,再和二进制码进行空满信号的判断。
上图就是将读时钟域和写时钟域的格雷码转换为二进制数,进行比较,按照之前提到的比较最高位相等于否和余下位是否相等来判断空满。
当然也可以格雷码直接去判断FIFO的空满状态。
如果按照二进制数那样直接判断最高位相等与否和低位是否相等来对格雷码进行比较来判断空满状态将会出错。
当读指针和写指针都指向7时,读指针格雷码与写指针格雷码相等,这时FIFO为空还是为满不确定,参考二进制数判断空满的方法,由于指针的宽度比地址的宽度大一位,所以比较最高位是否相同,相同则表示FIFO为空,相反呢?这时我们看格雷码7(0100)和8(1100),这时格雷码最高位相反,其他位相同,但是这时写指针指向7读指针指向8,FIFO并没有满,所以格雷码完全相等只能判断FIFO空。
那怎么判断FIFO满呢?
- 写指针和同步过来的读指针的MSB(最高位)不相等,因为写指针必须比读指针多折回一次。
- 写指针与读指针的次高位不相等
- 剩下的其余位完全相等。
总结下来就是如果读写指针的最高两位相反,剩余位相同,则可以判断FIFO为满状态。
verilog代码
一种代码
module asy_fifo_1#(
parameter WIDTH = 8,
parameter DEPTH = 8
)(
input [WIDTH - 1 : 0] wr_data,
input wr_clk,
input wr_rstn,
input wr_en,
input rd_clk,
input rd_rstn,
input rd_en,
output reg fifo_full,
output reg fifo_empty,
output reg [WIDTH - 1 : 0] rd_data
);
//定义读写指针
reg [$clog2(DEPTH) : 0] wr_ptr, rd_ptr;
//定义一个宽度为WIDTH,深度为DEPTH的fifo
reg [WIDTH - 1 : 0] fifo [DEPTH - 1 : 0];
//定义读数据
//写操作
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
wr_ptr <= 0;
else if(wr_en && !fifo_full) begin
fifo[wr_ptr[$clog2(DEPTH)-1:0]] <= wr_data;
wr_ptr <= wr_ptr + 1;
end
else
wr_ptr <= wr_ptr;
end
//读操作
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
rd_ptr <= 0;
rd_data <= 0;
end
else if(rd_en && !fifo_empty) begin
rd_data <= fifo[rd_ptr[$clog2(DEPTH)-1:0]];
rd_ptr <= rd_ptr + 1;
end
else
rd_ptr <= rd_ptr;
end
//定义读写指针格雷码
wire [$clog2(DEPTH) : 0] wr_ptr_g;
wire [$clog2(DEPTH) : 0] rd_ptr_g;
//读写指针转换成格雷码
assign wr_ptr_g = wr_ptr ^ (wr_ptr >>> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >>> 1);
//定义打拍延迟格雷码
reg [$clog2(DEPTH) : 0] wr_ptr_gr, wr_ptr_grr;
reg [$clog2(DEPTH) : 0] rd_ptr_gr, rd_ptr_grr;
//写指针同步到读时钟域
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
wr_ptr_gr <= 0;
wr_ptr_grr <= 0;
end
else begin
wr_ptr_gr <= wr_ptr_g;
wr_ptr_grr <= wr_ptr_gr;
end
end
//读指针同步到写时钟域
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn) begin
rd_ptr_gr <= 0;
rd_ptr_grr <= 0;
end
else begin
rd_ptr_gr <= rd_ptr_g;
rd_ptr_grr <= rd_ptr_gr;
end
end
//声明空满信号数据类型
// reg fifo_full;
// reg fifo_empty;
//写满判断
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
fifo_full <= 0;
else if((wr_ptr_g[$clog2(DEPTH)] != rd_ptr_grr[$clog2(DEPTH)]) && (wr_ptr_g[$clog2(DEPTH) - 1] != rd_ptr_grr[$clog2(DEPTH) - 1]) && (wr_ptr_g[$clog2(DEPTH) - 2 : 0] == rd_ptr_grr[$clog2(DEPTH) - 2 : 0]))
fifo_full <= 1;
else
fifo_full <= 0;
end
//读空判断
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn)
fifo_empty <= 0;
else if(wr_ptr_grr[$clog2(DEPTH) : 0] == rd_ptr_g[$clog2(DEPTH) : 0])
fifo_empty <= 1;
else
fifo_empty <= 0;
end
endmodule
相对应的testbench
module asy_fifo_test_zzt(
);
parameter width = 8;
parameter depth = 8;
reg wr_clk, wr_en, wr_rstn;
reg rd_clk, rd_en, rd_rstn;
reg [width - 1 : 0] wr_data;
wire fifo_full, fifo_empty;
wire [width - 1 : 0] rd_data;
//实例化
asy_fifo_1 myfifo (
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.wr_rstn(wr_rstn),
.rd_rstn(rd_rstn),
.wr_en(wr_en),
.rd_en(rd_en),
.wr_data(wr_data),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.fifo_full(fifo_full)
);
//时钟
initial begin
rd_clk = 0;
forever #25 rd_clk = ~rd_clk;
end
initial begin
wr_clk = 0;
forever #30 wr_clk = ~wr_clk;
end
initial begin
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
#10;
wr_rstn <= 1'b0;
rd_rstn <= 1'b0;
#10;
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
end
always @(posedge wr_clk or negedge wr_rstn)
if(!wr_rstn)
begin
wr_en<= 1'b0;
wr_data <= 8'd0;
end
else if(!fifo_full)
begin
wr_en <= 1'b1;
wr_data <= wr_data + 8'd1;
end
else
begin
wr_en<= 1'b0;
wr_data <= wr_data;
end
always @(posedge rd_clk or negedge rd_rstn)
if(!rd_rstn)
begin
rd_en<= 1'b0;
end
else if(!fifo_empty)
begin
rd_en <= 1'b1;
end
else
begin
rd_en<= 1'b0;
end
endmodule
cumming论文中的代码
module fifo1_all
#(
parameter
DATASIZE = 8,
ADDRSIZE = 4
)
(
output [DATASIZE-1:0] rdata,
output reg wfull,
output reg rempty,
input [DATASIZE-1:0] wdata,
input winc,
input wclk,
input wrst_n,
input rinc,
input rclk,
input rrst_n
);
wire [ADDRSIZE-1:0] waddr,raddr;
reg [ADDRSIZE:0] wptr,rptr,wq2_rptr,rq2_wptr;
//fifomem
localparam DEPTH = 1<<ADDRSIZE;
reg [DATASIZE-1:0] mem[0:DEPTH-1];
assign rdata = mem[raddr];
always @(posedge wclk)
if(winc && !wfull)
mem[waddr] <= wdata;
//sync_r2w
reg [ADDRSIZE:0] wq1_rptr;
always @(posedge wclk or negedge wrst_n)
if(!wrst_n)
{wq2_rptr,wq1_rptr} <= 0;
else
{wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
//sync_w2r
reg [ADDRSIZE:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n)
if(!rrst_n)
{rq2_wptr,rq1_wptr} <= 0;
else
{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
//wptr_full
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext,wbinnext;
wire wfull_val;
always @(posedge wclk or negedge wrst_n)
if(!wrst_n)
{wbin,wptr} <= 0;
else
{wbin,wptr} <= {wbinnext,wgraynext};
assign waddr = wbin[ADDRSIZE-1:0];
assign wbinnext = wbin + (winc & ~wfull);
assign wgraynext = (wbinnext>>1) ^wbinnext;
assign wfull_val = (wgraynext == {~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
always @(posedge wclk or negedge wrst_n)
if(!wrst_n)
wfull <= 1'b0;
else
wfull <= wfull_val;
//rptr_empty
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext,rbinnext;
wire rempty_val;
always @(posedge rclk or negedge rrst_n)
if(!rrst_n)
{rbin,rptr} <= 0;
else
{rbin,rptr} <= {rbinnext.rgraynext};
assign raddr = rbin[ADDRSIZE-1:0];
assign rbinnext = rbin + (rinc & ~rempty);
assign rgraynext = (rbinnext >>1) ^ rbinnext;
assign rempty_val = (rgraynext == rq2_wptr);
always @(posedge rclk or negedge rrst_n)
if(!rrst_n)
rempty <= 1'b1;
else
rempty <= rempty_val;
endmodule
和第一种差不多
加上alomst_full和almost_empty
module asy_fifo_1#(
parameter WIDTH = 8,
parameter DEPTH = 8
)(
input [WIDTH - 1 : 0] wr_data,
input wr_clk,
input wr_rstn,
input wr_en,
input rd_clk,
input rd_rstn,
input rd_en,
output reg fifo_full,
output reg fifo_empty,
output reg [WIDTH - 1 : 0] rd_data,
output almost_full,
output almost_empty
);
//定义读写指针
reg [$clog2(DEPTH) : 0] wr_ptr, rd_ptr;
//定义一个宽度为WIDTH,深度为DEPTH的fifo
reg [WIDTH - 1 : 0] fifo [DEPTH - 1 : 0];
//定义读数据
//写操作
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
wr_ptr <= 0;
else if(wr_en && !fifo_full) begin
fifo[wr_ptr[$clog2(DEPTH)-1:0]] <= wr_data;
wr_ptr <= wr_ptr + 1;
end
else
wr_ptr <= wr_ptr;
end
//读操作
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
rd_ptr <= 0;
rd_data <= 0;
end
else if(rd_en && !fifo_empty) begin
rd_data <= fifo[rd_ptr[$clog2(DEPTH)-1:0]];
rd_ptr <= rd_ptr + 1;
end
else
rd_ptr <= rd_ptr;
end
//定义读写指针格雷码
wire [$clog2(DEPTH) : 0] wr_ptr_g;
wire [$clog2(DEPTH) : 0] rd_ptr_g;
//读写指针转换成格雷码
assign wr_ptr_g = wr_ptr ^ (wr_ptr >>> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >>> 1);
//定义打拍延迟格雷码
reg [$clog2(DEPTH) : 0] wr_ptr_gr, wr_ptr_grr;
reg [$clog2(DEPTH) : 0] rd_ptr_gr, rd_ptr_grr;
//写指针同步到读时钟域
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn) begin
wr_ptr_gr <= 0;
wr_ptr_grr <= 0;
end
else begin
wr_ptr_gr <= wr_ptr_g;
wr_ptr_grr <= wr_ptr_gr;
end
end
//读指针同步到写时钟域
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn) begin
rd_ptr_gr <= 0;
rd_ptr_grr <= 0;
end
else begin
rd_ptr_gr <= rd_ptr_g;
rd_ptr_grr <= rd_ptr_gr;
end
end
//声明空满信号数据类型
// reg fifo_full;
// reg fifo_empty;
//格雷码转二进制
reg [$clog2(DEPTH) : 0] rd_ptr_grr_bin;
reg [$clog2(DEPTH) : 0] wr_ptr_grr_bin;
integer i;
always @(*)
begin
for(i=0;i<$clog2(DEPTH)+1;i=i+1)
begin
rd_ptr_grr_bin[i] = ^(rd_ptr_grr >> i);
wr_ptr_grr_bin[i] = ^(wr_ptr_grr >> i);
end
end
//写满判断
always @ (posedge wr_clk or negedge wr_rstn) begin
if(!wr_rstn)
fifo_full <= 0;
else if((wr_ptr_g[$clog2(DEPTH)] != rd_ptr_grr[$clog2(DEPTH)]) && (wr_ptr_g[$clog2(DEPTH) - 1] != rd_ptr_grr[$clog2(DEPTH) - 1]) && (wr_ptr_g[$clog2(DEPTH) - 2 : 0] == rd_ptr_grr[$clog2(DEPTH) - 2 : 0]))
fifo_full <= 1;
else
fifo_full <= 0;
end
reg [$clog2(DEPTH):0] data_avi;
always @(*)
begin
if(wr_ptr[$clog2(DEPTH)] == rd_ptr_grr_bin[$clog2(DEPTH)])
begin
data_avi = wr_ptr[$clog2(DEPTH)-1:0]-rd_ptr_grr_bin[$clog2(DEPTH)-1:0];
end
else
begin
data_avi = DEPTH - (rd_ptr_grr_bin[$clog2(DEPTH)-1:0]-wr_ptr[$clog2(DEPTH)-1:0]);
end
end
assign almost_full = (data_avi >= DEPTH-2)? 1'b1: 1'b0;
//读空判断
always @ (posedge rd_clk or negedge rd_rstn) begin
if(!rd_rstn)
fifo_empty <= 0;
else if(wr_ptr_grr[$clog2(DEPTH) : 0] == rd_ptr_g[$clog2(DEPTH) : 0])
fifo_empty <= 1;
else
fifo_empty <= 0;
end
reg [$clog2(DEPTH):0] room_avi;
always @(*)
begin
if(wr_ptr[$clog2(DEPTH)] == rd_ptr_grr_bin[$clog2(DEPTH)])
begin
//room_avi = wr_ptr[$clog2(DEPTH)-1:0]-rd_ptr_grr_bin[$clog2(DEPTH)-1:0];
room_avi = DEPTH - (wr_ptr_grr_bin[$clog2(DEPTH)-1:0]-rd_ptr[$clog2(DEPTH)-1:0]);
end
else
begin
//room_avi = DEPTH - (rd_ptr_grr_bin[$clog2(DEPTH)-1:0]-wr_ptr[$clog2(DEPTH)-1:0]);
room_avi = rd_ptr[$clog2(DEPTH)-1:0]-wr_ptr_grr_bin[$clog2(DEPTH)-1:0];
end
end
assign almost_empty = (room_avi >= DEPTH-2)? 1'b1: 1'b0;
endmodule
testbench
module asy_fifo_test_zzt(
);
parameter width = 8;
parameter depth = 8;
reg wr_clk, wr_en, wr_rstn;
reg rd_clk, rd_en, rd_rstn;
reg [width - 1 : 0] wr_data;
wire fifo_full, fifo_empty;
wire [width - 1 : 0] rd_data;
wire almost_full, almost_empty;
//实例化
asy_fifo_1 myfifo (
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.wr_rstn(wr_rstn),
.rd_rstn(rd_rstn),
.wr_en(wr_en),
.rd_en(rd_en),
.wr_data(wr_data),
.rd_data(rd_data),
.fifo_empty(fifo_empty),
.fifo_full(fifo_full),
.almost_full(almost_full),
.almost_empty(almost_empty)
);
//时钟
initial begin
rd_clk = 0;
forever #5 rd_clk = ~rd_clk;
end
initial begin
wr_clk = 0;
forever #15 wr_clk = ~wr_clk;
end
initial begin
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
#10;
wr_rstn <= 1'b0;
rd_rstn <= 1'b0;
#10;
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
end
reg pingpang;
initial
begin
pingpang = 1'b0;
end
always @(posedge wr_clk or negedge wr_rstn)
if(!wr_rstn)
begin
wr_en<= 1'b0;
wr_data <= 8'd0;
end
else if(!almost_full && ~pingpang)
begin
wr_en <= 1'b1;
wr_data <= wr_data + 8'd1;
end
else if(almost_full && ~pingpang)
begin
pingpang <= ~pingpang;
wr_en<= 1'b0;
wr_data <= wr_data;
end
else
begin
wr_en<= 1'b0;
wr_data <= wr_data;
end
always @(posedge rd_clk or negedge rd_rstn)
if(!rd_rstn)
begin
rd_en<= 1'b0;
end
else if(!almost_empty && pingpang)
begin
rd_en <= 1'b1;
end
else if(almost_empty && pingpang)
begin
rd_en<= 1'b0;
pingpang <= ~pingpang;
end
else
begin
rd_en<= 1'b0;
end
endmodule
因为上一种代码如果按照空满信号去控制数据的读写,还是会出现数据丢失的问题,所以如果可以的话还是可以按照almost_full和almost_empty信号去控制数据的读写。
almost_full和almost_empty状态的判断需要在各时钟域去计算FIFO还剩多少空间(或者还有多少数据),但是你既然都计算出来数据了,你也可以直接拿这个数据去判断空满,所以感觉这种方式又麻烦了,但是其他的方法也想不出,感觉增加这两个信号,麻烦了......
虚空与虚满
FIFO满信号在写时钟域判断,当读指针同步到写时钟域,与写指针相比较来判断满状态,写指针是最新的指针但同步过来的读指针却不是最新的,在读指针同步到写时钟域的过程中,FIFO还可能在进行着读操作,读指针可能还会增加,所以同步过来的读指针与写指针相比较,得出的的满状态不是最准确的。那这种不准确的结果会不会导致在判断满状态的过程中发生溢出?答案是不会。由于满状态的判断是一种悲观的或者说保守的策略,即写指针是最准确,读指针是不准确的值,当二者判断为满时,即停止向FIFO中写入数据,但是这时可能读指针要比同步过来的读指针要大或者说在同步过来的过程中,FIFO又读了几个数据,实际上FIFO应该是非满的状态,但是判为满,这就是虚满,虚满不会导致FIFO的溢出,因为这时FIFO还没满的时候就停止向FIFO中写入数据。
同理,FIFO空信号在读时钟域判断,当写指针同步到读时钟域,与读指针相比较来判断空状态,读指针是最新的指针但同步过来的写指针却不是最新的,在写指针同步到读时钟域的过程中,FIFO还可能在进行着写操作,写指针可能还会增加,所以同步过来的写指针与读指针相比较,得出的的空状态不是最准确的。那这种不准确的结果会不会导致在判断空状态的过程中发生空读?答案是不会。由于空状态的判断是一种悲观的或者说保守的策略,即读指针是最准确,写指针是不准确的值,当二者判断为空时,即停止从FIFO中读出数据,但是这时可能写指针要比同步过来的写指针要大或者说在同步过来的过程中,FIFO又写了几个数据,实际上FIFO应该是非空的状态,但是判为空,这就是虚空,虚空不会导致FIFO的空读,因为这时FIFO还没空的时候就停止从FIFO中读出数据。
在判满的过程中会出现空读吗?不会,在判满的过程中,写时钟域无法决定读的操作,需要由读时钟域来去控制不要出现空读,就又回到上面的问题。
在判空的过程中会出现溢出吗?不会,在判空的过程中,读时钟域无法决定写的操作,需要由写时钟域来去控制不要出现溢出,就又回到上面的问题。
读时钟与写时钟频率相差过大咋办?
读时钟与写时钟相差过大咋办?比如写时钟频率是读时钟频率的100倍,是否会出现风险?
在这种情况下当写时钟同步到读时钟域时,写指针可能又变化了很多次,写指针的每次变化不能都同步到读时钟域,因为写指针变化了很多次,读时钟域两次采样写指针,会出现两次采样不一样的位数比较多,两次采样的位数变化不再是1,这样打两拍的话会引起亚稳态。
是这样吗?
不是的!!!!!!
首先对于多位二进制数据跨时钟域不能用打两拍的方式是因为会出现多位同时变化的情况,引起亚稳态。
注意这里的同时,因为同时发生变化,在采样时,不同时钟域指针数据发生变化,可能无法满足寄存器的建立时间和保持时间,所以会出现亚稳态,所以我们采用格雷码,每次变化只又一位发生变化,这样即使发生亚稳态,一方面不会引起亚稳态的传播,一方面只有一位可能因为亚稳态出错,我们保守的判断空满状态的策略是可以容忍这种错误。
回到我们一开始讨论的问题,虽然对于读写时钟的频率相差比较大,两次采样格雷码之间会出现多位的不同,但这些位的变化不是同时变化,寄存器在采样时只可能有一位发生变化(因为格雷码在变化时只变化一位),换句话说,所谓跨时钟域出现亚稳态是因为当寄存器在采样时,正好碰到待采样信号发生变化,采样正好采样到变化的中间状态,所以会出现亚稳态,格雷码在两次采样时,虽然很多位不一样,但在寄存器采样时这些信号已经稳定了,没有发生变化,所以不会出现亚稳态,即使在采样时正在发生变化,也只是有一位信号在发生变化,所以是不会引起亚稳态。亚稳态出现的原因是带采样信号正在发生变化,已经发生的变化或者已经稳定了的变化不会引起亚稳态。
读写时钟相差过大可能会产生虚空和虚满,比如写地址同步到读时钟频率,判断此时读空与否,可能会判断已经读空,但是由于写时钟频率快,此时又写了很多,所以非空,但是只要深度大于200,因为在读时钟域打了2拍,这个就不会对数据安全性产生影响。
设计存在的问题可能是,对于FPGA来说,同时产生相差100倍的时钟频率比较困难,一般PLL锁相环输出时钟频率最大最小差值不到一百倍,这样可能最小的频率需要自己进行分频处理,时钟扇出质量不好,有timing问题。
复位的影响?
当对FIFO进行复位时,因为很多信号同时发生变化,会不会产生问题?
答案是不会。
首先读时钟域域写时钟域有自己的rst位
当复位时,FIFO里的数据清空,满信号被清空,写时钟域与读时钟域的所有寄存器清空置零,空信号这时候使能,不会发生问题。
当复位释放时,如果采用了异步复位同步释放,则也不会产生问题。
如何设计depth不是2的幂次的异步FIFO?
FIFO的深度必须是2的幂次吗?不是的,可以为任何数!!!
那怎么设计一个depth不是2的幂次的FIFO呢?整体结构和之前相同,难的是gray码怎么设计。
首先明确一点,如果地址为8个,那么指针就有16个,一位指针要比地址多一位。
如果地址是7个,那么指针就有14个,有两个指针地址我们用不到。
从上面的描述就可以看出,地址的个数可以是奇数也可以是偶数,指针的数量是地址数量的二倍,指针的位数要比地址的位数多一位来只是FIFO的空满状态。
我们设计深度为2的幂次是因为只有为2的幂次时,格雷码才能首尾相差一位,如果不是2的幂次,格雷码的首尾还能相差一位吗?
可以
从上面可以看出,如果需要7个地址,那么指针的数量就需要14个,那我们就可以将原来的16个格雷码掐头去尾,剩下的14个gray码也满足首尾相差一个bit。如果需要6个地址,那么指针的数量需要12个,那我们就可以接着将14个格雷码掐头去尾,剩下的12个gray码也满足首尾相差一位。能这么做的原因在于格雷码是对称码,他关于中间对称,除去第一位,剩下的位数关于中间对称,首尾只有最高位不同,那我们掐头去尾,剩下的除了最高位完全相等,只有最高位相反,也满足我们的要求。
具体怎么判断空满就不同于之前的前两位取反,就要自己找规律。
设计一个depth=1的异步FIFO
只需要考虑一个问题,只有深度为1,那么需要几位的地址或者指针呢?当然是1位就够了,那我们真的还需要一个指针吗?因为只有一个entry,当一次写,FIFO就满了,一次读,FIFO就空了。1个bit用来表示满和空就足够了。实际上这个就相当于CDC中的打两拍。
#2结构
2结构采用的是异步比较的方法去判断FIFO的空满状态。
从图上看,读写指针没有通过打两拍来同步到其他时钟域上,他们直接进行比较。因为我们跨时钟域的目的是为了得到空满信号,并在相应的时钟域上去控制FIFO的读写,所以我们可以在比较完读写指针之后将比较结果作为控制信号“异步置位同步释放”去控制空满信号,这样就减少了跨时钟域的信号的数量,提高了系统运行的速度。
这种结构相较于第一种结构来说,面积更小,速度更快。
FIFO的指针和地址都采用二进制数,在同步到其他时钟域时采用格雷码的形式。如果FIFO的地址是n位,那么FIFO的指针是n位。
具体的指针增加与转化格雷码的结构如上图,前面分析过,这种结构大大减少了组合逻辑的部分,虽然使用的资源相对较多,但是系统的最大速度提高了,实现起来也比较简单。
空满状态的判断
前面我们提到过,当两个指针相同时,就为满或空,那么怎么判断这个时候是空还是满呢?
方向
因为在FIFO满之前,写指针要将读指针套圈,所以看起来在FIFO满之前的时刻,写指针在追读指针,我们可以定义一个方向,这时把方向设为1,表明写指针在追读指针,当方向为1,读写指针相等,写指针追上读指针,FIFO为满
在FIFO空之前,读指针要追上写指针,这时把方向设为0,表明读指针要追上写指针,当方向为0,读写指针相等时,表明读指针已经追上写指针,FIFO为空。
如何去设置这个方向?一般的想法是当 FIFO 的字节计算超过某个预定上限,就认为 FIFO“going towards full(趋向满) ”,当字节计算低于预定下限时,就认为 FIFO“going towards empty(趋向空)”。
我们不妨选择 FIFO 容量的 75%和 25%作为门限。这样做比较有效,因为你只需比较指针的高两位就能决定是否越过门限。若用另一些值,你将不得不比较指针的所有位,而这有可能影响你所设计的系统的速度。
将FIFO的指针(格雷码)分为4个象限,根据地址的高两位来判断FIFO的方向。
如果写指针落后于读指针一个象限(两个指针不在一个象限),这表明写指针将要追上读指针,FIFO可能为满,在这种情况下方向Direction要置位1。
判断落后一个象限的方法可以通过比较指针的高两位,从图上可以看出,在四分之一地址空间的范围内,写指针的最高位与读指针的次高位相反,写指针的次高位与读指针的最高位相同。
当读指针落后于写指针一个象限,或者当复位时,就表明写指针要追上读指针,在这种情况下方向Direction 要置位为0.
判断落后一个象限的方法可以通过比较指针的高两位,从图上可以看出,在一个象限的范围内,读指针的最高位与写指针的次高位相反,读指针的次高位与写指针的最高位相同。当然在复位的时候方向Direction也要置位为0。
方向判断即为上图所示,注意方向置1还是置0,set和rst低电平有效
当FIFO复位时,方向direction同样置为0,表明FIFO将为空。
空满判断
当direction为1,同时读写指针相等时,这时FIFO为满。
当写指针相差一个象限就追上读指针时,dirset_n为低电平,direction为1,因为这时写指针和读指针不相等,所以afull_n为高电平,wfull为低电平。
当写指针与读指针在同一个象限,写指针就要追上读指针时,dirset_n为1,direction锁存之前的状态为1,因为这时写指针和读指针不相等,所以afull_n为高电平,wfull为低电平。
当写指针追上读指针时,这时dirset_n为1,direction锁存之前的状态,为1,afull_n为低电平,写时钟域上两个寄存器set为0,被置为1,wfull被置为1。
当读指针加一时,这时FIFO由于读出一个数据不是满状态,读指针与写指针不相等,如果两个指针还同在相同的地址空间内,directin还为1,afull_n为1,经过wclk的两个上升沿,wfull为0,满状态被清空。
当读指针超出写指针一个象限时,这时dirset_n为低电平,direction为1
当读指针远远超出写指针时,这时dirset_n为1,direction锁存之前的状态。
当direction为0,同时读写指针相等时,这时FIFO为空
当读指针相差一个象限就追上写指针时,dirrst_n为低电平,direction为0,因为这时写指针和读指针不相等,所以aempty_n为高电平,rempty为低电平。
当读指针与写指针在同一个象限,读指针就要追上写指针时,dirrst_n为1,direction锁存之前的状态为0,因为这时写指针和读指针不相等,所以aempty_n为高电平,rempty为低电平。
当读指针追上写指针时,这时dirrst_n为1,direction锁存之前的状态,为0,aempty_n为低电平,读时钟域上两个寄存器set为0,被置为1,rempty被置为1。
当写指针加一时,这时FIFO由于写入一个数据不是空状态,读指针与写指针不相等,如果两个指针还同在相同的地址空间内,directin还为0,aempty_n为1,经过wclk的两个上升沿,rempty为0,空状态被清空。
当写指针超出读指针一个象限时,这时dirrst_n为低电平,direction为0
当写指针远远超出读指针时,这时dirrst_n为1,direction锁存之前的状态。
总体结构为
verilog代码
cummings论文代码
module fifo_2_all
#(
parameter
DATASIZE = 8,
ADDRSIZE = 4
)
(
output [DATASIZE-1:0] rdata,
reg wfull,
reg rempty,
input [DATASIZE-1:0] wdata,
input winc,
input wclk,
input wrst_n,
input rinc,
input rclk,
input rrst_n
);
reg [ADDRSIZE-1:0] wptr,rptr;
//wire [ADDRSIZE-1:0] waddr,raddr;
wire aempty_n,afull_n;
reg [ADDRSIZE-1:0] rbin;
reg rempty2;
wire [ADDRSIZE-1:0] rgnext,rbnext;
reg [ADDRSIZE-1:0] wbin;
reg wfull2;
wire [ADDRSIZE-1:0] wgnext,wbnext;
//fifomem
localparam DEPTH = 1<<ADDRSIZE;
reg [DATASIZE-1:0] MEM[0:DEPTH-1];
assign rdata = MEM[rbin];
always @(posedge wclk)
if(winc)
MEM[wbin] <= wdata;
//async_cmp
localparam N = ADDRSIZE - 1;
reg direction;
wire high = 1'b1;
wire dirset_n;
assign dirset_n = ~((wptr[N]^rptr[N-1])&~(wptr[N-1]^rptr[N]));
wire dirclr_n ;
assign dirclr_n= ~((~(wptr[N]^rptr[N-1])&(wptr[N-1]^rptr[N]))|~wrst_n);
always @(posedge high or negedge dirset_n or negedge dirclr_n)
if(!dirclr_n)
direction <= 1'b0;
else if (!dirset_n)
direction <= 1'b1;
else
direction <= high;
assign aempty_n = ~ ((wptr == rptr) && !direction);
assign afull_n = ~((wptr == rptr)&& direction);
//rptr_empty
always @(posedge rclk or negedge rrst_n)
if(!rrst_n)
begin
rbin <= 0;
rptr <= 0;
end
else
begin
rbin <= rbnext;
rptr <= rgnext;
end
assign rbnext = ~rempty ? rbin + rinc : rbin;
assign rgnext = (rbnext>>1) ^ rbnext; // binary-to-gray conversion
always @(posedge rclk or negedge aempty_n)
if(!aempty_n)
{rempty,rempty2} <= 2'b11;
else
{rempty,rempty2} <= {rempty2,~aempty_n};
//wptr_full1
always @(posedge wclk or negedge wrst_n)
if(!wrst_n)
begin
wbin <= 0;
wptr <= 0;
end
else
begin
wbin <= wbnext;
wptr <= wgnext;
end
assign wbnext = !wfull ? wbin + winc : wbin;
assign wgnext = (wbnext >> 1)^ wbnext;//binanry-to-gray conversion
always @(posedge wclk or negedge wrst_n or negedge afull_n)
if(!wrst_n)
{wfull,wfull2}<= 2'b00;
else if (!afull_n)
{wfull,wfull2}<= 2'b11;
else
{wfull,wfull2}<={wfull2,~afull_n};
endmodule
testbench
module asy_fifo_test_zzt1(
);
parameter width = 8;
parameter depth = 8;
reg wr_clk, wr_en, wr_rstn;
reg rd_clk, rd_en, rd_rstn;
reg [width - 1 : 0] wr_data;
wire fifo_full, fifo_empty;
wire [width - 1 : 0] rd_data;
fifo_2_all #
(.DATASIZE(8),.ADDRSIZE(4))
fifo_2_all
(
.rdata(rd_data),
.wfull(fifo_full),
.rempty(fifo_empty),
.wdata(wr_data),
.winc(wr_en),
.wclk(wr_clk),
.wrst_n(wr_rstn),
.rinc(rd_en),
.rclk(rd_clk),
.rrst_n(rd_rstn)
);
//时钟
initial begin
rd_clk = 0;
forever #25 rd_clk = ~rd_clk;
end
initial begin
wr_clk = 0;
forever #30 wr_clk = ~wr_clk;
end
initial begin
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
#10;
wr_rstn <= 1'b0;
rd_rstn <= 1'b0;
#10;
wr_rstn <= 1'b1;
rd_rstn <= 1'b1;
end
//赋值
initial begin
wr_en = 0;
rd_en = 0;
wr_rstn = 1;
rd_rstn = 1;
#10;
wr_rstn = 0;
rd_rstn = 0;
#20;
wr_rstn = 1;
rd_rstn = 1;
@(negedge wr_clk)
wr_data = {$random}%30;
wr_en = 1;
repeat(15) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end
@(negedge wr_clk)
wr_en = 0;
@(negedge rd_clk)
rd_en = 1;
repeat(15) begin
@(negedge rd_clk);
end
@(negedge rd_clk)
rd_en = 0;
#150;
@(negedge wr_clk)
wr_en = 1;
wr_data = {$random}%30;
repeat(15) begin
@(negedge wr_clk)
wr_data = {$random}%30;
end
@(negedge wr_clk)
wr_en = 0;
#50;
$finish;
end
endmodule
一些问题
* 如果在aempty_n传递到读时钟域时,发生了读操作怎么办?当afull_n传递到写时钟域时,发生了写操作怎么办?
不会出现这种问题
如上图所示,aempty_n的传递和afull_n的传递经过了组合逻辑,在进行时序约束是需要满足上面两条关键路径的时延小于下一个寄存器的建立时间,在满足时序约束的条件下,当afull_n传递到写时钟域被下一个寄存器采样的时刻前,不会有新的写操作(上一个写操作进行完,因为异步比较,立即生成新的afull_n信号,该信号的关键路径又要满足写时钟域下一个寄存器的建立时间,所以相当于afull在一个写操作时钟之内完成了传递,afull信号的清空需要两个时钟),aempty同理。
* 当FIFO空时,aempty_n为0,当写入一个数据时,FIFO不为空,aempty_n为1,aempty的拉高与写时钟有关,与写时钟同步,但是rempty是在读时钟域上进行同步,两个不同时钟域上的采样可能会导致亚稳态,会不会有问题?
不会,从上面可以看出,这里采用了类似“异步置位同步释放”的方法,当aempty_n拉低时,寄存器立刻置位1,保证空信号能够立刻的置1,当aempty_n拉高时,rempty的拉高要经过两级寄存器,减少了亚稳态的几率。
* 因为aempty因为写时钟拉高,读时钟拉低,所以可能信号宽度非常小(高-低-高),可能两个寄存器无法置位,会不会有些影响?
没有。
如果两个寄存器都没有被置位,不会引起问题,因为这个时候aempty本来就是高电平,本不应该置位。
如果第一个寄存器被置位,第二个寄存器没有被置位,那么在下一个时钟第二个寄存器置位为1,再下一个时钟又恢复到0,因为空状态不会读,也不会造成影响。
如果第一个寄存器没有被置位,第二个寄存器被置位,那么空状态信号拉高,但在下一个时钟空状态信号又被拉低,空状态时不会读数据,所以也没有影响。
如果两个寄存器都被置位,那么就会生成两个时钟长的空状态信号,在这个期间不会进行读操作,也不会产生什么影响。
* 这里我们将空满信号通过类似于"异步复位同步释放"的方式去传递,可以不可直接异步比较得到空满信号,然后直接打两拍传到各自的时钟域上?
不行,打两拍还是会出现亚稳态,前面格雷码打两拍是因为如果有一位出错,我们采用悲观的判断策略,错误还能接受
但是打两拍空满信号,如果亚稳态后电平到达我们想要的电平,那还ok,如果本应该拉高,但是亚稳态出错,那么就会导致本应该拉高的空满信号没有拉高,导致最后出现溢出或空读的状况。
参考
(13条消息) 数字芯片设计面试问题——异步FIFO的本质和外延_dongker 的笔记的博客-CSDN博客_异步fifo面试题
今天华为面试题:异步FIFO读时钟是写时钟的100倍,或者写是读的100倍会出现什么问题? - FPGA/ASIC/IC前端设计 - EETOP 创芯网论坛 (原名:电子顶级开发网) -
怎么用Verilog语言描述同步FIFO和异步FIFO - 小翁同学 - 博客园 (cnblogs.com)
IC基础课:异步FIFO的"假"满空和"真"满空 - 知乎 (zhihu.com)
关于异步fifo的快转慢的问题 - 知乎 (zhihu.com)
(9条消息) 关于异步FIFO_fgupupup的博客-CSDN博客
(9条消息) 异步fifo简介_bleauchat的博客-CSDN博客_异步fifo
基于Verilog HDL的异步FIFO设计与实现.ashx (wanfangdata.com.cn)
(9条消息) 异步FIFO---Verilog实现_alangaixiaoxiao的博客-CSDN博客_异步fifo
Verilog中的FIFO设计-异步FIFO篇 (baidu.com)
(9条消息) 异步FIFO代码(包含almost_full以及almost_empty信号),测试代码,功能仿真结果_Roy_wang_24的博客-CSDN博客_almost_empty
(9条消息) 异步FIFO实现_Ter23LBJ的博客-CSDN博客