跨时钟域之异步FIFO
参考:https://www.cnblogs.com/aslmer/p/6114216.html
文章:Simulation and Synthesis Techniques for Asynchronous
Asynchronous FIFO Design
异步FIFO的读写指针
写指针
写指针指向当前将要写入数据的位置,复位之后,读写指针被置零。
执行写操作的时候,向写指针指向的存储区写入数据,之后写指针加1,指向接下来要被写入数据的位置。
On a FIFO-write operation, the memory location that is pointed to by the write pointer is written, and then the write pointer is incremented to point to the next location to be written.
读指针:
读指针指向当前要被读取数据的位置,复位时,读写指针被置零,FIFO为空读指针指向一个无效的数据(FIFO为空,empty信号有效——拉高)。当第一个有效数据被写入FIFO之后,写指针增加,empty flag信号被拉低,且读指针一直指向FIFO第一个数据的存储区域。接收逻辑没必要使用两个时钟周期读取数据,这样会使得效率很低。
FIFO空标志:
当读写指针是相等的时候:分两种情况
1.当读写指针执行复位操作的时候。
2.当读指针赶上写指针的时候,最后一笔数据从FIFO读出后FIFO为空
FIFO满标志:
读写指针相等,当FIFO里面的写指针写满一圈之后又转回到和读指针同样的位置。有个问题,读写指针相等的时候怎么判断FIFO是empty还是full?
设计的时候增加一位bit去辅助判断FIFO是空还是满。当写指针超过FIFO的最大寻址范围时,写指针将使辅助位zhi高.
FIFO满的时候:读写指针的低位(n-1位bit)相等,高位(第n位bit)不同。
FIFO空的时候,读写指针的低位和高位都相等。(针对二进制)
但是二进制FIFO指针综合电路复杂,一般采用**格雷码**,文章中采用二进制转换格雷码的方法,判断FIFO的空满标志4位二进制格雷码,有效地址位为三位。
二进制转换为格雷码的算法:rgraynext = (rbinnext>>1) ^ rbinnext;
采用格列码判断FIFO空满:
**
当最高位和次高位相同,其余位相同认为是读空
当最高位和次高位不同,其余位相同认为是写满
**
采用双寄存器同步后也不能直接判断空满
因为二进制编码判断空满的时候会有较多位电平的同时跳转,容易产生亚稳态现象,而格雷码相邻码组之间只有一位码元发生变化,故可以有效避免这个问题,此时及时产生相邻码元之间电平不跳转的现象,也不会产生危害(最多是让空标志在FIFO不是真正空的时候产生,而不会出现空读的情形)。防止亚稳态带来的危害。
采用双寄存器同步方法判断空满标志会使得电路额外消耗两个时钟周期,从而会使得空满信号的判断有所延迟,这种延迟会有危害吗?
- 读空标志的判断:现将写指针通过读时钟信号同步到读时钟域,然后将同步后的写指针同读指针比较。如果此时读时钟快,写时钟慢,同步消耗了两个时钟周期,使得同步后的读指针落后于当前的实际的读指针,从而使得满标志会提前产生,这样为FIFO满预留了一定的容限空间,只有稍微影响一下FIFO资源利用,对实际功能没有影响;如果读时钟慢,写时钟快,会造成同步的写指针不是当前时刻的写指针或者说会漏掉一部分写指针,使得FIFO判断是否空的时候,实际并不是空,同样为FIFO带来了一定的空容限。
- 写满标志的判断:现将读时钟同步到写时钟域,然后将同步后的读时钟和写时钟进行比较
1.顶层模块fifo:例化各个子模块
//顶层模块 实例化各个子模块
module fifo
#(
parameter DSIZE = 8, //读写数据位宽均设置为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.时钟域同步模块sync_r2w:读指针同步到写时钟域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
原理图
3.时钟域同步模块sync_w2r:写指针同步到读时钟域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_wptr <= wptr;
rq2_wptr <= rq1_wptr;
end
endmodule
RTL原理图
4.存储模块
//存储模块
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 将1左移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
原理图
5. rptr_empty模块:产生rempty和raddr信号
//产生empty信号和raddar信号的模块
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,//否则输出原先地址的数据waq
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'b0;
else
rempty <= rempty_val;
endmodule
RTL原理图
6.wfull和waddr信号产生的模块
//产生写满信号(wptr_full)以及写地址的逻辑部分
module wptr_full
#(
parameter ADDRSIZE = 4
)
(
output reg wfull,
output [ADDRSIZE-1:0] waddr,
output reg [ADDRSIZE :0] wptr,
//同步后的读指针,注意是多了一个位,用作判断是否fifo满(已经在rptr_empty模块将二进制读地址转化为格雷码形式,经过写时钟域同步后为wq2_rptr)
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};// wptr 被同步到sync_w2r.v读时钟域rclk
// Memory write-address pointer (okay to use binary to address memory)
assign waddr = wbin[ADDRSIZE-1:0];
assign wbinnext = wbin + (winc & ~wfull);// winc为1的时候数据才能写入对应的地址中,否则即使wdata=0,数据是没有被写入地址的!
assign wgraynext = (wbinnext>>1) ^ wbinnext; //二进制转为格雷码
//-----------------------------------------------------------------
assign wfull_val = (wgraynext=={~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]}); //当最高位和次高位不同其余位相同时则写指针超前于读指针一圈,即写满。
// assign wfull_val = ( (wgraynext[ADDRSIZE] !=wq2_rptr[ADDRSIZE])&&
// (wgraynext[ADDRSIZE-1] !=wq2_rptr[ADDRSIZE-1])&&
// (wgraynext[ADDRSIZE-2] ==wq2_rptr[ADDRSIZE-2]) );
//写满举例:假设读地址为fifo起始位置:00000(格雷码)
//当写地址由(01111)b增加1后变为(10000)b,此时要先判断存储区域是否写满,然后才会考虑是否写入数据
//转换为格雷码01000^10000=11000
//此时对比最高位和次高位可知,FIFO已满,实际上写指针和读指针此时刚好跨越一圈重合
always @(posedge wclk or negedge wrst_n)
if (!wrst_n)
wfull <= 1'b0;
else
wfull <= wfull_val;
endmodule
RTL原理图
testbench文件
`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 (写时钟50M)
initial begin
wclk = 0;
forever
#(CYCLE/2)
wclk=~wclk;
end
// 读时钟25M
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;
$display ("winc=%h", winc);
// rinc <= $random;
// $display ("winc=%h", winc);
end
end
always @(posedge rclk or negedge rrst_n)begin
if(rrst_n==1'b0)begin
rinc <= 0;
end
else begin
rinc <= $random;
$display ("rinc=%h", rinc);
end
end
always@(*)begin
if(winc == 1)
wdata= $random ;
else
wdata = 0;
end
endmodule
仿真图:
原理图: