异步fifo读空、写满分析(附完整Verilog代码)
一、fifo介绍
FIFO 是 First-In, First-Out 的缩写,即“先进先出”,是一种常见的数据结构,类似于排队服务。在 FIFO 结构中,最先进入的数据项首先被取出,后进入的数据项则会在队列中等待。
FIFO 在计算机科学和电子工程中被广泛使用,用于数据存储和传输。以下是 FIFO 的一些特点和应用领域:
-
特点:
- 数据按照先进先出的顺序进行存储和检索。
- 支持两个基本操作:入队(enqueue)将数据放入队列尾部,出队(dequeue)从队列头部取出数据。
- FIFO 可以用数组或链表等数据结构来实现。
-
应用领域:
- 数据缓冲:FIFO 用于临时存储数据,例如在数据传输中平衡发送者和接收者之间的速度差异。
- 任务调度:在操作系统中,FIFO 可用于调度任务执行顺序,确保按照提交顺序执行任务。
- 硬件设计:在电子工程中,FIFO 经常用于在不同速度的系统之间传输数据,如存储器和外设之间的数据传输。
-
实际应用:
- 计算机网络中的数据包传输;
- 操作系统中的进程调度;
- 缓存控制器中的数据暂存等。
总的来说,FIFO 是一种简单而有效的数据结构,可用于多种应用场景,通过保持数据的顺序性,确保了数据的正确性和完整性。
二、fifo的读空和写满
在使用 FIFO(First-In, First-Out)队列时,有两种常见的情况需要特别注意,即"读空"和"写满":
读空(Read Empty):读空表示在尝试从 FIFO 队列中读取数据时,队列已经为空的状态。这意味着没有可用的数据项供读取,如果此时再尝试读取数据,将导致读取操作失败或者返回空值。处理读空状态的方法通常包括等待新数据到达(阻塞式读取)、返回特定值(如错误代码)或者执行其他适当的错误处理逻辑。
写满(Write Full):写满表示在尝试向 FIFO 队列中写入数据时,队列已经达到了最大容量,无法再写入新数据的状态。如果继续尝试写入数据,可能导致写入操作失败或者丢失数据。处理写满状态的方法通常包括等待队列中的数据被消费(阻塞式写入)、返回特定值(如错误代码)或者执行其他适当的错误处理逻辑。
理解读空和写满的概念对于正确地处理 FIFO 队列非常重要。在实际应用中,可以通过监视读取和写入操作的返回状态(如返回值或错误码)来判断队列是否处于读空或写满状态,并根据情况采取相应的处理措施。合理处理读空和写满状态有助于确保数据的完整性和系统的稳定性。
同步fifo的读空和写满
对于同步fifo来说,读和写工作在一个时钟域,可以通过一个计数器来判断空满。
当同步FIFO写入一次数据,计数器加1。
当同步FIFO读出一次数据,计数器减1。
这时就可以通过计数器的值来判断空满,当计数器的值为0,则表示同步FIFO中没有可读的数据,此时同步FIFO为空。
当计数器的值等于同步FIFO的深度时,则表示同步FIFO中已经写满了。
异步fifo的读空和写满
异步fifo的读和写工作在不同的时钟域,因此无法通过计数器进行读空和写满的判断,需要用读指针和写指针来进行判断,后续会进行详细讲解。
三、异步fifo的读空和写满具体理解
通过比较写指针和读指针来判断空满,如下图所示,先对异步FIFO写入数据1,写两次,然后对异步FIFO读两次,把之前写进去的数据1都读出来,已经没有数据可读,所以此时异步FIFO处于空状态。注意此时写指针和读指针相等,都是指向地址为2的存储空间。
然后对异步FIFO写数据0,写6次,此时写指针重新指向地址0所对应的存储空间,此时的状态不是满状态,因为之前写入两次数据1已经被读出,此时地址0和地址1还是可以写入数据的,再对异步FIFO写数据0,写两次,此时异步FIFO中所有的存储空间都写入数据,并且没有一个数据是被读出过的,所以此时是满状态,此时也是写指针和读指针相等。
所以可以总结当读指针追上写指针时,异步FIFO处于空状态。写指针追上读指针时,异步FIFO处于满状态。但是这种方法处理后,仍然有一个问题就是读写指针相等时,难以区分是空状态还是满状态。
因此需要多引入1位来判断是读指针追上写指针还是写指针追上读指针,对于上图中的fifo,深度为8,本来需要3位就可以描述读指针和写指针,这里需要变成4位来描述读指针和写指针,FIFO的深度决定了指针扩展前的宽度,而这扩展的一位与FIFO深度无关,是为了标志指针多转了一圈。
因此,当读写指针完全相同时,此时FIFO为空状态。
当读写指针最高位不同,其余位宽相同时,FIFO为满状态。
用扩展指针的方式分析上面的例子,如下图所示,空状态时,rptr=0010,wptr=0010,读写指针完全相同,为空状态。满状态时,rptr=0010,wptr=1010,读写指针最高位不同,其余位宽相同,为满状态。
经过指针扩展,可以进行空满检测。但是异步FIFO读写时钟不同,而读写指针又是分属各自的时钟域,在进行空满检测的时候,是需要将读写指针进行跨时钟域处理的,即读指针跨时钟到写指针,写指针跨时钟到读指针。
在跨时钟域时,使用二进制的方式很容易出现错误,容易出现亚稳态,相邻二进制地址变化时,不止一位发生变化。比如写指针从0111到1000跳变时,4位地址都可以发生改变,这样读时钟在进行写指针同步后得到的写指针可能是0000~1111中任意一个值,这些都是不可控的,出错的概率非常大,因此需要引入格雷码。
格雷码对于相邻的数每次只变化一位,这样就大大的降低了亚稳态出现的概率。常用的格雷码和二进制码之间的转换关系如下表所示:
可以观察到,对于二进制码来说,当fifo深度为8时,低三位用来表示读和写的指针的位置,最高位(即第3位)用来判断读空和写满,而对于格雷码来说是要用高两位(即第3位和第2位)来判断读空和写满。当读和写指针的所有位都相同时,判断为读空;当读和写指针的高两位不同,低两位相同时,判断为写满。
四、Verilog实现
架构
异步fifo的内部电路架构如下图所示:
异步FIFO的内部电路主要分为5个部分,会在下面部分一一详细介绍。在这里主要介绍一下内部的信号。
wptr为写指针,写指针的位数比写地址多一位。
rptr为读指针,读指针的位数比读地址多一位。
waddr为写地址,为具体把数据写入FIFO_Memory的地址。
raddr为读地址,为具体读出FIFO_Memory中数据的地址。
wq2_rptr为将读指针同步到写时钟域的信号。
rq2_wptr为将写指针同步到读时钟域的信号。
写指针、写满信号和写地址产生模块
Verilog代码如下:
module wptr_wfull #(
parameter ADDRSIZE=4)
(
input winc,
input wclk,
input wrst_n,
input [ADDRSIZE:0] wq2_rptr,
output reg wfull,
output reg [ADDRSIZE:0] wptr,
output [ADDRSIZE-1:0] waddr
);
wire full_value;
wire [ADDRSIZE:0] wbinnext , wgraynext;
reg [ADDRSIZE:0] wbin;
//写地址产生
assign wbinnext=wbin+(winc&!wfull);
always@(posedge wclk or negedge wrst_n)
begin
if(wrst_n==1’b0)
wbin<=0;
else
wbin<=wbinnext;
end
assign waddr=wbin[ADDRSIZE-1:0];
//写指针产生
assign wgraynext=wbinnext^(wbinnext>>1);
always @(posedge wclk or negedge wrst_n)
begin
if(wrst_n==1’b0)
wptr <=0;
else
wptr<=wgraynext;
end
//满信号产生
assign full_value=wgraynext == ({~wq2_rptr[ADDRSIZE:ADDRSIZE-1],wq2_rptr[ADDRSIZE-2:0]});
always@(posedge wclk or negedge wrst_n)
begin
if(wrst_n==1’b0)
wfull<=1’b0;
else
wfull<=full_value;
end
endmodule
读指针、读空信号和读地址产生模块
Verilog代码如下:
module rptr_rempty #(
parameter ADDRSIZE=4)
(
input rinc,
input rclk,
input rrst_n,
input [ADDRSIZE:0] rq2_wptr,
output reg rempty,
output reg [ADDRSIZE:0] rptr,
output [ADDRSIZE-1:0] raddr
);
wire empty_value;
wire [ADDRSIZE:0] rbinnext , rgraynext;
reg [ADDRSIZE:0] rbin;
//读地址产生
assign rbinnext=rbin+(rinc&!rempty);
always@(posedge rclk or negedge rrst_n)
begin
if(rrst_n ==1’b0)
rbin <=0;
else
rbin <=rbinnext;
end
assign raddr=rbin[ADDRSIZE-1:0];
//读指针产生
assign rgraynext=rbinnext^(rbinnext>>1);
always@(posedge rclk or negedge rrst_n)
begin
if(rrst_n ==1’b0)
rptr <=0;
else
rptr<=rgraynext;
end
//空信号产生
assign empty_value=rgraynext == rq2_wptr;
always@(posedge rclk or negedge rrst_n)
begin
if(rrst_n==1’b0)
rempty<=1’b0;
else
rempty<=empty_value;
end
endmodule
读指针同步模块
Verilog代码如下:
module sync_r2w #(
parameter ADDRSIZE=4)
(
input wclk,
input wrst_n,
input [ADDRSIZE:0] rptr,
output reg [ADDRSIZE:0] wq2_rptr
);
reg [ADDRSIZE:0] wq1_rptr;
always@(posedge wclk or negedge wrst_n)
begin
if(wrst_n==1’b0)
{wq2_rptr, wq1_rptr}<=2’b0;
else
{wq2_rptr, wq1_rptr}<= {wq1_rptr, rptr} ;
end
endmodule
写指针同步模块
module sync_w2r #(
parameter ADDRSIZE=4)
(
input rclk,
input rrst_n,
input [ADDRSIZE:0] wptr,
output reg [ADDRSIZE:0] rq2_wptr
);
reg [ADDRSIZE:0] rq1_wptr;
always@(posedge rclk or negedge rrst_n)
begin
if(rrst_n==1’b0)
{rq2_wptr, rq1_wptr}<=2’b0;
else
{rq2_wptr, rq1_wptr}<= {rq1_wptr, wptr};
end
endmodule
数据存储模块
module fifomem #(
parameter ADDRSIZE=4,
parameter DATASIZE=8)
(
input winc,
input wclk,
input wfull,
input [ADDRSIZE-1:0] waddr , raddr,
input [DATASIZE-1:0] wdata ,
output [DATASIZE-1:0] rdata
);
localparam DEPTH=1<<ADDRSIZE;
reg [DATASIZE-1:0] mem [0:DEPTH-1];
assign rdata=mem[raddr];
always@(posedge wclk)
begin
if(! wfull & winc)
mem[waddr]<= wdata;
end
endmodule
顶层模块
module afifo #(
parameter ADDRSIZE=4,
parameter DATASIZE=8)
(
input wclk,
input rclk,
input wrst_n,
input rrst_n,
input winc,
input rinc,
input [DATASIZE-1:0] wdata,
output [DATASIZE-1:0] rdata,
output rempty,
output wfull
);
wire [ ADDRSIZE:0] wq2_rptr;
wire [ ADDRSIZE:0] wptr;
wire [ ADDRSIZE-1:0] waddr;
wire [ ADDRSIZE:0] rq2_wptr;
wire [ ADDRSIZE:0] rptr;
wire [ ADDRSIZE-1:0] raddr;
wptr_wfull u1(
.winc(winc),
.wclk(wclk),
.wrst_n(wrst_n),
.wq2_rptr(wq2_rptr),
.wfull(wfull),
.wptr(wptr),
.waddr(waddr)
);
rptr_rempty u2(
.rinc(rinc),
.rclk(rclk),
.rrst_n(rrst_n),
.rq2_wptr(rq2_wptr),
.rempty(rempty),
.rptr(rptr),
.raddr(raddr)
);
sync_r2w u3(
.wclk(wclk),
.wrst_n(wrst_n),
.rptr(rptr),
.wq2_rptr(wq2_rptr)
);
sync_w2r u4(
.rclk(rclk),
.rrst_n(rrst_n),
.wptr(wptr),
.rq2_wptr(rq2_wptr)
);
fifomem u5(
.winc(winc),
.wclk(wclk),
.wfull(wfull),
.waddr(waddr),
.raddr(raddr),
.wdata(wdata),
.rdata(rdata)
);
endmodule
Testbench
module afifo_tb();
reg wclk;
reg rclk;
reg wrst_n;
reg rrst_n;
reg winc;
reg rinc;
reg [7:0] wdata;
wire [7:0] rdata;
wire rempty;
wire wfull;
afifo u6(
.wclk(wclk),
.rclk(rclk),
.wrst_n(wrst_n),
.rrst_n(rrst_n),
.winc(winc),
.rinc(rinc),
.wdata(wdata),
.rdata(rdata),
.rempty(rempty),
.wfull(wfull)
);
initial begin //设置写时钟,写周期是20ns,50Mhz
wclk=0;
forever #10 wclk=~wclk;
end
initial begin //设置读时钟,读周期是10ns,100Mhz
rclk=0;
forever #5 rclk=~rclk;
end
initial begin
wrst_n=1'b0; //写复位
rrst_n=1'b0; //读复位
winc =1'b0; //写无效
rinc =1'b0; //读无效
wdata=0; //初始写数据为0
#28 wrst_n=1'b1; //松开写复位
rrst_n=1'b1; //松开读复位
winc =1'b1; //写有效
wdata=1; //输入数据为1
@(posedge wclk);//写入数据
repeat(7) //接着写入2,3,4,5,6,7,8这些数据
begin
#18;
wdata=wdata+1'b1;
@(posedge wclk);
end
#18 wdata=wdata+1'b1; //此时异步FIFO已经写满了,在往同步FIFO中写数据8
//8这个数据不会被写进
@(posedge rclk);
#8 rinc=1’b1; //读使能,写无效
winc=1'b0;
@(posedge rclk); //第一个读出的数为1
repeat(7) //读取剩余的数
begin
@(posedge rclk);
end
#2;
rinc=1‘b0; //结束读操作
end
endmodule
仿真结果如下: