异步FIFO的设计思路及verilog代码

一:设计要点

1.结构框图

异步FIFO的设计思路及verilog代码_第1张图片
如上图所示的同步模块synchronize to write clk,其作用是把读时钟域的读指针rd_ptr采集到写时钟(wr_clk)域,然后和写指针wptr进行比较从而产生或撤消写满标志位wfull;类似地,同步模块synchronize to read clk的作用是把写时钟域的写指针wptr采集到读时钟域,然后和读指针rptr进行比较从而产生或撤消读空标志位rempty。
另外还有写指针wptr和写满标志位wfull产生模块,读指针rptr和读空标志位rempty产生模块,以及双端口存储RAM模块。

2.对读写指针的计数比较得出写满、读空

当FIFO为满或为空时,写入指针和读取指针都是相等的。但我们需要将“满”与“空”区分,当FIFO工作时,写指针在前,读指针紧跟写指针。当FIFO为满时,写指针往前移动,返回并等于后面跟随的读指针,这就是所谓的套圈。这个时候我们再增加1bit给读写指针,可以通过这个bit为0还是1来显示“满”或“空”。

2.1 二进制计数

异步FIFO读写指针需要在数学上的操作和比较才能产生准确的空满标志位,但由于读写指针属于不同的时钟域及读写时钟相位关系的不确定性,同步模块采集另一时钟域的指针时,此指针有可能正处在跳变的过程中异步FIFO的设计思路及verilog代码_第2张图片
上图中,rd_ptr2sync 3和4以及4和5之间的中间态是由于到各寄存器的时钟rd_clk存在偏差而引起的。二进制的递增操作,在大多数情况下都会有两位或者两以上的bit位在同一个递增操作内发生变化,但由于实际电路中会存在时钟偏差和不同的路径延时,二进制计数器在自增时会不可避免地产生错误的中间结果。

2.2格雷码计数

格雷码一个最大的特点就是在递增或递减的过程中,每次只变化一位,这是它最大的优点。同时它也有自己的局限性,那就是循环计数深度必须是2的n次幂,否则就失去了每次只变化一位的特性。
通过观察格雷码相邻位每次只有1位发生变化,上下两部分,除最高位相反,其余镜像对称。
异步FIFO的设计思路及verilog代码_第3张图片
7 --> 8 ,格雷码从 0100 --> 1100 ,只有最高位发生变化其余位相同
6 --> 9 , 格雷码从 0101 --> 1101 , 只有最高位发生变化其余位相同
那么进行空满判断的时候,就不是看最高位了,因为7-8的最高位不同,而其他位相同,在之前的判断中就会被判断为“满”,这就出现误判了。所以,用格雷码来判断时,还要考虑次高位。
当最高位和次高位相同,其余位相同认为是读空。
当最高位和次高位不同,其余位相同认为是写满。

2.3 跨时钟域的同步解决

读指针是属于读时钟域的,写指针是属于写时钟域的,而异步FIFO的读写时钟域不同,是异步的。如果将rclk的读指针和wclk的写指针直接比较肯定是错误的,我们需要进行同步处理进行比较。
方案格雷码+两级寄存器同步
(1)将写时钟域的写指针同步到读时钟域,将同步后的写指针与读时钟域的读指针进行比较产生读空信号
(2)将读时钟域的读指针同步到写时钟域,将同步后的读指针与写时钟域的写指针进行比较产生写满信号
两点注意
1.打两拍(两级寄存器同步):输入信号来自异步时钟域(比如FPGA芯片外部的输入),必须寄存两拍。第一拍将输入信号同步化,同步化后的输出可能带来建立/保持时间的冲突,产生亚稳态。需要再寄存一拍,减少(注意是减少)亚稳态带来的影响。
2.同步的指针和两者比较的指针都是格雷码指针!

2.4 二进制转化格雷码

二进制数 1 0 1 1 0
二进制数右移1位,空位补0 0 1 0 1 1
异或运算 1 1 1 0 1
这样就可以实现二进制到格雷码的转换了,总结就是移位并且异或,代码实现如下:
assign wgraynext = (wbinnext >> 1) ^ wbinnext
assign rgraynext = (rbinnext >> 1) ^ rbinnext

3.代码实现

6个module
1.顶层模块fifo.v

//异步FIFO 宽度8,深度16,地址4bit,多加1bit判断空满
//顶层模块 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 wclk,wrst,wr_en,
 input rclk,rrst,rd_en
);
//

wire [ASIZE-1:0] waddr,raddr;
wire [ASIZE:0] rptr,wptr,rq2_wptr,wq2_rptr;

// 

fifomem i1(
 .wdata(wdata),
 .waddr(waddr),
 .raddr(raddr),
 .wr_en(wr_en),
 .wclk(wclk),
 .wfull(wfull),
 .rdata(rdata)
);

