FIFO( First Input First Output)简单说就是指先进先出,也是缓存机制的一种,下面是我总结的 FIFO 的三大用途:
1)提高传输效率,增加 DDR 带宽的利用率。比如我们有 4 路视频数据缓存到 DDR 中去,比较笨的方法是,每个通道视频数据对应一颗 DDR。现在对于 DDR 来说非常浪费,因为现在的 DDR3 可以跑 1600Mbps DDR4 可以跑到2400Mbps,如果你还是把一路视频数据对应一颗 DDR 显然严重浪费了带宽。加入 FIFO 后,只要把 4 路数据先缓存进入 DDR,在缓存的过程中,快速得把数据从 FIFO 取出并且写入到 DDR 中,只要 FIFO 没有满就不会出现数据丢失。现在我们带宽够用,FIFO 给的足够大就可以确保数据不丢失。
2)数据位宽转换,比如我们有 32bit 的数据需要转换成 128bit 或者 32bit 的数据需要转换成 8bit,那么用 FIFO 来转换也是非常方便的。
3)跨时钟域的应用,比如数据是 2 个不同步的时钟,那么我们就可以用 FIFO 实现跨时钟域的传输。
以上总计的三点,很多时候是混合使用的。
FIFO的重点和难点是空满状态的判断。
同步FIFO
同步FIFO是指读写数据使用的是同一个时钟,所以不用进行跨时钟域处理。有两种设计方法:高位扩展法和计数器法
本程序设置了统计FIFO内部数据数量的计数器cnt
,并根据计数器的大小判断空满。设FIFO的深度是DEPTH
,如果cnt==0
,说明FIFO内没有数据;如果cnt==DEPTH
,说明FIFO已存满。
计数器根据读写信号自增或者自减。当读写同时进行时,计数器数值不变;当有效写入时计数器减1;当有效读取时,计数器加1。
`timescale 1ns/1ns
/**********************************RAM************************************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
/**********************************SFIFO************************************/
module sfifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input clk ,
input rst_n ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output reg wfull ,
output reg rempty ,
output wire [WIDTH-1:0] rdata
);
reg [$clog2(DEPTH)-1:0] waddr, raddr;
reg [$clog2(DEPTH) :0] cnt;
always@(posedge clk or negedge rst_n) begin
if(~rst_n)
waddr <= 0;
else
waddr <= winc&~wfull? waddr+1: waddr;
end
always@(posedge clk or negedge rst_n) begin
if(~rst_n)
raddr <= 0;
else
raddr <= rinc&~rempty? raddr+1:raddr;
end
always@(posedge clk or negedge rst_n) begin
if(~rst_n)
cnt <= 0;
else if(rinc&~rempty&winc&~wfull)
cnt <= cnt;
else if(winc&~wfull)
cnt <= cnt + 1;
else if(rinc&~rempty)
cnt <= cnt - 1;
else
cnt <= cnt;
end
always@(posedge clk or negedge rst_n) begin
if(~rst_n) begin
wfull = 0;
rempty = 0;
end
else begin
wfull = cnt == DEPTH;
rempty = cnt == 0;
end
end
dual_port_RAM #(
.DEPTH(DEPTH ),
.WIDTH(WIDTH )
)
myRAM(
.wclk (clk ),
.wenc (winc&~wfull ),
.waddr(waddr ),
.wdata(wdata ),
.rclk (clk ),
.renc (rinc&~rempty),
.raddr(raddr ),
.rdata(rdata )
);
endmodule
异步FIFO
异步FIFO的与同步FIFO的核心区别是它的读时钟和写时钟是不同步的。所以用对比读写地址的方法产生空满信号时,要进行跨时钟域处理。为了降低亚稳态可能性,异步FIFO还引入了格雷码。同时,格雷码也更方便产生空满信号。
二进制 | 写地址 | 读地址 | 格雷码 | 写地址 | 读地址 |
---|---|---|---|---|---|
空FIFO | 0 0000 | 0 0000 | 0 0000 | 0 0000 | |
写满 | 1 0000 | 0 0000 | 1 1000 | 0 0000 | |
读空 | 1 0000 | 1 0000 | 1 1000 | 1 1000 | |
写满 | 0 0000 | 1 0000 | 0 0000 | 1 1000 | |
读空 | 0 0000 | 0 0000 | 0 0000 | 0 0000 |
FIFO深度为16时,地址位宽位5,当最高位和次高位不相同,其余位相同认为是写满;当所有位相同认为是读空。
异步FIFO主要包含四部分:读写地址发生器、格雷码的产生与打拍、空满信号发生器以及RAM。
`timescale 1ns/1ns
/***************************************RAM*****************************************/
module dual_port_RAM #(parameter DEPTH = 16,
parameter WIDTH = 8)(
input wclk
,input wenc
,input [$clog2(DEPTH)-1:0] waddr //深度对2取对数,得到地址的位宽。
,input [WIDTH-1:0] wdata //数据写入
,input rclk
,input renc
,input [$clog2(DEPTH)-1:0] raddr //深度对2取对数,得到地址的位宽。
,output reg [WIDTH-1:0] rdata //数据输出
);
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];
always @(posedge wclk) begin
if(wenc)
RAM_MEM[waddr] <= wdata;
end
always @(posedge rclk) begin
if(renc)
rdata <= RAM_MEM[raddr];
end
endmodule
/***************************************AFIFO*****************************************/
module asyn_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 16
)(
input wclk ,
input rclk ,
input wrstn ,
input rrstn ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output wire wfull ,
output wire rempty ,
output wire [WIDTH-1:0] rdata
);
parameter addr_width = $clog2(DEPTH);
//写指针--二进制
reg [addr_width:0]wptr_bin,rptr_bin;
always@(posedge wclk or negedge wrstn)
begin
if(!wrstn) wptr_bin <= 0;
else if(winc && !wfull) wptr_bin <= wptr_bin +1;
else ;
end
//读指针--二进制
always@(posedge rclk or negedge rrstn)
begin
if(!rrstn) rptr_bin <= 0;
else if(rinc && !rempty) rptr_bin <= rptr_bin +1;
else ;
end
// 指针二进制转格雷码
wire [addr_width:0]wptr_gray,rptr_gray;
assign wptr_gray = wptr_bin ^ wptr_bin>>1;
assign rptr_gray = rptr_bin ^ rptr_bin>>1;
//
reg [addr_width:0]wptr,rptr;
always @(posedge wclk or negedge wrstn)
begin
if(!wrstn) wptr <= 0;
else wptr <= wptr_gray;
end
//
always @(posedge rclk or negedge rrstn)
begin
if(!rrstn) rptr <= 0;
else rptr <= rptr_gray;
end
// 经两级锁存器进行时钟同步
reg [addr_width:0]sync_r2w,rptr_temp,sync_w2r,wptr_temp;
// 写时针同步
always @(posedge wclk or negedge wrstn)
begin
if(!wrstn)
begin
sync_r2w <= 0 ;
rptr_temp <= 0;
end
else
begin
rptr_temp <= rptr;
sync_r2w <= rptr_temp;
end
end
// 读时针同步
always @(posedge rclk or negedge rrstn)
begin
if(!rrstn)
begin
sync_w2r <= 0 ;
wptr_temp <= 0;
end
else
begin
wptr_temp <= wptr;
sync_w2r <= wptr_temp;
end
end
// 判断写满读空状态
assign wfull = wptr == {~sync_r2w[addr_width:addr_width-1],sync_r2w[addr_width-2:0]};
assign rempty = rptr == sync_w2r;
// 读数据(调用ram)
dual_port_RAM #(
.DEPTH(DEPTH),
.WIDTH(WIDTH)
)
dual_port_RAM
(
.wclk(wclk),
.wenc(winc && !wfull),
.waddr(wptr_bin[addr_width-1:0]),
.wdata(wdata),
.rclk(rclk),
.renc(rinc && !rempty),
.raddr(rptr_bin[addr_width-1:0]),
.rdata(rdata)
);
endmodule
补充
- 空和满时,读写指针末尾不一定全是0哦。换句话说,fifo的工作过程不一定是:先写满再读空再写满再读空这样的,也可能是边读边写,甚至可能同时读写。因此假设读指针为10011,写指针为00011(二进制),这也是fifo满。
- 亚稳态是在时钟跳变时,寄存器采样到一个逻辑0和逻辑1参考电压的中间值,这是亚稳态的概念。而亚稳态经过一段时间逐渐恢复成逻辑0或1,而具体会成为0还是1这件事是无法预测的。说回来,出现亚稳态的原因根源是被采样信号在时钟沿发生了跳变。一般情况下,同步时钟在保证setup和hold的情况下不会出现亚稳态(这也同步时钟不需要转格雷码的原因),而异步时钟相位关系无法设定,有可能同步前的信号正好在目标时钟沿跳变,有概率出现亚稳态,使用格雷码降低这种概率。一旦格雷码在跳变时也出现亚稳态,因为亚稳态最终也会恢复成逻辑0或1嘛,所以亚稳态后的格雷码相比于跳变前也可能会出现两种情况:正常跳变或者没有跳变。对于正常跳变,当然不会对结果产生任何影响;对于非正常跳变也就是格雷码没有跳变,会使被同步的指针更加保守,而可能加剧假空或者假满的程度,但不会造成功能错误,这也是选择用格雷码跨时钟的重要原因。