本文主要内容来自Clifford E. Cummings的
Simulation and Synthesis Techniques for Asynchronous FIFO Design
这篇文章的总结和个人理解。
一、FIFO简介
FIFO是英文First In First Out 的缩写,是一种先进先出的数据缓存器,它与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。
用途1:
异步FIFO读写分别采用相互异步的不同时钟。在现代集成电路芯片中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,多时钟域带来的一个问题就是,如何设计异步时钟之间的接口电路。异步FIFO是这个问题的一种简便、快捷的解决方案,使用异步FIFO可以在两个不同时钟系统之间快速而方便地传输实时数据。
用途2:
对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。
二、分类
同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作;
异步FIFO是指读写时钟不一致,读写时钟是互相独立的。
三、FIFO的常见参数
- FIFO的宽度:即FIFO一次读写操作的数据位;
- FIFO的深度:指的是FIFO可以存储多少个N位的数据(如果宽度为N)。
- 满标志:FIFO已满或将要满时由FIFO的状态电路送出的一个信号,以阻止FIFO的写操作继续向FIFO中写数据而造成溢出(overflow)。
- 空标志:FIFO已空或将要空时由FIFO的状态电路送出的一个信号,以阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出(underflow)。
- 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据。
- 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据。
四、FIFO设计的关键
1. 读写指针的工作原理
- 写指针:总是指向下一个将要被写入的单元,复位时,指向第1个单元(编号为0)。
- 读指针:总是指向当前要被读出的数据,复位时,指向第1个单元(编号为0)
- 循环数组:类似于C语言中用数组编写队列是
front = (front+1)%length
,在verilog中定义一个位宽为ADDRSIZE
的地址变量会自动溢出,天然就是一个循环数组
2. 产生可靠的empty/full控制信号
当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时,或者当读指针读出FIFO中最后一个字后,追赶上了写指针时,如下图所示:
当读写指针再次相等时,表明FIFO为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around)又追上了读指针,如下图:
那么读指针和写指针相等的时候究竟是空还是满呢?
为了区分到底是满状态还是空状态,软件中常用的办法如下:
- 规定
front=rear
为队列空 - 规定
front=rear+1
为队列满
但是这样相当于浪费了一个RAM地址,设计硬件本着减少面积的宗旨,则是采用给指针增加一位MSB,当写指针增加并越过最后一个FIFO地址时,就将写指针这个未用的MSB加1,其它位回零。对读指针也进行同样的操作。此时,对于深度为2n的FIFO,需要的读/写指针位宽为(n+1)位,如对于深度为8的FIFO,需要采用4bit的计数器,0000~1000、1001~1111,MSB作为折回标志位,而低3位作为地址指针。
- 如果两个指针的MSB不同,说明写指针比读指针多折回了一次;如r_addr=0000,而w_addr = 1000,为满。
- 如果两个指针的MSB相同,则说明两个指针折回的次数相等。其余位相等,说明FIFO为空;
3. 异步时钟域同步问题
异步FIFO通过比较读写地址进行满空判断,但是读写地址属于不同的时钟域,所以在比较之前需要先将读写地址进行同步处理,将写地址同步到读时钟域再和读地址比较进行FIFO空状态判断(同步后的写地址一定是小于或者等于当前的写地址,所以此时判断FIFO为空不一定是真空,这样更保守),将读地址同步到写时钟域再和写地址比较进行FIFO满状态判断(同步后的读地址一定是小于或者等于当前的读地址,所以此时判断FIFO为满不一定是真空,这样更保守),这样可以保证FIFO的特性:FIFO空之后不能继续读取,FIFO满之后不能继续写入。
大多数情形下,异步FIFO两端的时钟不是同频的,或者读快写慢,或者读慢写快,这时候进行地址同步的时候,可能会有地址遗漏,以读慢写快为例,进行满标志判断的时候需要将读地址同步到写时钟域,因为读慢写快,所以不会有读地址遗漏,同步后的读地址滞后当前读地址,所以可能满标志会提前产生。进行空标志判断的时候需要将写地址同步到读地址,因为读慢写快,所以当读时钟同步写地址的时候,必然会漏掉一部分写地址(写时钟快,写地址随写时钟翻转,直到满标志出现为止),那到底读时钟会同步到哪个写地址?不必在意是哪一个,我们关注的是漏掉的地址会不会对FIFO的空标志产生影响。比如写地址从0写到10,期间读时钟域只同步到了2,5,7这三个写地址,漏掉了其他地址。同步到7地址时,真实的写地址可能已经写到10地址,相当于“在读时钟域还没来得及觉察的情况下,写时钟域可能偷偷写了数据到FIFO去”,这样在比较读写地址的时候不会产生FIFO“空”读操作。漏掉的地址也没有对FIFO的逻辑操作产生影响。
binary编码的地址总线在跳变时极易产生毛刺,因为binary编码是多位跳变,在实现电路时不可能做到所有的地址总线等长,address bus skew必然存在,而且写地址和读地址分属不同时钟域,读写时钟完全异步,这样地址总线在进行同步过程中出错不可避免,比如写地址在从0111到1000转换时4条地址线同时跳变,这样读时钟在进行写地址同步后得到的写地址可能是0000-1111的某个值,这个完全不能确定,所以用这个同步后的写地址进行FIFO空判断的时候难免出错。
这个时候gray码体现了价值,一次只有一位数据发生变化,这样在进行地址同步的时候,只有两种情况:1.地址同步正确;2.地址同步出错,但是只有1位出错;第一种正确的情况不需要分析,我们关注第二种,假设写地址从000->001,读时钟域同步出错,写地址为000->000,也就是地址没有跳变,但是用这个错误的写地址去做空判断不会出错,最多是让空标志在FIFO不是真正空的时候产生,而不会出现空读的情形。所以gray码保证的是同步后的读写地址即使在出错的情形下依然能够保证FIFO功能的正确性,当然同步后的读写地址出错总是存在的(因为时钟异步,采样点不确定)。这里需要注意gray码只是在相邻两次跳变之间才会出现只有1位数据不一致的情形,超过两个周期则不一定,所有地址总线bus skew一定不能超过一个周期,否则可能出现gray码多位数据跳变的情况,这个时候gray码就失去了作用,因为这时候同步后的地址已经不能保证只有1位跳变了。
4. gray码编址情况下的empty/full如何产生
使用gray码解决了一个问题,但同时也带来另一个问题,即在格雷码域如何判断空与满。
- 对于“空”的判断依然依据二者完全相等(包括MSB);
而对于“满”的判断,如下图,由于gray码除了MSB外,具有镜像对称的特点,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,不能说它为满。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:
wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。
- wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。
- 剩下的其余位完全相等。
五、实现
top
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,
input rinc, rclk, rrst
);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wrptr2, rwptr2;
sync_r2w #(ASIZE) U1(
.wrptr2(wrptr2),
.rptr(rptr),
.wclk(wclk),
.wrst(wrst)
);
sync_w2r #(ASIZE) U2(
.rwptr2(rwptr2),
.wptr(wptr),
.rclk(rclk),
.rrst(rrst)
);
fifomem #(DSIZE, ASIZE) U3(
.rdata(rdata),
.wdata(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(winc),
.wclk(wclk)
);
rptr_empty #(ASIZE) U4(
.rempty(rempty),
.raddr(raddr),
.rptr(rptr),
.rwptr2(rwptr2),
.rinc(rinc),
.rclk(rclk),
.rrst(rrst)
);
wptr_full #(ASIZE) U5(
.wfull(wfull),
.waddr(waddr),
.wptr(wptr),
.wrptr2(wrptr2),
.winc(winc),
.wclk(wclk),
.wrst(wrst)
);
endmodule
empty/full generation
module rptr_empty #(
parameter ADDRSIZE = 4
)(
output [ADDRSIZE-1:0] raddr,
output reg [ADDRSIZE:0] rptr,
output reg rempty,
input [ADDRSIZE:0] rwptr2,
input rinc, rclk, rrst
);
reg [ADDRSIZE:0] rbin, rgnext, rbnext;
// Grey code pointer
always @(posedge rclk or posedge rrst) begin
if (rrst) begin
rptr <= 0;
rbin <= 0;
end
else begin
rptr <= rgnext;
rbin <= rbnext;
end
end
always @(*) begin
rbnext = (!rempty)? (rbin + rinc) : rbin;
rgnext = (rbnext>>1)^rbnext; // binary to gray
end
// Memory read-address pointer
assign raddr = rbin[ADDRSIZE-1:0];
// FIFO empty on reset or when the next rptr == synchronized wptr
always @(posedge rclk or posedge rrst) begin
if (rrst) rempty <= 1'b1;
else rempty <= (rgnext == rwptr2);
end
endmodule
module wptr_full #(
parameter ADDRSIZE = 4
)(
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE:0] wptr,
output reg wfull,
input [ADDRSIZE:0] wrptr2,
input winc, wclk, wrst
);
reg [ADDRSIZE:0] wbin, wgnext, wbnext;
// Grey code pointer
always @(posedge wclk or posedge wrst) begin
if (wrst) begin
wptr <= 0;
wbin <= 0;
end
else begin
wptr <= wgnext;
wbin <= wbnext;
end
end
always @(*) begin
wbnext = (!wfull)? (wbin + winc) : wbin;
wgnext = (wbnext>>1)^wbnext;
end
// Memory read-address pointer
assign waddr = wbin[ADDRSIZE-1:0];
// FIFO full generation
always @(posedge wclk or posedge wrst) begin
if (wrst) wfull <= 1'b0;
else wfull <= ((wgnext[ADDRSIZE] != wrptr2[ADDRSIZE] ) &&
(wgnext[ADDRSIZE-1] != wrptr2[ADDRSIZE-1]) &&
(wgnext[ADDRSIZE-2:0] == wrptr2[ADDRSIZE-2:0]));
end
endmodule
sync
module sync_r2w #(
parameter ADDRSIZE = 4
)(
output reg [ADDRSIZE:0] wrptr2,
input [ADDRSIZE:0] rptr,
input wclk, wrst
);
reg [ADDRSIZE:0] wrptr1;
always @(posedge wclk or posedge wrst) begin
if (wrst) begin
{wrptr2, wrptr1} <= 0;
end
else begin
{wrptr2, wrptr1} <= {wrptr1, rptr};
end
end
endmodule
module sync_w2r #(
parameter ADDRSIZE = 4
)(
output reg [ADDRSIZE:0] rwptr2,
input [ADDRSIZE:0] wptr,
input rclk, rrst
);
reg [ADDRSIZE:0] rwptr1;
always @(posedge rclk or posedge rrst) begin
if (rrst) begin
{rwptr2, rwptr1} <= 0;
end
else begin
{rwptr2, rwptr1} <= {rwptr1, wptr};
end
end
endmodule
testbench
首先是一个行为级的FIFO,二进制编码
module beh_fifo #(
parameter DSIZE = 8,
parameter ASIZE = 4
)(
output [DSIZE-1:0] rdata,
output wfull,
output rempty,
input [DSIZE-1:0] wdata,
input winc, wclk, wrst,
input rinc, rclk, rrst
);
localparam MEMDEPTH = 1<<ASIZE;
reg [DSIZE-1:0] ex_mem [0:MEMDEPTH-1];
reg [ASIZE:0] wptr, wrptr1, wrptr2, wrptr3;
reg [ASIZE:0] rptr, rwptr1, rwptr2, rwptr3;
always @(posedge wclk or posedge wrst) begin
if (wrst) begin
wptr <= 0;
end
else if (winc && !wfull) begin
ex_mem[wptr[ASIZE-1:0]] <= wdata;
wptr <= wptr+1;
end
end
always @(posedge wclk or posedge wrst) begin
if (wrst) begin
{wrptr3, wrptr2, wrptr1} <= 0;
end
else begin
{wrptr3, wrptr2, wrptr1} <= {wrptr2, wrptr1, rptr};
end
end
always @(posedge rclk or posedge rrst) begin
if (rrst) begin
rptr <= 0;
end
else if (rinc && !rempty) begin
rptr <= rptr+1;
end
end
always @(posedge rclk or posedge rrst) begin
if (rrst) begin
{rwptr3, rwptr2, rwptr1} <= 0;
end
else begin
{rwptr3, rwptr2, rwptr1} <= {rwptr2, rwptr1, wptr};
end
end
assign rdata = ex_mem[rptr[ASIZE-1:0]];
assign rempty = (rptr == rwptr3);
assign wfull = ((wptr[ASIZE-1:0] == wrptr3[ASIZE-1:0]) &&
(wptr[ASIZE] != wrptr3[ASIZE]));
endmodule
`timescale 1ns/100ps
module fifo_tb;
parameter DSIZE = 8;
parameter ASIZE = 4;
wire [DSIZE-1:0] rdata, beh_rdata;
wire wfull, beh_wfull;
wire rempty, beh_rempty;
reg [DSIZE-1:0] wdata;
reg winc, wclk, wrst;
reg rinc, rclk, rrst;
fifo1 #(DSIZE, ASIZE) U1(
.rdata(rdata),
.wfull(wfull),
.rempty(rempty),
.wdata(wdata),
.winc(winc),
.wclk(wclk),
.wrst(wrst),
.rinc(rinc),
.rclk(rclk),
.rrst(rrst)
);
beh_fifo #(DSIZE, ASIZE) U2(
.rdata(beh_rdata),
.wfull(beh_wfull),
.rempty(beh_rempty),
.wdata(wdata),
.winc(winc),
.wclk(wclk),
.wrst(wrst),
.rinc(rinc),
.rclk(rclk),
.rrst(rrst)
);
always #30 wclk = ~wclk;
always #20 rclk = ~rclk;
always #30 wdata = {$random}%256;
initial begin
wrst = 0;
rrst = 0;
rclk = 0;
wclk = 0;
winc = 0;
rinc = 0;
#50 wrst = 1;
rrst = 1;
#50 wrst = 0;
rrst = 0;
#10 rinc = 1;
#100 rinc = 0;
#100 winc = 1;
#1000 winc = 0;
#100 rinc = 1;
#2000 $finish;
end
always @((rdata != beh_rdata ) &&
(wfull != beh_wfull ) &&
(rempty != beh_rempty)) begin
$display($time, "rdata is %h, beh_rdata is %h", rdata, beh_rdata);
end
endmodule