sync_r2w i2(
 .wclk(wclk),
 .wrst(wrst),
 .rptr(rptr),
 .wq2_rptr(wq2_rptr)
);

sync_w2r i3(
 .rclk(rclk),
 .rrst(rrst),
 .rq2_wptr(rq2_wptr),
 .wptr(wptr)
);

wptr_full i4(
 .wclk(wclk),
 .wrst(wrst),
 .wr_en(wr_en),
 .waddr(waddr),
 .wfull(wfull),
 .wptr(wptr),
 .wq2_rptr(wq2_rptr)
);

rptr_empty i5(
 .rd_en(rd_en),
 .rclk(rclk),
 .rrst(rrst),
 .rq2_wptr(rq2_wptr),
 .rempty(rempty),
 .raddr(raddr),
 .rptr(rptr)
);

endmodule

2.fifomem.v生成RAM

//fifomem.v  生成存储实体
//FIFO 的本质是RAM,因此在设计存储实体的时候有两种方法:用数组存储数据或者调用RAM的IP核
module fifomem
#(parameter DATASIZE = 8,parameter ADDRSIZE = 4)
(
 input [DATASIZE-1:0] wdata,
 input [ADDRSIZE-1:0] waddr,raddr,
 input wr_en,wfull,wclk,
 output [DATASIZE-1:0] rdata
);
//
/*
`ifdef RAM //调用一个RAM IP核
my_ram mem
(
 .dout(rdata),
 .din(wdata),
 .waddr(waddr),
 .raddr(raddr),
 .wr_en(wr_en),
 .wclk(wclk),
 .wclken_n(wfull)
)
`else //数组生成存储体
localparam DEPTH=1 << ADDRSIZE;
reg [DATASIZE-1:0] mem [0:DEPTH-1];
assign rdata = mem[raddr];
always@(posedge wclk)begin    //当写使能有效且还未写满的时候将数据写入存储实体中,注意这里是与wclk同步的
 if(wr_en && !wfull) mem[waddr] <= wdata;
end

`endif
endmodule
*/

localparam DEPTH = 1 << ADDRSIZE;
reg [DATASIZE-1:0] ram [DEPTH-1:0];
//
assign rdata = ram [raddr];//读数据
//
always@(posedge wclk)begin
 if(wr_en && !wfull) ram [waddr] <= wdata;
end

endmodule

3.sync_r2w.v 将rclk中的格雷码读指针打两拍同步到wclk

//将 rclk 时钟域的格雷码形式的读指针同步到 wclk 时钟域,简单来讲就是用两级寄存器同步,即打两拍
module sync_r2w
#(parameter ADDRSIZE = 4)
(
 input [ADDRSIZE:0] rptr,//格雷码形式的读指针
 input wclk,wrst,
 output reg [ADDRSIZE:0] wq2_rptr//同步到写时钟域的打两拍的读指针
);
//
reg [ADDRSIZE:0] wq1_rptr;//打一拍的
//
always@(posedge wclk or negedge wrst)begin
 if(!wrst)begin
  wq1_rptr <= 0;
  wq2_rptr <= 0;
 end
 else begin
  wq1_rptr <= rptr;
  wq2_rptr <= wq1_rptr;
 end
end

endmodule

4.sync_w2r.v 将wclk中的格雷码写指针打两拍同步到rclk

//sync_w2r.v 将 wclk 时钟域的格雷码形式的写指针同步到 rclk 时钟域
module sync_w2r
#(parameter ADDRSIZE = 4)
(
 input [ADDRSIZE:0] wptr,//格雷码形式的写指针
 input rclk,rrst,
 output reg [ADDRSIZE:0] rq2_wptr//同步到读时钟域的打两拍的写指针
);
//
reg [ADDRSIZE:0] rq1_wptr;//打一拍的
//
always@(posedge rclk or negedge rrst)begin
 if(!rrst)begin
  rq1_wptr <= 0;
  rq2_wptr <= 0;
 end
 else begin
  rq1_wptr <= wptr;
  rq2_wptr <= rq1_wptr;
 end
end

endmodule 

5.wptr_full.v

//wptr_full.v 将  sync_r2w.v 同步后的读指针与wclk 时钟域的写指针进行比较生成写满信号
module wptr_full
#(parameter ADDRSIZE = 4)
(
 input [ADDRSIZE:0] wq2_rptr,//同步后的读指针
 input wclk,wrst,wr_en,
 output reg wfull,
 output reg [ADDRSIZE:0] wptr,//格雷码形式写指针
 output  [ADDRSIZE-1:0] waddr//二进制写指针
);
//
wire [ADDRSIZE:0] wbinnext,wgraynext;
reg [ADDRSIZE:0] wbin;
//
always@(posedge wclk or negedge wrst)begin
 if(!wrst)begin
  wbin <= 0;
  wptr <= 0;
 end
 else begin
  wbin <= wbinnext;//直接作为存储实体的地址
  wptr <= wgraynext;//输出到 sync_w2r.v模块,被同步到 rdclk 时钟域
 end
