FIFO是一种数据缓冲器,用来实现数据先入先出的读/写方式。FIFO有一个写端口和一个读端口,外部无需使用者控制地址,使用方便。FIFO与普通的Block RAM有一个很明显的区别就是使用Block RAM来做数据缓存处理,使用者必须自己控制读和写地址的管理,必须保证写的数据不把Block RAM中未被读出的内容覆盖掉从而造成数据错误,同时保证读的时候要读出未被写入的地址。而采用FIFO时,只需要关注FIFO控制器给出的空满状态信号即可知道当前有没有错误的操作了FIFO,使FIFO的数据写溢出或读空。
同步FIFO是指读时钟和写时钟为同一个时钟。
异步FIFO是指读/写时钟不一致,是相互独立的。数据是由某一个时钟写入FIFO,而由另一个时钟域的控制信号将数据读出FIFO。也就是说,读/写指针的变化动作是由不同的时钟产生的。
异步FIFO的重要参数有:时钟、数据位宽、深度、读/写指针、空满指针和RAM的大小。
(1)写时钟(wrclk):该时钟为异步FIFO写操作的工作时钟。
(2)读时钟(rdclk):该时钟为异步FIFO读操作的工作时钟。
(3)FIFO的数据位宽(bit width):就是FIFO一次读/写操作的数据位,其数据位宽根据设计需要进行定义。
(4)深度(depth):在FIFO实际工作中,其数据的空/满标志可以控制数据的继续写入或读出,故实际中用到的FIFO要根据电路的具体情况,在兼顾系统性能和FIFO的成本的情况下估算一个大概的宽度和深度就可以了。指FIFO中可以存储多少个N位的数据(假设宽度为N)。
(5)满标志(full):FIFO已满或将要满时,由FIFO的状态电路送出一个信号,从而阻止FIFO的写操作继续向FIFO中写数据造成的溢出(Overflow)。同时,使用者可根据这个信号来做出相应的处理。
(6)空标志(empty):FIFO已空或将要空时,由FIFO的状态电路送出一个信号,从而阻止FIFO的读操作继续从FIFO中读出数据而造成无效数据的读出。
(7)将满信号(almost full):
(8)将空信号(almost empty):
(9)读写指针:就是读写地址,只不过这个地址不能任意选择,而是连续的关系,其写指针减读指针得到FIFO中所含的数据个数
(10)RAM:常用的RAM包括单口RAM、简单双口RAM、真双口RAM、单口ROM、双口ROM这五种类型的RAM,可以使用简单双口RAM来做FIFO内部的RAM,也可以使用寄存器来实现FIFO的存储器。寄存器实现的方式比较合适深度和位宽较小的情况,一般在FPGA中还是推荐使用Block RAM来实现异步FIFO的存储器。
异步FIFO基本上分为6个部分,分别是写时钟域的地址管理、读时钟域的地址管理、读时钟域读地址到写时钟域的格雷码同步、写时钟域写地址到读时钟域的格雷码方式同步、写时钟域的满和将满信号的产生、读时钟域的空和将空信号的产生。
复位后,读和写地址指针均指在0地址处,同时almost empty和empty信号均有效。在写时钟域,当用户的写请求wren有效时,如果此时的写时钟域的状态正常(即还未满),则RAM写使能信号wr_ram有效,数据将会写入RAM中,同时在下一个clk原来的写地址加1,指向下一个写地址。
读时钟域侧,当用户的读请求rden有效,数据将会从RAM里被读出,同时在下一个clk原来的读地址加1,指向下一个读地址。
在写时钟域FIFO状态的判断及产生是使用了一种叫格雷码同步的方法实现多比特跨时钟域的转换、把读地址从读时钟rd_clk同步到wr_clk,然后判断写地址和读地址之间的差从而判断FIFO是否被写满。在读时钟域FIFO状态判断及产生也使用了格雷码同步的方法实现多比特跨时钟域的转换、把写地址从写时钟wr_clk到rd_clk的转换,然后判断写地址和读地址之差进而得出FIFO是否为空的结论。
FIFO本身空满信号产生的判断方式也有一定的技巧, 因为FIFO是一种回卷式的写和读, 假设FIFO的深度为1024,在T0时刻写地址第一次写的时候是从0地址开始写起,假设在T1时刻写到地址512的时候发生了读操作,在T2时刻写到1023的时候读地址指针来到了地址256,那么如果继续写的话又将出现从0地址开始写的情况,因为这时候地址0到地址255的数据已经被读走,在接下来的下一个时钟周期将会出现写地址在0的情况。
为了解决这一回卷的问题,将使用的地址宽展到11位,即wraddr[10:0]和raddr[10:0], 这是为了方便1024的判断以及处理写、读回卷问题。当读写地址相同时,即wraddr与raddr相等,此时为空;如果wraddr的最高位和raddr的最高位不同,但是其余位都相同,那就是FIFO满。
异步FIFO设计的重点:
第一点: 跨时钟域的转换及同步,因为FIFO的地址是逐1递增的,因此在做跨时钟域转换的时候可以把按照逐1递增的地址编码成每次只变化一位的格雷码,实现对数据跨时钟域直接采集。如果跨时钟域的数据每次只能改变一位,那么可以用但比特跨时钟域同步的方法直接采样,这种方式是安全的。即,将二进制码转换为格雷码,然后在目的的时钟域被采集
第二点: 状态信号的产生相对保守的方式。在做空、满等信号的时候,采用保守做法。即如果系统即将为空或者满,信号一定会有效,而且可能还会提前出现。另外,空或者满是禁止对FIFO进行操作的,这样可以保证对FIFO操作正常。
在异步FIFO的设计中,读/写的时钟是分开的,因此必须将工作于读时钟的读地址指针同步过来,并作为FIFO的满状态判断;同样,必须将工作于写时钟的写地址指针同步过来,并作为FIFO的空状态判断。由于读/写时钟是异步的,此时就涉及到一个跨时钟域问题,如果直接拿读/写地址来用会出现误判。
FIFO的读/写地址在写入或者读出数据时都是逐1递增的,即从地址0、地址1、地址2…因此可以考虑把该二进制转换成每次只跳变一个比特的格雷码,然后再另一个时钟域对该格雷码进行采样。由于同步前只有一比特的信号可能被错误采样,而一比特的错误采样只会导致一个单位地址的判断错误,不会带来FIFO状态误判的影响。
因此,在FIFO中,地址总线可以先转换为Gray码,然后用双D触发器对Gray码地址进行同步,其误码概率与单比特信号的跨时钟域转换是一致的。
Gray码相邻两个加一地址之间永远只有一个比特的变化,如果异步时钟刚好在地址跳变的时刻采样Gray编码,即便数据不稳定,但译码后错误的偏差只是1,设计时判断FIFO深度、满空等留较小的余量就可以满足要求。
Gray码可以有效的解决异步FIFO读/写地址采样不稳定的问题,提高系统的可靠性;缺点是有延迟(可提前判断来补偿),电路门数、复杂度增加。 但是,用Gray编码来解决该问题也是异步FIFO设计最常用的方法。
二进制码到格雷码的映射方式为将原二进制码左移一位,高位补零,然后与原二进制码进行异或即可。
G r a y n = B n ⊗ B n + 1 ⊗ … ( n = 0 , 1 , … , N − 2 ) . G r a y n = B n … ( n = N − 1 ) . Gray_n = B_n \otimes B_{n+1} \otimes \ldots (n = 0,1,\ldots ,N - 2). \\Gray_n = B_n \ldots (n =N - 1). Grayn=Bn⊗Bn+1⊗…(n=0,1,…,N−2).Grayn=Bn…(n=N−1).
Gray码到二进制码的解码则是一个反过程,由于数字逻辑具有这样的特性:如果a ^ b = c,那么a ^ c = b,又因为原Gray码的最高位与二进制的最高位相等,因此Gray码的次高位与Gray码的最高位异或得到二进制编码的次高位,依此可以退出余下其他位。
以FIFO深度为16的情况为例,地址Gray编/解码的Verilog代码如下:
//-----------Gray编码
//a为二进制码,b为转换后的格雷码
always@(posedge clka or posedge reset)
if(reset)
b <= 4'b0;
else
b <= a ^ (a >> 1)
//-----------异步采样
always@(posedge clkb or posedge reset)
if(reset)
c <= 4'b0;
else
c <= b;
//----------Gray译码
always@(posedge clkb or posedge reset)
if(reset)
d <= 4'b0;
else
begin
d[3] <= c[3];
d[2] <= c[3] ^ c[2];
d[1] <= c[3] ^ c[2] ^ c[1];
d[0] <= c[3] ^ c[2] ^ c[1] ^ c[0];
end
先要将a时钟的地址进行Gray编码,然后用b时钟采样Gray编码地址并暂存,最后用b时钟对暂存的Gray编码地址进行译码。不要省略b时钟采样缓存的过程。
从简单的理论上讲,FIFO的最小深度应当是一段时间内,写入数据的个数减去读出的数据后得到的FIFO中需要暂存的数据个数。在讨论这个问题之前,有一个隐含的必要条件就是FIFO写和读的吞吐量要相同;
例如:写时钟频率为100MHz,读时钟频率为200MHz,每100个写时钟写入60个数据,而每10个读时钟读出3个数据,这里可以先计算一个数据吞吐的速度,写入带宽为(60/100)X100MHz = 60MHz,读出数据带宽为(3/10)X200MHz = 60MHz,为了讨论问题的方便,我们这里假设写入、读出的数据位宽相同。
因为我们不知道100个时钟写入60个数据是怎么写入的,考虑一种最坏的情况,即背靠背的写入,即前100个时钟的后60个时钟连续写入数据,后100个时钟前60个时钟连续写入数据,那这样一次可以连续写入120个数据,那些这120个数据需要的时间是120 X (1/100 MHz) = 1200 ns,这段时间读出了多少数据呢?(1200ns X 200MHz X 3) / (1000 X 10) = 72个数据。那么FIFO中有120 - 72 = 48个数据。理论上FIFO的深度最小为48,而实际应用中又是用几乎空和几乎满来作为控制信号的依据,或者读/写时钟会有少许的相位上的差别,所以还要考虑一些裕量,也就是说在算出来的FIFO深度上加深一些,比如设置为64位。
按照上面的例子可以抽象出用于一般情况运算的公式。设写时钟的频率为fw,读时钟的频率为fr,写入数据的方式为每B个时钟写入A个数据,读数据的方式为每X个时钟读出Y个数据,计算深度有个必要条件就是“一段时间内”,写数据的个数等于读数据的个数,即吞吐量要相同,即(A/B) × fw = (Y/X) × fr。那么FIFO的深度为:
f i f o _ d e p t h = b u r s t _ l e n g t h − ( b u r s t _ l e n g t h / f w ) ⋅ [ f r ⋅ ( Y / X ) ] fifo\_depth = burst\_length - (burst\_length/f_w) \cdot [ f_r \cdot (Y/X) ] fifo_depth=burst_length−(burst_length/fw)⋅[fr⋅(Y/X)]
即写入的数据减去读出的数据。这里burst_length是连续最多可能写入的数据,要慎重选择,要考虑写入数据的方式,上面例子中burst_length = 120。同步FIFO的时候可以看做是fr = fw。
module async_fifo_16x16
(
//fifo write
input wr_clk ,
input wr_reset ,
input wr_en ,
input [DATA_WIDTH - 1:0] wr_data ,
output almost_full ,
output full ,
//fifo_read
input rd_clk ,
input rd_en ,
input rd_reset ,
output almost_empty ,
output empty ,
output [DATA_WIDTH-1:0] rd_data
);
parameter ADDR_WIDTH = 4;
parameter DATA_WIDTH = 4;
parameter ALMOST_FULL_GAP = 3;//离满还有ALMOST_FULL_GAP时,almost_full有效
parameter ALMOST_EMPTY_GAP = 3;//离空还有ALMOST_EMPTY_GAP时,almost_empty有效
parameter FIFO_DEEP = 16;
//wire declaration
wire [ADDR_WIDTH - 1:0] wr_addr ;//FIFO写地址
wire [ADDR_WIDTH - 1:0] rd_addr ;//FIFO读数据
wire [ADDR_WIDTH - 1:0] data_out_temp ;//FIFO读出数据
reg [ADDR_WIDTH:0] wr_gap ;//写指针与读指针之间的间隔
reg [ADDR_WIDTH:0] rd_gap ;//读指针与写指针之间的间隔
//register declaration
wire [DATA_WIDTH - 1:0] rd_data ;//fifo data output
reg almost_full ;//fifo almost full
reg full ;//fifo full
reg almost_empty ;//fifo almost empty
reg empty ;//fifo empty
reg [ADDR_WIDTH : 0] waddr ;//写地址,最高位为指针循环指示
reg [ADDR_WIDTH : 0] waddr_gray ;//写地址格雷码
reg [ADDR_WIDTH : 0] waddr_gray_sync_d1 ;//写地址格雷码,同步到读时针
reg [ADDR_WIDTH : 0] waddr_gray_sync ;
reg [ADDR_WIDTH : 0] raddr ;//读地址,最高位为指针循环指示位
reg [ADDR_WIDTH : 0] raddr_gray ;//读地址格雷码
reg [ADDR_WIDTH : 0] raddr_gray_sync_d1 ;//同步到写时钟的读地址格雷码
reg [ADDR_WIDTH : 0] raddr_gray_sync ;//
reg [ADDR_WIDTH : 0] raddr_gray2bin ;//读地址的二进制码
reg [ADDR_WIDTH : 0] waddr_gray2bin ;//写地址的二进制码
//此处为异步FIFO读/写地址的格雷码编码
//写控制逻辑
//RAM写使能与地址
assign wen = wr_en && (!full);//当FIFO满时,禁止写入RAM
//fifo wire address generated
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)
waddr <= {(ADDR_WIDTH + 1){1'b0}};
else if(wen)
waddr <= waddr + 1'b1;
assign wr_addr = waddr[ADDR_WIDTH - 1:0];//写地址连接到RAM存储器
//fifo write address: bin to gray
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)
waddr_gray <= {(ADDR_WIDTH + 1){1'b0}};
else
waddr_gray <= waddr ^ {1'b0,waddr[ADDR_WIDTH:1]};
//fifo read address gray sync to wr_clk
//为什么要在这里打一拍?
//同步直接赋予即可吗?读和写的速度差?
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)begin
raddr_gray_sync <= {(ADDR_WIDTH + 1){1'b0}};
raddr_gray_sync_d1 <= {(ADDR_WIDTH + 1){1'b0}};
end
else begin
raddr_gray_sync <= raddr_gray;
raddr_gray_sync_d1 <= raddr_gray_sync;
end
//读地址格雷码转变为二进制码
always@(*)begin
raddr_gray2bin = { raddr_gray_sync_d1[4],
raddr_gray_sync_d1[4]^raddr_gray_sync_d1[3],
raddr_gray_sync_d1[4]^raddr_gray_sync_d1[3]^raddr_gray_sync_d1[2],
raddr_gray_sync_d1[4]^raddr_gray_sync_d1[3]^raddr_gray_sync_d1[2]^raddr_gray_sync_d1[1],
raddr_gray_sync_d1[4]^raddr_gray_sync_d1[3]^raddr_gray_sync_d1[2]^raddr_gray_sync_d1[1]^raddr_gray_sync_d1[0]
};
end
//为什么要计算这个间隔?
//写指针与读指针间隔计算
always@(*)begin
if(raddr_gray2bin[4]^waddr[4])
wr_gap = raddr_gray2bin[3:0] - waddr[3:0];
else
wr_gap = FIFO_DEEP + raddr_gray2bin - waddr;
end
//almost_full信号产生
always@(posedge wr_clk or posedge wr_reset)begin
if(wr_reset)
almost_full <= 1'b0;
else begin
if(wr_gap < ALMOST_FULL_GAP)begin
almost_full <= 1'b1;
end
else
almost_full <= 1'b1;
end
end
//full信号产生
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)
full <= 1'b0;
else
full <= (!(|wr_gap)) || ((wr_gap == 1) & wr_en);
//异步FIFO读控制逻辑
//RAM读使能
assign ren = rd_en && (!empty);//当为empty时,读使能对内部RAM无效
//FIFO读地址产生
always@(posedge rd_clk or posedge rd_reset)
if(rd_reset)
raddr <= {(ADDR_WIDTH + 1){1'b0}};
else if(ren)
raddr <= raddr + 1;
assign rd_addr = raddr[ADDR_WIDTH - 1:0];//连接到FIFO内ARM存储器的读地址
//读地址:二进制到格雷码的转换
always@(posedge rd_clk or posedge)
if(rd_reset)
raddr_gray <= {(ADDR_WIDTH + 1){1'b0}};
else
raddr_gray <= raddr ^ {1'b0,raddr[ADDR_WIDTH:1]};
//写地址同步到读时钟
always@(posedge rd_clk or posedge rd_reset)
if(rd_reset)begin
waddr_gray_sync <= {(ADDR_WIDTH + 1){1'b0}};
waddr_gray_sync_d1 <= {(ADDR_WIDTH + 1){1'b0}};
end
else begin
waddr_gray_sync <= waddr_gray ;
waddr_gray_sync_d1 <= waddr_gray_sync;
end
//写地址格雷码转变为二进制码
always@(*)begin
waddr_gray2bin = { waddr_gray_sync_d1[4],
waddr_gray_sync_d1[4]^waddr_gray_sync_d1[3],
waddr_gray_sync_d1[4]^waddr_gray_sync_d1[3]^waddr_gray_sync_d1[2],
waddr_gray_sync_d1[4]^waddr_gray_sync_d1[3]^waddr_gray_sync_d1[2]^waddr_gray_sync_d1[1],
waddr_gray_sync_d1[4]^waddr_gray_sync_d1[3]^waddr_gray_sync_d1[2]^waddr_gray_sync_d1[1]^waddr_gray_sync_d1[0]
};
end
//读指针与写指针的间隔
always@(*)
rd_gap = wadddr_gray2bin - raddr;
//almost_empty信号产生
always@(posedge rd_clk or posedge rd_reset)begin
if(rd_reset)begin
almost_empty <= 1'b1;//重置时,fifo应该为空
end else begin
if(rd_gap < ALMOST_EMPTY_GAP)
almost_empty <= 1'b1;
else
almost_empty <= 1'b0;
end
end
//产生empty信号
always@(posedge rd_clk or posedge rd_reset)begin
if(rd_reset)
empty <= 1'b1;
else
empty <= (!(|rd_gap)) || ((rd_gap == 1) & rd_en);
end
ram_16x16 ram_16x16_u1
(
//port a
.clka ( wr_clk ),
.addra ( wr_addr ),
.dina ( wr_data ),
.wra ( wen ),
//port b0
.clkb ( rd_clk ),
.addrb ( rd_addr ),
.doutb ( rd_data ),
.rdb ( ren )
);
endmodule
这是一个完整的异步FIFO的代码,可以看到,该异步FIFO对用户的端口有wr_clk写时钟、wr_en写使能以及FIFO将满信号almost_full、满信号full,这几个信号均工作在wr_clk时钟域;在读时钟域,则有rd_clk,rd_en读使能以及FIFO将空信号almost_empty、空信号empty,这几个信号均工作在rd_clk时钟域。在该异步FIFO中,例化了一个双口存储器ram_16x16_u1,其具有写地址、写使能、写数据端口以及读地址、读使能和数据输出端口。
在这段代码中,关键点有:
//----------------Code9-2------------------------//
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)begin
raddr_gray_sync <= {(ADDR_WIDTH + 1){1'b0}};
raddr_gray_sync_d1 <= {(ADDR_WIDTH + 1){1'b0}};
end
else begin
raddr_gray_sync <= raddr_gray;
raddr_gray_sync_d1 <= raddr_gray_sync_temp;
end
此处把读时钟域读地址的格雷码同步到wr_clk时钟域里,可以看到,先对raddr_gray进行了一次同步到写时钟域的raddr_gray_sync_temp寄存器,然后打多一拍同步到raddr_gray_sync寄存器,对于rd_clk时钟域的同步也是如此。这里可以看到,格雷码和二进制码都是5位,而实际上FIFO存储器的深度只有4位,这样做是为判断FIFO空满状态做准备的,实际上写/读FIFO存储器只用了低4位。
assign wr_addr = waddr[ADDR_WIDTH - 1:0];
assign rd_addr = raddr[ADDR_WIDTH - 1:0];
always@(*)
rd_gap = waddr_gray2bin - raddr;
//算出rd_grap以后就可以产生almost_empty和empty
always@(posedge rd_clk or posedge rd_reset)begin
if(rd_reset)begin
almost_empty <= 1'b1;//when reset, the FIFO should be empty
end
else beign
if(rd_gap < ALMOST_EMPTY_GAP)
almost_empty <= 1'b1;
else
almost_empty <= 1'b0;
end
end
//generate empty signal
always@(posedge rd_clk or posedge rd_reset)
if(rd_reset)
empty <= 1'b1;
else
empty <= (!(|rd_gap)) || ((rd_gap == 1)&rd_en);
always@(*)begin
if(raddr_gray2bin[4]^waddr[4])
wr_gap = raddr_gray2bin[3:0] - waddr[3:0];
else
wr_gap = FIFO_DEEP + raddr_gray2bin - waddr;
end
always@(posedge wr_clk or posedge wr_reset)begin
if(wr_reset)
almost_full <= 1'b0;
else begin
if(wr_gap < ALMOST_FULL_GAP)begin
almost_full <= 1'b1;
end
else
almost_full <= 1'b0;
end
end
//generated full signal
always@(posedge wr_clk or posedge wr_reset)
if(wr_reset)
full <= 1'b0;
else
full <= (!(|wr_gap)) || ((wr_gap == 1)&wr_en);
对于异步FIFO来说,对写时钟和读时钟之间的速率差有没有要求?如果写时钟是读时钟的4倍,那么当写时钟的写地址格雷码被同步到读时钟的时候,此时的格雷码已经跳变了若干次了,那么会不会违反格雷码每次只跳变一次的原则?答案是不会。因为格雷码是每个周期只跳变一位,假设格雷码在读时钟域每次被同步时跟上一次同步时的差别是四个周期,但是实际上在本次同步时写时钟域的格雷码跟上一个写时钟域的格雷码只有一个比特产生了变化,因此在读时钟域即使发生了采集错误,也只是有一个比特的格雷码(跳变的那个)被采错,而不是若干个比特都采错,那么引起的结果也只是对地址正负1误差的判断而已,并不影响读时钟域的状态信号的正确产生。
在采样时刻,变化的波形最多只有一比特。因为在同一个芯片内,寄存器的建立时间和保持时间都是一致的,因此即使是寄存器的工作频率再低,其建立和保持时间都是一致的,因此即使是寄存器的工作频率再低,其建立时间和保持时间都一样,因此可以确定地说,在时钟采样的时刻最多只有一比特跳变,最多也只有一比特采错的可能性发生。