一、什么是异步FIFO
异步FIFO,是FIFO设计(First In First Out,即先入先出,首先写入的数据首先读出)中的一种。由于读写操作是独立的,异步FIFO常用于多比特位数据的跨时钟域传输。下文中将异步FIFO简称为FIFO。
在一个时钟域(写时钟域)的控制信号作用下,数据被写入FIFO缓冲存储器阵列。在另一时钟域(读时钟域)的控制信号作用下,数据从同一FIFO缓冲存储器阵列的另一个端口读出。从概念上讲,这样的假设看似简单,实际执行起来并不很容易。FIFO 设计的难点在于应该使用怎么样的FIFO指针,以及使用怎样的方法来确定FIFO缓冲存储器阵列的满状态和空状态。
二、使用怎样的指针?
在讨论如何设置合适的指针之前,我们应首先清楚FIFO中的指针是如何工作的。写指针(write pointer)总是指向下一个要写入数据的地址。同样,读指针(read pointer)总是指向下一个要读取数据的地址。在复位信号有效时,两个指针都被设置为零。此时,FIFO中不存储任何数据,为空状态(empty),读指针指向无效数据。当进行FIFO写操作时,写指针指向的地址位置被写入,然后写指针递增,指向下一个要写入的位置。当首个数据被写入FIFO后,写指针就会递增,读空信号无效,指向首个FIFO存储位置的读指针会立刻将首个有效数据进行输出,同时读指针也递增,指向下一个要读取的数据。
通过对指针工作方式的说明,我们可以知道:当两个指针在复位操作中都被重置为零时,或者当读指针从FIFO中读取最后一个字后追上写指针时,读写指针相等,FIFO为空;当写指针绕过并赶上读指针时,读写指针再次相等,FIFO 为满。这就产生了一个问题:当指针相等时,FIFO究竟处于空状态(empty),还是满状态(full)?
通过为指针增加额外的MSB的方法,可以对FIFO的空满状态进行有效的判断。当写指针递增至FIFO的最终地址时,写入指针将递增未使用的MSB位,同时将其余位设置为零。读指针也是如此。如果两个指针的MSB不同,则表示写指针比读指针多缠绕了一圈。如果两个指针的 MSB 相同,则表示两个指针缠绕的次数相同。
图1 利用添加了额外MSB的指针进行FIFO空满状态的判断
使用n位指针(其中 n-1位是访问整个FIFO内存缓冲区所需的地址位数),当包括MSB在内的两个指针各bit位均相等时,FIFO为空。而当两个指针除MSB外的各bit位均相等,MSB不相等时,FIFO为满。本文中的FIFO设计采用n位指针,用于具有2^(n-1)个可写入位置的FIFO,以进行空满状态的判断。
我们现在已经知道,可以通过对比写指针和读指针来进行FIFO空满状态的判断。问题在于,我们所设计的是异步FIFO,写指针位于写时钟域,读指针位于读时钟域,两个时钟域彼此异步。对于普通的二进制编码来说,其相邻数值的变化可能牵涉到多bit位上的变化。例如,从7到8,二进制编码的变化为从0111到1000,四个bit位全部产生了翻转,翻转过程中有14种可能的中间态。使用普通的二进制编码,将读(写)指针传输到写(读)时钟域进行对比判断时,很可能因为采集到编码变化的中间态从而产生误判。怎样解决这样的问题呢?
将普通二进制编码转为格雷码(Gray code)是一种行之有效的方法。格雷码相邻数值之间的转换仅需要变化一个bit位,大大减少了亚稳态的发生概率,从而消除了在同一时钟沿同时变化多bit位信号的问题。
图2 四位格雷码的与其十进制数值的对应情况
三、异步FIFO设计详解
异步FIFO设计的原理框图如下所示。
图3 异步FIFO设计的原理框图
为便于对各部分进行分析理解,将整个FIFO设计划分为六个Verilog模块。分别是:
①、fifo1.v:顶层封装模块。包括了FIFO设计所有的对外部的输入、输出信号,并对所有的模块进行例化;
②、fifomem.v:这是同时由写时钟域和读时钟域访问的FIFO内存缓冲区。本文中缓冲区表示为一个实例化的同步双端口RAM(DPRAM);
③、sync_r2w.v:同步模块,用于将读指针同步到写时钟域。同步后的读指针将被 wptr_full模块用于写满信号的判断。该模块仅包含与写时钟同步的触发器,不包含其他逻辑;
④、sync_w2r.v:同步模块,用于将写指针同步到读时钟域。同步后的写指针将被rptr_empty模块用于读空信号的判断。该模块仅包含与读时钟同步的触发器,不包含其他逻辑;
⑤、rptr_empty.v:该模块与读时钟域完全同步。用于进行读地址自增,以及读空信号的判断;
⑥、wptr_full.v:该模块与写时钟域完全同步。用于进行写地址自增,以及写满信号的判断。
下面依次对各模块的功能以及需要注意的问题进行说明:
3.1 fifo1.v
module fifo1 #(parameter DSIZE = 8,
parameter ASIZE = 4)
(output [DSIZE-1:0] rdata,
output wfull,
output rempty,
input [DSIZE-1:0] wdata,
input winc, wclk, wrst_n,
input rinc, rclk, rrst_n);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wq2_rptr, rq2_wptr;
sync_r2w sync_r2w (.wq2_rptr(wq2_rptr), .rptr(rptr),.wclk(wclk), .wrst_n(wrst_n));
sync_w2r sync_w2r (.rq2_wptr(rq2_wptr), .wptr(wptr),.rclk(rclk), .rrst_n(rrst_n));
fifomem #(DSIZE, ASIZE) fifomem(.rdata(rdata), .wdata(wdata),.waddr(waddr), .raddr(raddr),.wclken(winc), .wfull(wfull),.wclk(wclk));
rptr_empty #(ASIZE) rptr_empty(.rempty(rempty),.raddr(raddr),.rptr(rptr), .rq2_wptr(rq2_wptr),.rinc(rinc), .rclk(rclk),.rrst_n(rrst_n));
wptr_full #(ASIZE) wptr_full(.wfull(wfull), .waddr(waddr),.wptr(wptr), .wq2_rptr(wq2_rptr),.winc(winc), .wclk(wclk),.wrst_n(wrst_n));
endmodule
顶层FIFO模块是一个参数化的FIFO设计,所有子块都使用推荐的命名端口连接方式进行实例化。这里所设计的FIFO中传输数据的位宽为[DSIZE-1,0],即8位宽;所使用指针的位宽为[ASIZE,0],即5位宽(包括4位寻址DPRAM的地址以及1位额外的MSB)。FIFO从外部接入的信号包括写数据wdata、写使能winc、写时钟wclk、写复位wrst_n、读使能rinc、读时钟rclk、读复位rrst_n。FIFO向外部输出的信号包括读数据rdata、读空信号rempty、写满信号wfull。
3.2 fifomem.v
module fifomem #(parameter DATASIZE = 8,
parameter ADDRSIZE = 4)
(output [DATASIZE-1:0] rdata,
input [DATASIZE-1:0] wdata,
input [ADDRSIZE-1:0] waddr, raddr,
input wclken, wfull, wclk);
`ifdef VENDORRAM
vendor_ram mem (.dout(rdata), .din(wdata),.waddr(waddr), .raddr(raddr),.wclken(wclken),.wclken_n(wfull), .clk(wclk));
`else
localparam DEPTH = 1<<ADDRSIZE;
reg [DATASIZE-1:0] mem [0:DEPTH-1];
assign rdata = mem [raddr];
always @(posedge wclk)
if (wclken && !wfull)
mem [waddr] <= wdata;
`endif
endmodule
可以看到,fifomem.v部分实质上是一块双端口RAM(DPRAM,Dual Port RAM)。源码中使用了预编译语句,由于事先并未对vendor_ram进行定义,仅需要关注else到endif之间的部分就好。FIFO存储的深度DEPTH=1<<ADDRSIZE=10000=16(即可写入数据的个数)。DPRAM实质上由深度为DEPTH、存储数据位宽为DATASIZE的存储器型变量mem表示。采用时序逻辑,当时钟上升沿来临时,若写使能有效且FIFO未写满时,对FIFO写入数据。采用组合逻辑,对写入的数据进行输出。
3.3 sync_r2w.v
module sync_r2w #(parameter ADDRSIZE = 4)
(rptr,wclk,wrst_n,wq2_rptr);
input [ADDRSIZE:0] rptr;
input wclk,wrst_n;
output [ADDRSIZE:0] wq2_rptr;
reg [ADDRSIZE:0] wq1_rptr;
reg [ADDRSIZE:0] wq2_rptr;
always@ (posedge wclk or negedge wrst_n)
begin
if (!wrst_n)
begin
wq1_rptr <= 0;
wq2_rptr <= 0;
end
else
begin
wq1_rptr <= rptr;
wq2_rptr <= wq1_rptr;
end
end
endmodule
将读时钟域中转换为格雷码的读地址rptr,通过写时钟域中的两级D触发器以wq2_rptr的形式(即:打两拍)输入到写时钟域中。注意采用非阻塞幅值的方式,以综合出两级D触发器。
3.4 sync_w2r.v
module sync_w2r #(parameter ADDRSIZE = 4)
(wptr,rclk,rrst_n,rq2_wptr);
input [ADDRSIZE:0] wptr;
input rclk,rrst_n;
output [ADDRSIZE:0] rq2_wptr;
reg [ADDRSIZE:0] rq1_wptr;
reg [ADDRSIZE:0] rq2_wptr;
always@ (posedge rclk or negedge rrst_n)
begin
if (!rrst_n)
begin
rq1_wptr <= 0;
rq2_wptr <= 0;
end
else
begin
rq1_wptr <= wptr;
rq2_wptr <= rq1_wptr;
end
end
endmodule
将写时钟域中转换为格雷码的写地址wptr,通过读时钟域中的两级D触发器以rq2_wptr的形式输入到读时钟域中。注意采用非阻塞幅值的方式。
3.5 rptr_empty.v
module rptr_empty #(parameter ADDRSIZE = 4)
(rq2_wptr,rinc, rclk, rrst_n,
rempty,raddr,rptr);
input [ADDRSIZE :0] rq2_wptr;
input rinc, rclk, rrst_n;
output rempty;
output [ADDRSIZE-1:0] raddr;
output [ADDRSIZE :0] rptr;
reg rempty;
reg [ADDRSIZE :0] rptr;
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext, rbinnext;
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
特别要注意这部分中各信号的区别。本模块中用到的几个信号包括:传输到读时钟域的格雷码写指针rq2_wptr、二进制读指针rbin、下一拍的二进制读指针rbinnext、格雷码读指针rptr、下一拍的格雷码读指针rgraynext、写入到RAM中的二进制读地址raddr。
采用时序逻辑,实现对二进制指针、格雷码指针的控制(并非一直递增,当读使能有效且未读空时,读地址才能自增)。
前文中提到,n位指针可用于具有2^(n-1)个可写入位置的FIFO。将5位读指针的后4位作为读地址raddr即可实现fifomem.v中对读地址的寻址。
采用组合逻辑,即可实现从rbinnext(二进制码)到rgraynext(格雷码)的转换。具体方法比较简单,这里不再赘述。
前文中提到,使用n位指针(其中 n-1位是访问整个FIFO内存缓冲区所需的地址位数),当包括MSB在内的两个指针各bit位均相等时,FIFO为空。对于格雷码指针这一条件仍成立。为了更高效地进行读空信号的判断,实际上采用了下一拍的格雷码读指针rgraynext与传输到读时钟域的格雷码写指针rq2_wptr进行比较,二者各bit位完全相同时,即认为FIFO读空。否则,读空信号无效,FIFO持续进行读操作。
3.6 wptr.full.v
module wptr_full #(parameter ADDRSIZE = 4)
(wq2_rptr,winc, wclk, wrst_n,
wfull,waddr,wptr);
input [ADDRSIZE :0] wq2_rptr;
input winc, wclk, wrst_n;
output wfull;
output [ADDRSIZE-1:0] waddr;
output [ADDRSIZE :0] wptr;
reg wfull;
reg [ADDRSIZE :0] wptr;
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext, wbinnext;
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;
endmodule
本模块中用到的几个信号包括:传输到写时钟域的格雷码读指针wq2_rptr、二进制写指针wbin、下一拍的二进制写指针wbinnext、格雷码写指针wptr、下一拍的格雷码写指针wgraynext、写入到RAM中的二进制写地址waddr。
采用时序逻辑,实现对二进制指针、格雷码指针的控制(并非一直递增,当写使能有效且未写满时,写地址才能自增)。
前文中提到,n位指针可用于具有2^(n-1)个可写入位置的FIFO。将5位写指针的后4位作为写地址waddr即可实现fifomem.v中对写地址的寻址。
采用组合逻辑,即可实现从wbinnext(二进制码)到wgraynext(格雷码)的转换。具体方法比较简单,这里不再赘述。
前文中提到,使用n位指针(其中 n-1位是访问整个FIFO内存缓冲区所需的地址位数),当两个指针除MSB外的各bit位均相等,MSB不相等时,FIFO为满。对于格雷码指针这一条件需要做出修改。具体来说,两个指针的最高两位MSB需要恰好相反,其他各bit位相同。为了更高效地进行写满信号的判断,实际上采用了下一拍的格雷码写指针wgraynext与传输到写时钟域的格雷码读指针wq2_rptr进行比较,二者最高两位MSB需要恰好相反,其他各bit位相同时,即认为FIFO写满。否则,写满信号无效,FIFO持续进行写操作。
四、testbench文件撰写以及时序图分析
根据FIFO的特点,不难写出对其进行测试的testbench文件。
`timescale 1ns / 1ps
module fifo1_tb;
parameter ASIZE = 4;
parameter DSIZE = 8;
reg winc,wclk,wrst_n,rinc,rclk,rrst_n;
wire wfull,rempty;
reg [DSIZE-1:0] wdata;
wire [DSIZE-1:0] rdata;
reg init_done;
initial
begin
winc = 0;
wclk = 0;
wrst_n = 1;
rinc = 0;
rclk = 0;
rrst_n = 1;
init_done = 0;
#30 wrst_n = 0;
rrst_n = 0;
#30 wrst_n = 1;
rrst_n = 1;
#30 init_done = 1;
end
always #2 wclk = ~wclk;
always #4 rclk = ~rclk;
always @(*)
begin
if (init_done)
begin
winc = 1;
rinc = 1;
end
end
always @(posedge wclk)
begin
if (~init_done)
wdata <= 'b0;
else if ( {winc,wfull} == 2'b10 )
wdata <= wdata + 1;
else
wdata <= wdata;
end
fifo1 fifo1_test (.winc(winc),.wclk(wclk),.wrst_n(wrst_n),
.rinc(rinc),.rclk(rclk),.rrst_n(rrst_n),
.wdata(wdata),.rdata(rdata),.wfull(wfull),.rempty(rempty));
endmodule
注意,为方便进行时序分析,这里采用了写使能winc、写满信号wfull对写数据自增进行控制。读/写时钟的周期可以方便地进行修改,以便观察快慢时钟对比下的FIFO运行情况。
4.1 写快(2ns)读慢(4ns)
通过Vivado进行时序波形的查看。首先设置写时钟2ns一翻转,读时钟4ns一翻转。前90ns内完成了部分信号的复位置0。90ns时令读写使能有效,正式开始进行数据传输。
可以看到,90ns时,开始向FIFO中写入数据,wdata开始递增。需要注意到,这里显示出的wdata是受控递增的外部写数据,真实写入FIFO中的数据会比这里显示的wdata慢一拍写时钟。而由于读数据rdata为组合逻辑输出,90ns时rdata输出初始化的写数据wdata,即0信号。
当下一写时钟来临时(94ns),mem[0]处才正式写入第一个写数据01(十六进制)。由于读数据rdata为组合逻辑输出,94ns时rdata立即输出01信号。而01信号的读出却跨越了多个时钟节拍,原因是写指针需要多个时钟节拍才能传入读时钟域中与读指针作比较,使读空信号无效后才可持续读出新的信号。
可以看到,178ns时,用于判断写满信号的wgraynext和wq2_rptr分别为1d(11101)和04(00101),恰好满足高两位MSB取反,其余位相同的写满信号条件,写满信号有效。因而在下一写时钟节拍来临时(182ns),写数据wdata不再递增(这里在testbench文件中利用winc和wfull控制了写数据的输入。若按恒定节拍从外部输入数据,会造成漏读数据的情况,不便于时序图观察)。同时(182ns),判断写满信号的wgraynext和wq2_rptr分别为1d(11101)和04(00100),不再满足写满信号有效条件,写满信号无效。后续各时钟上升沿处分析可以此类推。
4.2 写快(2ns)读慢(32ns)
设置读时钟32ns一翻转,仿真时序图如下:
可以看到,由于缓慢的读时钟,在rq2_wptr来得及发生变化之前,写满信号就先一步有效(wgraynext和wq2_rptr高两位MSB相反,其余位相同),写数据不再自增加,以等待数据读出。
160ns时,rq2_wptr终于发生变化(在读时钟打了两拍的情况下,这种变化是必然的),与rgraynext不全等,在下一读时钟节拍来临时使得读空信号rempty无效,并在下下一读时钟节拍来临时对外输出数据。
4.3 读快(2ns)写慢(4ns)
设置写时钟4ns一翻转,读时钟2ns一翻转。时序仿真如下:
时序分析步骤与写快读慢情况类似。需要注意的是,这里由于init_done设置的原因,导致无效数据00被写入mem中且早期未被覆盖。设置init_done为92ns时可以解决这一问题。具体来说,设置init_done信号与写时钟上升沿处于同一时刻即可,以使当前时刻的写时钟上升沿无效。
4.4 读快(2ns)写慢(32ns)
笔者在时序仿真时发现,原有的采用组合逻辑对读数据进行输出的方式存在一定的问题。如下图所示。
当FIFO写入数据后,在rq2_wptr与rgraynext的作用下,读空信号无效,开始读取FIFO中的数据。此时,由于采用了组合逻辑进行读数据的输出,rdata会输出当前读地址mem[01]处的数据。事实上,此时该处还未来得及写入数据,这就造成了无效数据的输出。
将读数据采用时序逻辑的方法进行输出,能够有效解决这一问题。注意,这时已经将读时钟、读使能等信号接入RAM中,FIFO框图相较于图3已经发生了变化。下面贴出修改后的fifomem.v代码。此时,顶层模块fifo1也要对例化模块的端口作出相应的修改。
module fifomem #(parameter DATASIZE = 8,
parameter ADDRSIZE = 4)
(output reg [DATASIZE-1:0] rdata,
input [DATASIZE-1:0] wdata,
input [ADDRSIZE-1:0] waddr, raddr,
input wclken, wfull, wclk,
input rinc, rempty, rclk);
// reg [DATASIZE-1:0] rdata;
`ifdef VENDORRAM
vendor_ram mem (.dout(rdata), .din(wdata),.waddr(waddr), .raddr(raddr),.wclken(wclken),.wclken_n(wfull), .clk(wclk));
`else
localparam DEPTH = 1<<ADDRSIZE;
reg [DATASIZE-1:0] mem [0:DEPTH-1];
// assign rdata = mem [raddr];
always @(posedge rclk)
if (rinc && !rempty)
rdata <= mem [raddr];
always @(posedge wclk)
if (wclken && !wfull)
mem [waddr] <= wdata;
`endif
endmodule
此时仿真时序图如下:
由于对读数据添加了时序控制,不会再产生输出无效数据的情况!经测试,对读数据添加时序控制后,在4.1、4.2、4.3的相应情况下均没有出错。
五、FIFO设计中的一些重要问题
(待补充)