FIFO根据输入输出时钟是否一致,分为同步FIFO与异步FIFO。同步FIFO中,读写控制信号以及数据均处于同一时钟域,满足STA分析时一般不会出现亚稳态等不稳定情形;而对于异步FIFO,读写相关信号处于不同时钟域,信号的不同步可能会导致亚稳态,导致FIFO工作异常,设计较为复杂;在之前的记录中,我们对同步FIFO的设计进行了分析:
此处我们不再对同步FIFO进行介绍而直接以异步FIFO与同步FIFO的异同为线索,逐步对异步FIFO进行分析,介绍异步FIFO相比于同步FIFO的额外处理,并进一步实现异步FIFO。
目录
同步FIFO的读写控制信号以及数据均处于同一时钟域,即:FIFO在同一时钟驱动下进行读写操作,读控制信号有效且FIFO不为空时,输出读指针对应地址的数据,随后读指针加1;写控制信号有效且FIFO不为满时,将输入数据存储到写指针对应地址处,随后写指针加1;
异步FIFO的工作内容与同步FIFO类似:FIFO在时钟驱动下进行读写操作,读控制信号有效且FIFO不为空时,输出读指针对应地址的数据,随后读指针加1;写控制信号有效且FIFO不为满时,将输入数据存储到写指针对应地址处,随后写指针加1;
但是异步FIFO的控制并不像同步FIFO那么简单,因为异步FIFO工作在不同的时钟域,这就带来了一些问题:
(1)如何进行空满检测?还能像同步FIFO中通过计数值来判断吗?
(2)需要同步电路
同步FIFO的空满检测可以通过计数很简单的实现:
读写逻辑是同一个时钟,因此可以在每次时钟来临时进行判断,如果不执行读写操作/同时读写,则计数值不变;只执行读操作,计数值减1;只执行写操作,计数值加1;
如果计数值为0,则说明FIFO空,只能写不能读(直到写入一次数据,计数值加1,FIFO不再为空才能执行读操作);
如果计数值为FIFO深度,则说明FIFO满,只能读不能写(直到读出一次数据,计数值减1,FIFO不再为满才能执行写操作);
异步FIFO不能采用同步FIFO这种计数方式来实现空满检测,因为用两个时钟去控制同一个计数器的加剪很明显是不可取的。
如果不明白为什么不能用两个时钟控制同一个计数器,可以查阅:Verilog中always@()语句双边沿触发(语法与综合的差异)
读写指针指向读写操作面向的FIFO地址空间,因此空满检测的另一个思路是比较读写指针。每次写操作执行,写指针加1;而每次读操作执行,读指针加1,因此:
FIFO空发生在:读指针追上写指针时;
FIFO满发生在:写指针追上读指针时;
但是这种处理仍存在一个问题,就是读写指针相等时,难以区分FIFO是空还是满。
如上分析,直接比较读写指针时存在一个特例:读写指针相等时,难以区分FIFO是空还是满。
因此,有人提出,将指针进行高位扩展。即指针加宽一位,当写指针超出FIFO深度时,这额外的一位就会改变。FIFO的深度决定了指针扩展前(即除了最高位的其余位)的宽度,而这扩展的一位与FIFO深度无关,是为了标志指针多转了一圈,因此:
当读写指针完全相同时,FIFO空;
当读写指针高位不同,其余位完全相同时,FIFO满;
经过指针扩展,可以明确的进行空满检测。但是这种处理仍然不够,因为异步FIFO读写时钟相互独立,分属不同时钟域,相邻二进制地址位变化时,不止一个地址位都要变化,这样指针在进行同步过程中很容易出错,比如写指针在从0111到1000跳变时4位同时改变,这样读时钟在进行写指针同步后得到的写指针可能是0000-1111的某个值,一共有2^4个可能的情况,而这些都是不可控制的,你并不能确定会出现哪个值,那出错的概率非常大。
如上分析,直接比较扩展读写指针时可能因多位改变导致错误。因此,进一步采用gray码形式的指针,利用格雷码每次只变化一位的特性,降低同步发生时错误的概率。如图,为一个深度为8FIFO的格雷码指针(绿色框中):
0-7为真实的FIFO地址,而8-15是指针多转一圈以后的地址(8-0,9-1…)。应注意,此时指针按照格雷码方式进行编码,不能再用二级制指针的比较方式来判断空满。比如:位置6(0101)和位置9(1101),除最高位外其余位均相等。但是很明显,位置9实际对应位置1处的存储空间。
因此,格雷码指针下的空满检测条件为:
当最高位和次高位均相同,其余位相同:FIFO空
当最高位和次高位均相反,其余位相同:FIFO满
因此,最终的空满检测方式为:将二进制指针转换为格雷码,用于另一时钟域接收,随后按照检测条件进行检测。
二进制指针转换为格雷码的详情见:Verilog实现二进制码与格雷码转换 此处不再展开。
判断FIFO空满状态时,需要在读FIFO时获取写时钟域的写指针,与读指针比较来判断FIFO是否为空;需要在写FIFO时获取读时钟域的读指针,与写指针比较来判断FIFO是否为满;
也就是说,判断空满状态时牵扯到跨时钟域问题,需要进行同步;
采用两级寄存器打两拍的方式进行同步,具体实现见:亚稳态专题
异步FIFO通过比较读写指针进行满空判断,但是读写指针属于不同的时钟域,所以在比较之前需要先将读写指针进行同步处理,将读写指针同步后再进行比较,判断FIFO空满状态。但是因为在同步指针时需要时间(如延迟两拍同步),而在这个同步的时间内有可能还会写入/读出新的数据,因此同步后的指针一定是小于或者等于当前实际的读/写指针,那么此时判断FIFO满空状态时是否会出错?是否会导致错误?
先说结论:异步逻辑进行同步时,不可避免需要额外的时钟开销,这会导致满空趋于保守,但是保守并不等于错误,这么写会稍微有性能损失,但是不会出错。
FIFO满检测发生在写时钟域,将读指针同步到写时钟域后再和写指针比较,进行FIFO满状态判断。因为同步时间的存在,同步后的读指针一定是小于或者等于当前真正的读指针(同步时间内可能出现了读操作,导致读指针增加),所以此时判断FIFO为满不一定是真满,这样更保守(即:写指针=同步读指针<=真实读指针),这样可以保证FIFO的特性:FIFO空之后不能继续读取。
FIFO空检测发生在读时钟域,将写指针同步到读时钟域后再和读指针比较,进行FIFO空状态判断。因为同步时间的存在,同步后的写指针一定是小于或者等于当前真正的写指针(同步时间内可能出现了写操作,导致写指针增加),所以此时判断FIFO为空不一定是真空,这样更保守(即:读指针=同步写指针<=真实写指针),这样可以保证FIFO的特性:FIFO满之后不能继续写入。
1、读时钟信号clk_r,作为异步FIFO的读驱动信号
2、写时钟信号clk_w,作为异步FIFO的写驱动信号
3、异步复位信号rst_n
// 写FIFO相关
4、数据输入信号din[DW-1:0],作为FIFO数据写入端,DW数据为位宽
5、写使能we
// 读FIFO相关
6、数据输出信号dout[DW-1:0],作为FIFO数据输出端
7、读使能re
// 标志相关
8、FIFO满标志full,FIFO满时不能再写入数据
9、FIFO空标志empty,FIFO空时不能再读出数据
1、读指针wp,作为读地址(FIFO读写只能顺序进行,不能外部设置,因此为内部信号)
2、格雷码读指针wp_g,供写时钟域同步后判断FIFO满使用
3、写指针rp,作为写地址
4、格雷码写指针wp_g,供读时钟域同步后判断FIFO空使用
5、[DW-1:0]ram[0:Depth-1],数据存储空间,Depth为FIFO深度
读逻辑:
clk_r来临,re有效,并且FIFO非空(empty=0)时,将读指针rp对应地址处的数据读出至dout;
// 读操作
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
dout <= {DW{1'bz}};
else if(!empty & re)
dout <= ram[rp[AW-1:0]];
else
dout <= dout;
end
读指针逻辑:
clk_r来临,re有效,并且FIFO非空(empty=0)时,进行一次读操作,rp加1(即顺序读出),并进行格雷码转换,生成对应rp_g;
// 读指针
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
rp <= {AW{1'b0}};
else if(!empty & re)
rp <= rp+1'b1;
else
rp <= rp;
end
写逻辑:
clk_w来临,we有效,并且FIFO不满(full=0)时,将din写入写指针wp对应地址处;
//写操作
always@(posedge clk_w)
begin
if(!full & we)
ram[wp[AW-1:0]] <= din;
else
ram[wp[AW-1:0]] <= ram[wp[AW-1:0]];
end
写指针逻辑:
clk_w来临,we有效,并且FIFO不满(full=0)时,进行一次写操作,wp加1(即顺序存储),并进行格雷码转换,生成对应wp_g;
//写指针
always@(posedge clk_w or negedge rst_n)
begin
if(!rst_n)
wp <= {AW{1'b0}};
else if(!full & we)
wp <= wp+1'b1;
else
wp <= wp;
end
格雷码指针生成逻辑:
利用二进制与格雷码的转换关系,将二进制指针转换为格雷码指针,用于另一个时钟域的同步接收;
// 二进制指针转换为格雷指针
assign wp_g = (wp>>1) ^ wp;
assign rp_g = (rp>>1) ^ rp;
格雷码指针同步逻辑:
读时钟域,同步写地址,用于空逻辑判断;写时钟域,同步读地址,用于满逻辑判断;
// 读时钟域,写地址同步
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
begin
wp_m <= {AW{1'b0}};
wp_s <= {AW{1'b0}};
end
else
begin
wp_m <= wp_g;
wp_s <= wp_m;
end
end
// 写时钟域,读地址同步
always@(posedge clk_w or negedge rst_n)
begin
if(!rst_n)
begin
rp_m <= {AW{1'b0}};
rp_s <= {AW{1'b0}};
end
else
begin
rp_m <= rp_g;
rp_s <= rp_m;
end
end
空满检测逻辑:
根据格雷码指针判断逻辑,进行空满检测,应注意:
读时钟域,同步写地址。读格雷码指针与同步后写格雷码指针比较,用于空逻辑判断;
写时钟域,同步读地址,写格雷码指针与同步后读格雷码指针比较,用于满逻辑判断;
// 空满检测,使用同步后的格雷指针?
assign empty = (wp_s == rp_g)?1'b1:1'b0;// 空检测,使用同步后的写格雷指针
assign full = ( {~wp_g[AW:AW-1] , wp_g[AW-2:0]} == {rp_s[AW:AW-1] , rp_s[AW-2:0]} )?1'b1:1'b0; // 满检测,使用同步后的读格雷指针
`timescale 1ns / 1ps
//
// Company:
// Engineer: guoliang CLL
//
// Create Date: 2020/03/24 20:51:06
// Design Name:
// Module Name: afifo
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module afifo
#(parameter DW = 8,AW = 4)//默认数据宽度8,FIFO深度16
(
input clk_r,
input clk_w,
input rst_n,
input we,
input re,
input [DW-1:0]din,
output reg [DW-1:0]dout,
output empty,
output full
);
// internal signal
parameter Depth = 1 << AW;//depth of FIFO
reg [DW-1:0]ram[0:Depth-1];
reg [AW:0]wp; //point
reg [AW:0]rp;
wire [AW:0]wp_g;//Gray point
wire [AW:0]rp_g;
reg [AW:0]wp_m;//mid_point for syn
reg [AW:0]rp_m;
reg [AW:0]wp_s;//point after syn
reg [AW:0]rp_s;
// FIFO declaration
// 二进制指针转换为格雷指针
assign wp_g = (wp>>1) ^ wp;
assign rp_g = (rp>>1) ^ rp;
// 空满检测,使用同步后的格雷指针?
assign empty = (wp_s == rp_g)?1'b1:1'b0;// 空检测,使用同步后的写格雷指针
assign full = ( {~wp_g[AW:AW-1] , wp_g[AW-2:0]} == {rp_s[AW:AW-1] , rp_s[AW-2:0]} )?1'b1:1'b0; // 满检测,使用同步后的读格雷指针
// 读指针
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
rp <= {AW{1'b0}};
else if(!empty & re)
rp <= rp+1'b1;
else
rp <= rp;
end
//写指针
always@(posedge clk_w or negedge rst_n)
begin
if(!rst_n)
wp <= {AW{1'b0}};
else if(!full & we)
wp <= wp+1'b1;
else
wp <= wp;
end
// 读操作
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
dout <= {DW{1'bz}};
else if(!empty & re)
dout <= ram[rp[AW-1:0]];
else
dout <= dout;
end
//写操作
always@(posedge clk_w)
begin
if(!full & we)
ram[wp[AW-1:0]] <= din;
else
ram[wp[AW-1:0]] <= ram[wp[AW-1:0]];
end
// 读时钟域,写地址同步
always@(posedge clk_r or negedge rst_n)
begin
if(!rst_n)
begin
wp_m <= {AW{1'b0}};
wp_s <= {AW{1'b0}};
end
else
begin
wp_m <= wp_g;
wp_s <= wp_m;
end
end
// 写时钟域,读地址同步
always@(posedge clk_w or negedge rst_n)
begin
if(!rst_n)
begin
rp_m <= {AW{1'b0}};
rp_s <= {AW{1'b0}};
end
else
begin
rp_m <= rp_g;
rp_s <= rp_m;
end
end
endmodule
测试文件如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer: CLL guoliang
//
// Create Date: 2020/03/24 21:22:45
// Design Name:
// Module Name: afifo_tsb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module afifo_tsb(
);
// port declaration
reg clk_r;
reg clk_w;
reg rst_n;
reg we;
reg re;
reg [7:0]din;
wire [7:0]dout;
wire empty;
wire full;
//clk
initial
begin
clk_r = 1'b0;
forever #25 clk_r = ~clk_r;
end
initial
begin
clk_w = 1'b0;
forever #10 clk_w = ~clk_w;
end
//
initial
begin
rst_n = 1'b1;
din = 1'b0;
re = 1'b0;
we = 1'b0;
#50 rst_n = 1'b0;
#50 rst_n = 1'b1;
// only write
we = 1'b1;
repeat(20) #20 din = din+1'b1;
// only read
we = 1'b0;
re = 1'b1;
repeat(20) #50;
// read and write
// we = 1'b1;
// re = 1'b1;
// din = 1'b0;
// repeat(20) #20 din = din+1'b1;
end
// inst
afifo inst2(
.clk_r(clk_r),
.clk_w(clk_w),
.rst_n(rst_n),
.we(we),
.re(re),
.din(din),
.dout(dout),
.empty(empty),
.full(full)
);
endmodule
仿真结果如下:
篇幅有限,仿真结果不再展开分析。提醒自己,应注意仿真测试是很必要的,通过功能仿真能暴露出设计上的不足、缺陷、以及实现过程中因粗心等导致的其余问题;
因此,如何设计测试文件也具有重要意义。测试文件容易编写,但是如何使得测试文件能全面的对设计进行检测,高效准确的对设计进行测试,无疑是一门学问;
我只简单记录一下,我调试时关注的部分
1、写逻辑
数据能否在写时钟驱动下,顺序写入FIFO中对应地址;FIFO满时,是否停止写入;
2、读逻辑
能否在读时钟驱动下,顺序读出FIFO中对应数据;FIFO空时,是否停止读出;
3、满判断
设计能否在写时钟驱动下,同步读指针,并且在适当位置产生满标志;
3、空判断
设计能否在读时钟驱动下,同步写指针,并且在适当位置产生空标志;
RTL电路如下:
Verilog中always@()语句双边沿触发(语法与综合的差异)
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/170193.html原文链接:https://javaforall.cn