一、FIFO综述
1.FIFO的本质是RAM, 数据先进先出。
2.重要参数:fifo深度(数据量),fifo位宽(数据位宽)。
3.FIFO有同步和异步两种,同步即读写时钟相同,异步即读写时钟不相同,同步FIFO用的少,可以作为数据缓存,异步FIFO可以解决跨时钟域的问题,在应用时需根据实际情况考虑好fifo深度即可。
二、设计要点解析
1、读空信号如何产生?写满信号如何产生?
读空信号:复位的时候,读指针和写指针相等,读空信号有效(这里所说的指针其实就是读地址、写地址),当读指针赶上写指针的时候,写指针等于读指针意味着最后一个数据被读完,此时读空信号有效
写满信号:当写指针比读指针多一圈时,写指针等于读指针意味着写满了,此时写满信号有效。
解决方法:将指针的位宽多定义一位,假设要设计深度为 8 的异步FIFO,此时定义读写指针只需要 3 位(2^3=8)就够用了,但是我们在设计时将指针的位宽设计成 4 位,最高位的作用就是区分是读空还是写满。
二进制指针:
当最高位相同,其余位相同认为是读空;当最高位不同,其余位相同认为是写满
格雷码指针:
当最高位和次高位相同,其余位相同认为是读空;当最高位和次高位不同,其余位相同认为是写满
注:在实际设计中如果不想用格雷码比较,就可以利用格雷码将读写地址同步到一个时钟域后再将格雷码再次转化成二进制数再用理论1进行比较就好了。
2、由于是异步FIFO的设计,读写时钟不一样,在产生读空信号和写满信号时,会涉及到跨时钟域的问题,如何解决?
跨时钟域的问题:上面我们已经提到要通过比较读写指针来判断产生读空和写满信号,但是读指针是属于读时钟域的,写指针是属于写时钟域的,而异步FIFO的读写时钟域不同,是异步的,要是将读时钟域的读指针与写时钟域的写指针不做任何处理直接比较肯定是错误的,因此我们需要进行同步处理以后仔进行比较
解决方法:两级寄存器同步 + 格雷码
同步的过程:
(1)将写时钟域的写指针同步到读时钟域,将同步后的写指针与读时钟域的读指针进行比较产生读空信号
(2)将读时钟域的读指针同步到写时钟域,将同步后的读指针与写时钟域的写指针进行比较产生写满信号
同步的思想:两级寄存器同步。
格雷码:我们如果直接用二进制编码的读写指针去完成上述的两种同步是不行的,而使用格雷码指针更合适,格雷码的编码特点是相邻位每次只有 1 位发生变化,保证是同步后的读写指针即使在出错的情形下依然能够保证FIFO功能的正确性。
3、由于设计的时候读写指针用了至少两级寄存器同步,同步会消耗至少两个时钟周期,势必会使得判断空或满有所延迟,这会不会导致设计出错呢?
异步逻辑转到同步逻辑不可避免需要额外的时钟开销,这会导致满空趋于保守,但是保守并不等于错误,这么写会稍微有性能损失,但是不会出错。
4、多位二进制码如何转化为格雷码
二进制码转换成二进制格雷码,其法则是保留二进制码的最高位作为格雷码的最高位,而次高位格雷码为二进制码的高位与次高位相异或,而格雷码其余各位与次高位的求法相类似。
举例:
(1)二进制数: 1 0 1 1 0
(2)二进制数右移1位,空位补0 : 0 1 0 1 1
(3)异或运算 : 1 1 1 0 1
(4)verilog代码实现:assign wgraynext = (wbinnext>>1) ^ wbinnext;.
三、代码
异步FIFO的信号接口:
wclk wrst_n winc wdata //写时钟、写复位、写请求、写数据 与wclk同步
rclk rrst_n rinc rdata //读时钟、读 复位、读 请求、读 数据 与rclk同步
wfull //写满 与wclk同步
rempty // 读空 与rclk同步
1、fifo.v 是顶层模块,作用是将各个小模块例化联系起来
module 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_n,
input rinc, rclk, rrst_n
);
wire [ASIZE-1:0] waddr, raddr;
wire [ASIZE:0] wptr, rptr, wq2_rptr, rq2_wptr;
// synchronize the read pointer into the write-clock domain
sync_r2w sync_r2w
(
.wq2_rptr (wq2_rptr),
.rptr (rptr ),
.wclk (wclk ),
.wrst_n (wrst_n )
);
// synchronize the write pointer into the read-clock domain
sync_w2r sync_w2r
(
.rq2_wptr(rq2_wptr),
.wptr(wptr),
.rclk(rclk),
.rrst_n(rrst_n)
);
//this is the FIFO memory buffer that is accessed by both the write and read clock domains.
//This buffer is most likely an instantiated, synchronous dual-port RAM.
//Other memory styles can be adapted to function as the FIFO buffer.
fifomem
#(DSIZE, ASIZE)
fifomem
(
.rdata(rdata),
.wdata(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(winc),
.wfull(wfull),
.wclk(wclk)
);
//this module is completely synchronous to the read-clock domain and contains the FIFO read pointer and empty-flag logic.
rptr_empty
#(ASIZE)
rptr_empty
(
.rempty(rempty),
.raddr(raddr),
.rptr(rptr),
.rq2_wptr(rq2_wptr),
.rinc(rinc),
.rclk(rclk),
.rrst_n(rrst_n)
);
//this module is completely synchronous to the write-clock domain and contains the FIFO write pointer and full-flag logic
wptr_full
#(ASIZE)
wptr_full
(
.wfull(wfull),
.waddr(waddr),
.wptr(wptr),
.wq2_rptr(wq2_rptr),
.winc(winc),
.wclk(wclk),
.wrst_n(wrst_n)
);
endmodule
2、fifomem.v 生成存储实体,有两种方法:用数组存储数据或者调用RAM的IP核
module fifomem
#(
parameter DATASIZE = 8, // Memory data word width
parameter ADDRSIZE = 4 // 深度为8即地址为3位即可,这里多定义一位的原因是用来判断是空还是满,详细在后文讲到
) // Number of mem address bits
(
output [DATASIZE-1:0] rdata,
input [DATASIZE-1:0] wdata,
input [ADDRSIZE-1:0] waddr, raddr,
input wclken, wfull, wclk
);
`ifdef RAM //可以调用一个RAM IP核
// instantiation of a vendor's dual-port RAM
my_ram mem
(
.dout(rdata),
.din(wdata),
.waddr(waddr),
.raddr(raddr),
.wclken(wclken),
.wclken_n(wfull),
.clk(wclk)
);
`else //用数组生成存储体
// RTL Verilog memory model
localparam DEPTH = 1<<ADDRSIZE; // 左移相当于乘法,2^4
reg [DATASIZE-1:0] mem [0:DEPTH-1]; //生成2^4个位宽位8的数组
assign rdata = mem[raddr];
always @(posedge wclk) //当写使能有效且还未写满的时候将数据写入存储实体中,注意这里是与wclk同步的
if (wclken && !wfull)
mem[waddr] <= wdata;
`endif
endmodule
3、sync_r2w.v 将 rclk 时钟域的格雷码形式的读指针同步到 wclk 时钟域,简单来讲就是用两级寄存器同步,即打两拍。
module sync_r2w
#(
parameter ADDRSIZE = 4
)
(
output reg [ADDRSIZE:0] wq2_rptr, //读指针同步到写时钟域
input [ADDRSIZE:0] rptr, // 格雷码形式的读指针,格雷码的好处后面会细说
input wclk, wrst_n
);
reg [ADDRSIZE:0] wq1_rptr;
always @(posedge wclk or negedge wrst_n)
if (!wrst_n) begin
wq1_rptr <= 0;
wq2_rptr <= 0;
end
else begin
wq1_rptr<= rptr;
wq2_rptr<=wq1_rptr;
end
endmodule
4、sync_w2r.v 将 wclk 时钟域的格雷码形式的写指针同步到 rclk 时钟域
module sync_w2r
#(parameter ADDRSIZE = 4)
(
output reg [ADDRSIZE:0] rq2_wptr, //写指针同步到读时钟域
input [ADDRSIZE:0] wptr, //格雷码形式的写指针
input rclk, rrst_n
);
reg [ADDRSIZE:0] rq1_wptr;
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)begin
rq1_wptr <= 0;
rq2_wptr <= 0;
end
else begin
rq1_wpt <= wptr;
rq2_wptr <= rq1_wptr;
end
endmodule
5、rptr_empty.v 将 sync_w2r.v 同步后的写指针与 rclk 时钟域的读指针进行比较生成都空信号
module rptr_empty
#(
parameter ADDRSIZE = 4
)
(
output reg rempty,
output [ADDRSIZE-1:0] raddr, //二进制形式的读指针
output reg [ADDRSIZE :0] rptr, //格雷码形式的读指针
input [ADDRSIZE :0] rq2_wptr, //同步后的写指针
input rinc, rclk, rrst_n
);
reg [ADDRSIZE:0] rbin;
wire [ADDRSIZE:0] rgraynext, rbinnext;
// GRAYSTYLE2 pointer
//将二进制的读指针与格雷码进制的读指针同步
always @(posedge rclk or negedge rrst_n)
if (!rrst_n) begin
rbin <= 0;
rptr <= 0;
end
else begin
rbin<=rbinnext; //直接作为存储实体的地址
rptr<=rgraynext;//输出到 sync_r2w.v模块,被同步到 wrclk 时钟域
end
// Memory read-address pointer (okay to use binary to address memory)
assign raddr = rbin[ADDRSIZE-1:0]; //直接作为存储实体的地址,比如连接到RAM存储实体的读地址端。
assign rbinnext = rbin + (rinc & ~rempty); //不空且有读请求的时候读指针加1
assign rgraynext = (rbinnext>>1) ^ rbinnext; //将二进制的读指针转为格雷码
// FIFO empty when the next rptr == synchronized wptr or on reset
assign rempty_val = (rgraynext == rq2_wptr); //当读指针等于同步后的写指针,则为空。
always @(posedge rclk or negedge rrst_n)
if (!rrst_n)
rempty <= 1'b1;
else
rempty <= rempty_val;
endmodule
6、wptr_full.v 将 sync_r2w.v 同步后的读指针与wclk 时钟域的写指针进行比较生成写满信号
module wptr_full
#(
parameter ADDRSIZE = 4
)
(
output reg wfull,
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE :0] wptr,
input [ADDRSIZE :0] wq2_rptr,
input winc, wclk, wrst_n
);
reg [ADDRSIZE:0] wbin;
wire [ADDRSIZE:0] wgraynext, wbinnext;
// GRAYSTYLE2 pointer
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
{wbin, wptr} <= 0;
else
{wbin, wptr} <= {wbinnext, wgraynext};
// Memory write-address pointer (okay to use binary to address memory)
assign waddr = wbin[ADDRSIZE-1:0];
assign wbinnext = wbin + (winc & ~wfull);
assign wgraynext = (wbinnext>>1) ^ wbinnext; //二进制转为格雷码
//-----------------------------------------------------------------
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]}); //当最高位和次高位不同其余位相同时则写指针超前于读指针一圈,即写满。后面会详细解释。
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
wfull <= 1'b0;
else
wfull <= wfull_val;
endmodule
7、测试文件
`timescale 1ns /1ns
module test();
reg [7:0] wdata;
reg winc, wclk, wrst_n;
reg rinc, rclk, rrst_n;
wire [7: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;
//时钟周期,单位为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
winc <= 0;
rinc <= 0;
end
else begin
winc <= $random;
rinc <= $random;
end
end
always @(posedge rclk or negedge rrst_n)begin
if(rrst_n==1'b0)begin
rinc <= 0;
end
else begin
rinc <= $random;
end
end
always@(*)begin
if(winc == 1)
wdata= $random ;
else
wdata = 0;
end
endmodule