读了Cummings的论文,对FIFO设计有了一定的掌握,在此进行总结梳理,以便日后复习。
文章目录
1.FIFO的概念
FIFO 是英文 First In First Out 的缩写,是一种先进先出的数据缓存器,它与普通存储器的区别是:没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
2.什么情况下用 FIFO?
一般用于不同时钟域之间的数据传输,比如FIFO的一端是 AD数据采集,另一端时计算机的 PCI 总线,假设其 AD 采集的速率为16 位 100Kbps,那么每秒的数据量为 100K×16bit=1.6Mbps,而 PCI总线的速度为 33MHz,总线宽度 32bit,其最大传输速率为在两个不同的时钟域间就可以采用 FIFO 来作为数据缓冲。
另外对于不同宽度的数据接口也可以用 FIFO,例如单片机位 8 位数据输出,而 DSP 可能是 16 位数据输入,在单片机与 DSP 连接时就可以使用FIFO 来达到数据匹配的目的。
3.同步/异步FIFO
FIFO的分类根均FIFO工作的时钟域,可以将FIFO分为同步FIFO和异步FIFO。
同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。
异步FIFO是指读写时钟不一致,读写时钟是互相独立的。
4.FIFO的常见参数
FIFO的宽度:即FIFO一次读写操作的数据位;
FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
5.二进制码空满检测
异步FIFO的写指针总是指向下一个要被写入的地址,当发生一次写操作时,写指针所指的地址填入数据,然后写指针地址增加。
读指针总是指向当前FIFO中下一个要被读出的数据所在的地址,当发生一次读操作时,读指针所指的地址的数据被取出,然后读指针地址增加。
复位时,读写指针都指向FIFO缓冲区的0地址。
FIFO空的标志发生时是当读的指针和写的指针相等时。
当读写指针复位到0的时候如清空FIFO等操作的时候,或者是当读的指针赶上了写指针。
FIFO满的标志也是当读指针和写指针相等的时候。
当写指针已经环绕一周(wrapped around),然后追上读指针的时候。
因此就存在一个问题:当读写指针相等的时候,如何判断是读空还是写满。
为了区分到底是满状态还是空状态,可以采用以下方法:
在指针中添加一个额外的位(extra bit),当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1(0变1,1变0),其它位回零。对读指针也进行同样的操作。
此时,对于深度为2的n次方的FIFO,需要的读/写指针位宽为(n+1)位;如对于深度为8的FIFO,需要采用4bit的计数器;
0000(0)~0111(7)~1000(8) ~1001(9)~1111(15) ~0000(16)…
MSB作为折回标志位,而低3位作为二进制地址指针。
- 如果两个指针的MSB不同,而其余位相同,说明写指针比读指针多折回了一圈,写满;如r_addr=0000,而w_addr = 1000。
- 如果两个指针的MSB相同,且其余位相同,则说明两个指针折回的次数相等,读空。
到这里逻辑就很清晰了,就可以去设计FIFO的架构了。如下
架构图1
架构图2
winc为写使能;rinc为读使能。
到这里,写指针wptr和读指针rptr是4bit;
wptr经读时钟域二级D触发器同步后成为rq2_wptr,送去读空标志位产生模块与读指针进行比较,进而判断是否为空,从而产生读空标志位rempty;
rptr经写时钟域二级D触发器同步后成为wq2_rptr,送去写满标志位产生模块与写指针进行比较,进而判断是否为满,从而产生写满标志位wfull;
送入RAM的写地址waddr和读地址raddr,是只取写指针wptr、读指针rptr的低三位即可。
但是!!!容易亚稳态!!!
在将读指针发送到写时钟域下进行同步时,如果采用二进制,那么就会面临地址指针同时有多位变化的情况。比如0011->0100,一次就变了3位。在数电的学习中我们知道,这种情况是要尽量避免的,容易引起亚稳态。
rd_ptr2sync 的3和4、4和5之间的中间态是到各寄存器的时钟rd_clk存在偏差而引起的。
二进制的递增操作,在大多数情况下都会有两位或者两以上的bit位在同一个递增操作内发生变化,但由于实际电路中会存在时钟偏差和不同的路径延时,二进制计数器在自增时会不可避免地产生错误的中间结果。
由于rd_clk上升沿到达三个寄存器的时间各不相同,这就导致了rd_ptr2sync的值从3’b011跳变3’b100的过程中经历了:3’b011=>3’b111=>3’b101=>3’b100,中间结果经历了3=>7=>5=>4的过程,持续的时间虽然相对短暂,但是这些不正确的中间结果完全有可能被其它时钟域的同步模块采集到而产生错误的动作。
由此可见,要避免中间结果的产生,其中一个可行的方案就是使被同步模块采集的数据递变时,每次只有一个bit位发生改变,即格雷码计数器。
6.格雷码空满检测
格雷码一个最大的特点就是在递增或递减的过程中,每次只变化一位,这是它最大的优点。同时它也有自己的局限性,那就是循环计数深度必须是2的n次幂,否则就失去了每次只变化1位的特性。
深度为16的二进制及格雷码递变表如下:
6.1 二进制和格雷码的相互转换
6.1.1 二进制到格雷码
6.1.2 格雷码到二进制
格雷码转换代码如下:
assign gray_code = (bin_code>>1) ^ bin_code;
6.2 格雷码空满检测逻辑
如上图,是二进制地址码转换成格雷码的框图,格雷码为了具备空满检测的功能,也需要加一位MSB,但检测方法跟二进制有所不同。
-
对于“空”的判断依据依然是:写指针和读指针完全相等(包括MSB);
-
对于“满”的判断:最高位相反 && 次高位相反 && 其余低位相同。
如上图,gray码除了MSB外,上下两部分具有镜像对称的特点,当读指针指向7(0100),写指针指向8(1100)时,除了MSB,其余位皆相同,但并不能说它为满。
因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:最高位相反 && 次高位相反 && 其余低位相同。
如读指针指向7(0100),写指针指向15(1000),此时为满状态。
读空标志产生逻辑:
读空标志产生时序
写满标志位产生逻辑:
代码如下:
assign full = (wr_addr_gray =={~(rd_addr_gray_d2[addr_width-:2]),rd_addr_gray_d2[addr_width-2:0]});//高两位不同
assign empty = ( rd_addr_gray == wr_addr_gray_d2 );
7. 跨时域比较的同步化操作
跨时钟域的问题:上面我们已经提到要通过比较读写指针来判断产生读空和写满信号,但是读指针是属于读时钟域的,写指针是属于写时钟域的,而异步FIFO的读写时钟域不同,是异步的,要是将读时钟域的读指针与写时钟域的写指针不做任何处理直接比较肯定是错误的,因此我们需要进行同步处理以后再进行比较。
解决方法:两级寄存器同步 + 格雷码
- 将写时钟域的格雷码写指针同步到读时钟域,将同步后的格雷码写指针与读时钟域的格雷码读指针进行比较产生读空信号
- 将读时钟域的格雷码读指针同步到写时钟域,将同步后的格雷码读指针与写时钟域的格雷码写指针进行比较产生写满信号
补充:设计的时候读写指针用了至少两级寄存器同步,同步会消耗至少两个时钟周期,势必会使得判断空或满有所延迟,这会不会导致设计出错呢?
异步FIFO通过比较读写指针进行满空判断,但是读写指针属于不同的时钟域,所以在比较之前需要先将读写指针进行同步处理,将写指针同步到读时钟域再和读指针比较进行FIFO空状态判断,因为在同步写指针时需要时间,而在这个同步的时间内有可能还会写入新的数据,因此同步后的写指针一定是小于或者等于当前实际的写指针,所以此时判断FIFO为空不一定是真空,这样更加保守,一共不会出现空读的情况,虽然会影响FIFO的性能,但是并不会出错,同理将读指针同步到写时钟域再和写指针比较进行FIFO满状态判断,同步后的读指针一定是小于或者等于当前的读指针,所以此时判断FIFO为满不一定是真满,这样更保守,这样可以保证FIFO的特性:FIFO空之后不能继续读取,FIFO满之后不能继续写入。总结来说异步逻辑转到同步逻辑不可避免需要额外的时钟开销,这会导致满空趋于保守,但是保守并不等于错误,这么写会稍微有性能损失,但是不会出错。
8. 代码与仿真
8.1 单module式
module fifo_async#(
parameter data_width = 16,
parameter data_depth = 256,
parameter addr_width = 8
)
(
input rst,
input wr_clk,
input wr_en,
input [data_width-1:0] din,
input rd_clk,
input rd_en,
output reg valid,
output reg [data_width-1:0] dout,
output empty,
output full
);
reg [addr_width:0] wr_addr_ptr;//地址指针,比地址多一位,MSB用于检测在同一圈
reg [addr_width:0] rd_addr_ptr;
wire [addr_width-1:0] wr_addr;//RAM 地址
wire [addr_width-1:0] rd_addr;
wire [addr_width:0] wr_addr_gray;//地址指针对应的格雷码
reg [addr_width:0] wr_addr_gray_d1;
reg [addr_width:0] wr_addr_gray_d2;
wire [addr_width:0] rd_addr_gray;
reg [addr_width:0] rd_addr_gray_d1;
reg [addr_width:0] rd_addr_gray_d2;
reg [data_width-1:0] fifo_ram [data_depth-1:0];
//=========================================================write fifo
genvar i;
generate
for(i = 0; i < data_depth; i = i + 1 )
begin:fifo_init
always@(posedge wr_clk or posedge rst)
begin
if(rst)
fifo_ram[i] <= 'h0;//fifo复位后输出总线上是0,并非ram中真的复位。可无
else if(wr_en && (~full))
fifo_ram[wr_addr] <= din;
else
fifo_ram[wr_addr] <= fifo_ram[wr_addr];
end
end
endgenerate
//========================================================read_fifo
always@(posedge rd_clk or posedge rst)
begin
if(rst)
begin
dout <= 'h0;
valid <= 1'b0;
end
else if(rd_en && (~empty))
begin
dout <= fifo_ram[rd_addr];
valid <= 1'b1;
end
else
begin
dout <= 'h0;//fifo复位后输出总线上是0,并非ram中真的复位,只是让总线为0;
valid <= 1'b0;
end
end
assign wr_addr = wr_addr_ptr[addr_width-1-:addr_width];
assign rd_addr = rd_addr_ptr[addr_width-1-:addr_width];
//=============================================================格雷码同步化
always@(posedge wr_clk )
begin
rd_addr_gray_d1 <= rd_addr_gray;
rd_addr_gray_d2 <= rd_addr_gray_d1;
end
always@(posedge wr_clk or posedge rst)
begin
if(rst)
wr_addr_ptr <= 'h0;
else if(wr_en && (~full))
wr_addr_ptr <= wr_addr_ptr + 1;
else
wr_addr_ptr <= wr_addr_ptr;
end
//=========================================================rd_clk
always@(posedge rd_clk )
begin
wr_addr_gray_d1 <= wr_addr_gray;
wr_addr_gray_d2 <= wr_addr_gray_d1;
end
always@(posedge rd_clk or posedge rst)
begin
if(rst)
rd_addr_ptr <= 'h0;
else if(rd_en && (~empty))
rd_addr_ptr <= rd_addr_ptr + 1;
else
rd_addr_ptr <= rd_addr_ptr;
end
//========================================================== translation gary code
assign wr_addr_gray = (wr_addr_ptr >> 1) ^ wr_addr_ptr;
assign rd_addr_gray = (rd_addr_ptr >> 1) ^ rd_addr_ptr;
assign full = (wr_addr_gray == {~(rd_addr_gray_d2[addr_width-:2]),rd_addr_gray_d2[addr_width-2:0]}) ;//高两位不同
assign empty = ( rd_addr_gray == wr_addr_gray_d2 );
endmodule
8.2 多module式
顶层模块
module AsyncFIFO#(
parameter ADDR_SIZE = 4,
parameter DATA_SIZE = 8
)
(
input [DATA_SIZE-1:0] wdata,
input winc,
input wclk,
input wrst_n,
input rinc,
input rclk,
input rrst_n,
output [DATA_SIZE-1:0] rdata,
output wfull,
output rempty
);
wire [ADDR_SIZE-1:0] waddr,raddr;
wire [ADDR_SIZE:0] wptr,rptr,wq2_rptr,rq2_wptr;
sync_r2w #(
.ADDR_SIZE(ADDR_SIZE)
)
I1_sync_r2w(
.wq2_rptr(wq2_rptr),
.rptr(rptr),
.wclk(wclk),
.wrst_n(wrst_n)
);
sync_w2r #(
.ADDR_SIZE(ADDR_SIZE)
)I2_sync_w2r(
.rq2_wptr(rq2_wptr),
.wptr(wptr),
.rclk(rclk),
.rrst_n(rrst_n)
);
DualRAM #(
.ADDR_SIZE(ADDR_SIZE),
.DATA_SIZE(DATA_SIZE)
)I3_DualRAM(
.rdata(rdata),
.wdata(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(winc),
.wclk(wclk)
);
rptr_empty #(
.ADDR_SIZE(ADDR_SIZE)
)I4_rptr_empty(
.rempty(rempty),
.raddr(raddr),
.rptr(rptr),
.rq2_wptr(rq2_wptr),
.rinc(rinc),
.rclk(rclk),
.rrst_n(rrst_n));
wptr_full #(
.ADDR_SIZE(ADDR_SIZE)
)I5_wptr_full(
.wfull(wfull),
.waddr(waddr),
.wptr(wptr),
.wq2_rptr(wq2_rptr),
.winc(winc),
.wclk(wclk),
.wrst_n(wrst_n));
endmodule
双口RAM模块
module DualRAM #(
parameter DATA_SIZE = 8,//数据位宽
parameter ADDR_SIZE = 4//FIFO地址宽度
)(
input wclken,
input wclk,
input [ADDR_SIZE-1:0] raddr,
input [ADDR_SIZE-1:0] waddr,
input [DATA_SIZE-1:0] wdata,
output [DATA_SIZE-1:0] rdata
);
localparam RAM_DEPTH = 1<<ADDR_SIZE;//RAM深度,1左移4位为16
reg [DATA_SIZE-1:0] mem [0:RAM_DEPTH-1];//开辟内存
always@(posedge wclk) begin
if(wclken==1'b1) begin
mem[waddr] <= wdata;
end
else begin
mem[waddr] <= mem[waddr];//保持
end
end
assign rdata = mem[raddr];//给地址直接出数据
endmodule
写指针同步到读时钟
module sync_w2r#(
parameter ADDR_SIZE = 4
)
(
input [ADDR_SIZE:0] wptr,
input rclk,
input rrst_n,
output reg [ADDR_SIZE:0] rq2_wptr
);
reg [ADDR_SIZE:0] rq1_wptr;
//D触发器,两级同步
always@(posedge rclk or negedge rrst_n) begin
if(!rrst_n) begin
{rq2_wptr,rq1_wptr} <=0;
end
else begin
{rq2_wptr,rq1_wptr} <= {rq1_wptr,wptr};
end
end
endmodule
读指针同步到写时钟
module sync_r2w#(
parameter ADDR_SIZE = 4)
(
input [ADDR_SIZE:0] rptr,
input wclk,
input wrst_n,
output reg [ADDR_SIZE:0] wq2_rptr
);
reg [ADDR_SIZE:0] wq1_rptr;
//D触发器,两级同步
always@(posedge wclk or negedge wrst_n) begin
if(!wrst_n) begin
{wq2_rptr,wq1_rptr} <= 0;
end
else begin
{wq2_rptr,wq1_rptr} <= {wq1_rptr,rptr};
end
end
endmodule
判空模块
module rptr_empty#(
parameter ADDR_SIZE = 4
)
(
output reg rempty,
output [ADDR_SIZE-1:0] raddr,//输出到RAM的读地址
output reg [ADDR_SIZE:0] rptr,//输出到写时钟域的格雷码
input [ADDR_SIZE:0] rq2_wptr,
input rinc,
input rclk,
input rrst_n
);
reg [ADDR_SIZE:0] rbin;//二进制地址
wire [ADDR_SIZE:0] rgraynext,rbinnext;//二进制和格雷码地址
wire rempty_val;
//----------------------------
//地址逻辑
//----------------------------
always@(posedge rclk or negedge rrst_n)begin
if(!rrst_n)begin//
rbin <=0;
rptr <= 0;
end
else begin //
rbin <= rbinnext;
rptr <= rgraynext;
end
end
//地址产生逻辑
assign rbinnext = !rempty ?(rbin+rinc):rbin;
assign rgraynext = (rbinnext>>1)^(rbinnext);
assign raddr = rbin[ADDR_SIZE-1:0];
//FIFO判空
assign rempty_val = (rgraynext==rq2_wptr) ;
always@(posedge rclk or negedge rrst_n)begin
if(!rrst_n)
rempty <= 1'b1;
else begin
rempty <= rempty_val;
end
end
endmodule
判满模块
module wptr_full#(
parameter ADDR_SIZE = 4
)
(
output reg wfull,
output [ADDR_SIZE-1:0] waddr,
output reg [ADDR_SIZE:0] wptr,
input [ADDR_SIZE:0] wq2_rptr,
input winc,
input wclk,
input wrst_n
);
reg [ADDR_SIZE:0] wbin;
wire [ADDR_SIZE:0] wbinnext;
wire [ADDR_SIZE:0] wgraynext;
wire wfull_val;
always@(posedge wclk or negedge wrst_n) begin
if(!wrst_n)begin
wbin <= 0;
wptr <= 0;
end
else begin
wbin <= wbinnext;
wptr <= wgraynext;
end
end
//地址逻辑
assign wbinnext = !wfull?(wbin + winc):wbin;
assign wgraynext = (wbinnext>>1)^wbinnext;
assign waddr = wbin[ADDR_SIZE-1:0];
//判满
assign wfull_val = (wgraynext=={~wq2_rptr[ADDR_SIZE:ADDR_SIZE-1],wq2_rptr[ADDR_SIZE-2:0]});//最高两位取反,然后再判断
always@(posedge wclk or negedge wrst_n)begin
if(!wrst_n)
wfull <=0;
else begin
wfull <= wfull_val;
end
end
endmodule
测试文件
module test();
parameter DATA_SIZE = 16;
parameter ADDR_SIZE = 16;
reg [DATA_SIZE-1:0] wdata;
reg winc, wclk, wrst_n;
reg rinc, rclk, rrst_n;
wire [DATA_SIZE-1:0] rdata;
wire wfull;
wire rempty;
integer i=0;
AsyncFIFO #(
.ADDR_SIZE(ADDR_SIZE),
.DATA_SIZE(DATA_SIZE)
)
u_fifo (
.rdata(rdata),
.wfull(wfull),
.rempty(rempty),
.wdata (wdata),
.winc (winc),
.wclk (wclk),
.wrst_n(wrst_n),
.rinc(rinc),
.rclk(rclk),
.rrst_n(rrst_n)
);
localparam CYCLE = 20;
localparam CYCLE1 = 40;
//ʱÖÓÖÜÆÚ£¬µ¥Î»Îªns£¬¿ÉÔÚ´ËÐÞ¸ÄʱÖÓÖÜÆÚ¡£
//Éú³É±¾µØʱÖÓ50M
initial begin
wclk = 0;
forever
#(CYCLE/2)
wclk=~wclk;
end
initial begin
rclk = 0;
forever
#(CYCLE1/2)
rclk=~rclk;
end
//²úÉú¸´Î»ÐźÅ
initial begin
wrst_n = 1;
#2;
wrst_n = 0;
#(CYCLE*3);
wrst_n = 1;
end
initial begin
rrst_n = 1;
#2;
rrst_n = 0;
#(CYCLE*3);
rrst_n = 1;
end
always @(posedge wclk or negedge wrst_n)begin
if(wrst_n==1'b0)begin
i <= 0;
end
else if(!wfull)begin
i = i+1;
end
else begin
i <= i;
end
end
always @(rempty or rrst_n)begin
if(rrst_n==1'b0)begin
rinc = 1'b0;
end
else if(!rempty)begin
rinc = 1'b1;
end
else
rinc = 1'b0;
end
always@(wfull or wrst_n)begin
if(wrst_n)
winc = 1'b0;
if(!wfull)
winc = 1'b1;
else
winc = 1'b0;
end
always@(*)begin
if(!wfull)
wdata= i;
else
wdata = 0;
end
endmodule