end
//
assign waddr = wbin [ADDRSIZE-1:0];
assign wbinnext = wbin + (wr_en & ~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)begin
 if(!wrst) wfull <= 1'b0;
 else wfull <= wfull_val;
end

endmodule

6.rptr_empty.v

//rptr_empty.v 将 sync_w2r.v 同步后的写指针与 rclk 时钟域的读指针进行比较生成都空信号
module rptr_empty
#(parameter ADDRSIZE = 4)
(
 input rd_en,rrst,rclk,
 input [ADDRSIZE:0] rq2_wptr,//同步后的写指针
 output reg rempty,
 output [ADDRSIZE-1:0] raddr,//二进制形式读指针
 output reg [ADDRSIZE:0] rptr//格雷码形式读指针
 
);
//
wire [ADDRSIZE:0] rbinnext,rgraynext;
reg [ADDRSIZE:0] rbin;
//将二进制的读指针与格雷码进制的读指针同步
always@(posedge rclk or negedge rrst)begin
 if(!rrst)begin
  rbin <= 0;
  rptr <= 0;
 end
 else begin
  rbin <= rbinnext;//直接作为存储实体的地址
  rptr <= rgraynext;//输出到 sync_r2w.v模块,被同步到 wrclk 时钟域
 end
end
//
assign raddr = rbin [ADDRSIZE-1:0];
assign rbinnext = rbin + (rd_en & ~rempty);//不空且有读请求的时候读指针加1 
assign rgraynext = (rbinnext >> 1) ^ rbinnext;//将二进制的读指针转为格雷码

assign rempty_val = (rgraynext == rq2_wptr);

always@(posedge rclk or negedge rrst)begin
 if(!rrst) rempty <= 1'b1;
 else rempty <= rempty_val; //rempty_val不等于1 
end

endmodule

测试代码 用$random函数生成数据

`timescale 1 ns/ 1 ns
module fifo_vlg_tst();


reg rclk;
reg rd_en;
reg rrst;
reg wclk;
reg [7:0] wdata;
reg wr_en;
reg wrst;
// wires                                               
wire [7:0]  rdata;
wire rempty;
wire wfull;

// assign statements (if any)                          
fifo m1 (
// port map - connection between master ports and signals/registers   
	.rclk(rclk),
	.rd_en(rd_en),
	.rdata(rdata),
	.rempty(rempty),
	.rrst(rrst),
	.wclk(wclk),
	.wdata(wdata),
	.wfull(wfull),
	.wr_en(wr_en),
	.wrst(wrst)
);
//
localparam CYCLE = 20;
localparam CYCLE1 = 40;

//产生时钟
initial                                                
begin                                                  
wclk = 0;
forever
#(CYCLE/2) wclk = ~wclk;
end

initial
begin
rclk = 0;
forever
#(CYCLE1/2) rclk = ~rclk;
end
//产生复位
initial
begin
wrst = 1;
#2 wrst = 0;
#(CYCLE*3) wrst = 1;
end

initial
begin
rrst = 1;
#2 rrst = 0;
#(CYCLE1*2) rrst = 1;
end
//
initial
begin
#2000 $stop;
end
                                                    
always@(posedge wclk or negedge wrst)                                                                
begin                                                  
if(!wrst)begin
 wr_en <= 0;
 rd_en <= 0;
end
else begin
 wr_en <= $random;
 rd_en <= $random;
end                                           
end

always@(posedge rclk or negedge rrst)begin
 if(!rrst)begin
  rd_en <= 0;
 end
 else begin
  rd_en <= $random;
 end
end                                                    

always@(*)begin
 if(wr_en) wdata <= $random;
 else wdata <= 0;
end


endmodule


4.仿真结果

异步FIFO的设计思路及verilog代码_第4张图片
异步FIFO的设计思路及verilog代码_第5张图片
仿真中看出我们将wdata 00001101写入[0000] [0001] [0010] [0011],11100101写入[0100] [0101],01100101写入[0110] [0111],而rdata也的确从这些地址读出了相应数据。

RTL结构图如下所示:
异步FIFO的设计思路及verilog代码_第6张图片
一篇来自EETOP很好的异步FIFO文章我也已经上传,有兴趣的可以下载看看

你可能感兴趣的:(异步FIFO的设计思路及verilog代码)