FIFO介绍1
1. 什么是FIFO?
FIFO是一种先入先出的数据缓存器。优点是没有外部读写地址线,使用简单,缺点是只能顺序写入数据与读出数据。数据地址由内部指针自动加1完成
2.FIFO应用场景
1. FIFO一般用于不同时钟域之间的数据传输:设FIFO一端的AD数据采集的速率为16位 100K,每秒速度为100K×16bit=1.6Mbps,另一端计算机的PCI总线速度为33MHz,总线宽度为32bit,其最大传输速率为1056Mbps,两个不同的时钟域可以采用FIFO来做数据缓冲。
2. FIFO还可用于不同位宽的数据接口:单片机8位数据输出,DSP16位数据输入,在单片机与DSP连接时可以使用FIFO来进行数据匹配。
3.典型异步FIFO结构图2
下图为典型异步FIFO结构图,可分为几个部分:
- 双端RAM:存储输入端的数据,输出端取出数据。输入数据和输出数据位宽可以不一致,但要保证写数据、写地址位宽与读数据、读地址位宽的一致性。例如写数据位宽 8bit,写地址位宽为 6bit(64 个8bit数据)时,若输出数据位宽为 32bit,则输出地址位宽应该为 4bit(2^4 ×32)。 memory 数组定义时,以长位宽地址、短位宽数据的参数为参考,方便数组变量进行选择访问。
- 写地址与写指针:(计数器)写指针从0开始,每写一次数据写地址指针加一,指向下一个存储单元。当 FIFO 写满后,数据将不能再写入,否则数据会因覆盖而丢失。
- 读地址与读指针:(计数器)读指针指向下一个读出地址,读完自动加 1。 读写指针其实就是读写的地址,只不过这个地址不能任意选择,而是连续的。
- 写数据指针同步到读时钟域
- 读数据指针同步到写时钟域
- 写满判断:通过两级寄存器,将读指针同步到写时钟域与写指针对比,判断是否写满,实际读指针会大一些,写满非真满,会影响一定性能,但不影响功能
- 读空判断:通过两级寄存器,将写指针同步到读时钟域与读指针对比是否读空,实际写指针会大一些,读空非真空,会影响一定性能,但不影响功能
- 满空标志:在用到触发器的设计中,会遇到亚稳态的问题(亚稳态是在时钟跳变的时,寄存器采样到一个逻辑0和逻辑1参考电压的中间值。而亚稳态经过一段时间逐渐恢复成逻辑0或1,而具体会成为0还是1这件事是无法预测的。出现亚稳态的原因根源是被采样信号在时钟沿发生了跳变。一般情况下,同步时钟在保证setup和hold的情况下不会出现亚稳态(这也同步时钟不需要转格雷码的原因),而异步时钟相位关系无法设定,有可能同步前的信号正好在目标时钟沿跳变,有概率出现亚稳态3)。亚稳态的发生会使得 FIFO出现错误,读/写时钟采样的地址指针会与真实的值之间不同,这就导致写入或读出的地址错误。
a. 使用格雷码降低亚稳态概率:格雷码在相邻的两个码元之间只有一位变换(二进制码在很多情况下是很多码元在同时变化),这就会减少计数器与时钟同步的时候发生亚稳态现象。
b. 使用冗余的触发器:假设一个触发器发生亚稳态的概率为 P,那么两个及联的触发器发生亚稳态的概率就为 P的平方,但这会导致延时的增加,考虑延时的作用,空/满标志的产生并不一定出现在 FIFO真的空/满时才出现,可能FIFO还未空/满时就出现了空/满标志。
FIFO工作流程4
格雷码形式表示指针,二进制表示地址
1. 双端RAM:
输入:输入数据,输入地址,输出地址,写时钟,写时钟使能,写满标志
输出:输出数据
module fifomem
#( parameter DATASIZE = 8, // 数据位宽
parameter ADDRSIZE = 4) // 地址位宽,2^4 × 8bit
(output wire [DATASIZE - 1 : 0] rdata,
input wire [DATASIZE - 1 : 0] wdata,
input wire [ADDRSIZE - 1 : 0] waddr, raddr,
input wire wclken, wfull, wclk);
`ifdef VENDORRAM
//实例化一个双端RAM
vendor_ram mem(
.dout(rdata),
.din(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(wclken),
.clk(wclk));
`else
//RTL Verilog memory model
localparam DEPTH = 1 << ADDRSIZE;
reg[DATASIZE - 1 : 0] mem [DEPTH - 1 : 0];//根据位宽与地址定义memory
assign rdata = mem[raddr];//读出读地址的数据
always @ (posedge wclk)
if(wclken == 1'b1 && wfull == 1'b0) //未写满,写时钟使能时,将数据写入
mem[waddr] <= wdata;
`endif
endmodule
2.两级寄存机将读指针同步到写域
输入:写时钟,写复位,读指针
输出:写域的读指针
module sync_r2w
#(parameter ADDRSIZE = 4)
(output reg[ADDRSIZE : 0] wq2_rptr,
input wire[ADDRSIZE :0] rptr,
input wire wclk, wrst_n);
reg[ADDRSIZE :0] wq1_rptr;//2级寄存器
always @ (posedge wclk or negedge wrst_n)
if(wrst_n == 1'b0)
{wq2_rptr, wq1_rptr} <= 0;
else
{wq2_rptr, wq1_rptr} <= {wq1_rptr, rptr};
endmodule
3.两级寄存机将写指针同步到读域
输入:读时钟,读复位,写指针
输出:读域的写指针
module sync_w2r
#(parameter ADDRSIZE = 4)
(output reg[ADDRSIZE : 0] rq2_wptr,
input wire[ADDRSIZE :0] wptr,
input wire rclk, rrst_n);
reg[ADDRSIZE :0] rq1_wptr;
always @ (posedge rclk or negedge rrst_n)
if(rrst_n == 1'b0)
{rq2_wptr, rq1_wptr} <= 0;
else
{rq2_wptr, rq1_wptr} <= {rq1_wptr, wptr};
endmodule
4. 读空判断与读指针的移动
输入:读域的写指针,读使能,读时钟,读复位
输出:读空标志,读指针,读地址
module rptr_empty
#(parameter ADDSIZE = 4)
(output reg[ADDSIZE :0] rptr,//格雷码形式的读指针
output reg[ADDSIZE - 1 : 0] raddr,//二进制形式的读地址
output reg rempty,
input wire[ADDSIZE : 0] rq2_wptr,//同步到读域的写指针
input wire rinc, rclk, rrst_n);
//=============临时变量==================//
reg [ADDSIZE : 0] rbin;//输出的读地址
wire [ADDSIZE : 0] rgraynext, rbinnext;//移动后的读指针与读地址
wire rempty_val;//读空标志
//读指针移动:读时钟到,读指针更新(+1)
always @ (posedge rclk or negedge rrst_n)
if(rrst_n == 1'b0)
{rbin, rptr} <= 0;
else
{rbin, rptr} <= {rbinnext, rgraynext};
assign raddr = rbin[ADDSIZE - 1 : 0];
assign rbinnext = rbin + (rinc & ~rempty);//不空且有读请求的时候读指针+1
assign rgraynext = (rbinnext >> 1) ^ rbinnext;//读地址转换为格雷码->读指针
//判断是否读空:当前读指针与同步到读域的写指针完全相同
assign rempty_val = (rgraynext == rq2_wptr);
//读空赋值
always @ (posedge rclk or negedge rrst_n)
if(rrst_n == 1'b0)
rempty <= 1'b1;
else
rempty <= rempty_val;
endmodule
5. 写满判断与写指针的移动
输入:写域的读指针,写使能,写时钟,写复位
输出:写满标志,写指针,写地址
module wptr_full
#(parameter ADDRSIZE = 4)
(output reg[ADDRSIZE : 0] wptr,//格雷码形式的写指针
output wire[ADDRSIZE - 1 : 0] waddr,//二进制形式的写地址
output reg wfull,
input wire[ADDRSIZE :0] wq2_rptr,
input wire wclk, winc, wrst_n);
//=============临时变量==================//
reg [ADDRSIZE : 0] wbin;//输出的写地址
wire [ ADDRSIZE : 0] wbinnext, wgraynext};//移动后的写地址与写指针
wire full_val;//写满标志
//写指针移动:写时钟到,写指针更新(+1)
always @ (posedge wclk or negedge wrst_n)
if(wrst_n == 1'b0)
{wbin, wprt} <= 0;
else
{wbin, wprt} <= {wbinnext, wgraynext};
assign wgraynext = (wbinnext >> 1) ^ wbinnext; //写地址转换为格雷码->写指针
assign wbinnext = wbin + (winc + ~wfull); //不满且有写请求时,写地址+1;溢出后重新归零
assign waddr = wbin[ADDRSIZE - 1 : 0];//更新写地址
assign full_val = (wgraynext == {~wq2_rptr[ADDRSIZE : ADDRSIZE - 1], wq2_rptr[ADDRSIZE - 2] : 0});//最高位和次高位不同,其余位相同时,为写满
//写满赋值
always @ (posedge wclk or negedge wrst_n)
if(wrst_n == 1'b0);
wfull <= 1'b0;
else
wfull <= full_val;
endmodule
6.top层
输入:写数据;写使能,写时钟,写复位;读使能,读时钟,读复位
输出:读数据;写满标志,读空标志
module fifo
#(parameter DATASIZE = 8, //数据位宽
parameter ADDRSIZE = 4) //地址位宽
(output wire[DATASIZE - 1 : 0] rdata,
output wire wfull,
output wire rempty,
input wire[DATASIZE - 1 : 0] wdata,
input wire winc, wclk, wrst_n, //写请求、写时钟、写复位
input wire rinc, rclk, rrst_n); //读请求、读时钟、读复位
//====================临时变量=====================//
wire [ADDRSIZE - 1 : 0] waddr, raddr;//读地址,写地址
wire [ADDRSIZE : 0] wptr, rptr, wq2_rptr, rq2_wptr;//读指针,写指针,读域的写指针,写域的读指针
//=============读域与写域的时钟同步==================//
sync_r2w U_sync_r2w (.wq2_rptr(wq2_rptr), .rptr(rtpr),
.wclk(wclk), .wrst_n(wrst_n));
sync_w2r U_sync_w2r (.rq2_wptr(rq2_wptr), .wptr(wptr),
.rclk(rclk), .rrst_n(rrst_n));
//===================mem实例化====================//
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
7. testbentch
`timescale 1ns/1ns
module fifo_tb;
reg wrstn, winc, wclk;
reg rrstn, rinc, rclk;
reg [31:0] wdata ;
wire [31:0] rdata ;
wire wfull;
wire rempty;
fifo 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;
//生成本地时钟50M
initial begin
wclk = 0;
forever
#(CYCLE/2)
wclk=~wclk;
end
initial begin
rclk = 0;
forever
#(CYCLE1/2)
rclk=~rclk;
end
//reset
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
initial begin
winc=0;
#5 winc=1;
end
initial begin
rinc=0;
#5 rinc=1;
end
//data generate
always #30 wdata= $random ;
//stop sim
initial begin
forever begin
#100;
if ($time >= 5000) $finish ;
end
end
endmodule
未完待续。。。。。。
仿真结果。。。。
画个图可以清晰一点
专栏里面关于读写数据位宽与读写地址位宽不同的转换
一个PDF上看见的,不知道原文出处了。。 ↩︎
小姐姐讲的很赞,和小姐姐代码差不多https://www.bilibili.com/video/BV14A4y197Fq ↩︎