本文参考:
面试——异步FIFO详解
关于异步FIFO设计,这7点你必须要搞清楚
【CDC 系列】跨时钟域处理(一)同步器
02【Verilog实战】异步FIFO设计(附源码RTL/TB)
在上一篇同步FIFO中已经介绍过,FIFO是一种先进先出的数据缓存器,它与普通存储器的区别是没有读写外部地址,这样使用起来非常简单,但缺点就是只能顺序写入数据。
FIFO分为同步FIFO和异步FIFO两种,其中同步FIFO
的输入与输出采用相同的时钟,而异步FIFO
的读写时钟是互相独立的。
如图所示为异步FIFO结构图,其中FIFO Memory相当于一个双端口RAM,两个格雷码计数器用于计数读写指针并将二进制读写指针转换成格雷码,两个同步模块分别用于将写指针同步到读时钟域判断读空以及将读指针同步到写时钟域判断写满,此外还有full/empty标志信号产生模块。
相较于同步FIFO,由于异步FIFO的读写时钟不同步,如果按照同步FIFO一样直接把读写指针进行比较,会带来很多亚稳态问题,比如在比较时写指针正处于亚稳态,会造成比较出现错误。因此就需要将读写指针同步到相同的时钟域进行比较。
同步器是一种对异步信号采样并输出具有与本地或采样时钟同步的转换信号的设备,最常用的同步器是双触发器同步器,如下图所示。
第一个触发器将异步输入信号采样到新的时钟域并等待一个完整的时钟周期以允许第一级输出信号上的任何亚稳态衰减,然后由同一时钟将第一级信号采样到第二个阶段触发器,其预期目标是,阶段 2 信号现在是一个稳定且有效的信号,已同步并准备好在新时钟域内分发。
但使用这样的方法仍然存在问题,如果读写指针采用二进制计数器,每次计数时都会有多个位同时变化,如0111到1000,所有位都会进行翻转。如果恰巧在翻转的亚稳态过程中进行采样,就会出现问题。所以在对读写指针进行跨时钟域同步的时候,最好将二进制码转换成格雷码。
使用gray码解决了一个问题,但同时也带来另一个问题,即在格雷码域如何判断空与满。
对于“空”的判断依然依据二者完全相等(包括MSB);
而对于“满”的判断,如下图,由于gray码除了MSB外,具有镜像对称的特点,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,不能说它为满。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:
- wptr和同步过来的rptr的MSB不相等,因为wptr必须比rptr多折回一次。
- wptr与rptr的次高位不相等,如上图位置7和位置15,转化为二进制对应的是0111和1111,MSB不同说明多折回一次,111相同代表同一位置。
- 剩下的其余位完全相等。
这里直接给出结论:
异步FIFO的读、写指针是不同时钟域的信号,那么就不能直接对比,而是需要将其同步到同一时钟域才能进行对比。现在问题来了?指针应该被同步到哪个时钟域?可选项有第三方时钟域、读时钟域和写时钟域。接下来不妨逐个分析。
同步到写时钟域:
读指针同步到写时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。
- 写满判断:写指针超过同步后的读指针一圈,但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是
“假写满”
。那么“假写满”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设一个深度为100的FIFO,在写到第98个数据的时候就报了“写满”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了FIFO的深度我少用一点点就是的。事实上这还可以算是某种程度上的保守设计(安全)。- 读空判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了“读空”,却仍然有错误数据读出。所以这种情况就造成了FIFO的功能错误。这时候输出的是真空信号,但是会造成“读空”,所以不可取。
同步到读时钟域:
写指针同步到读时钟域需要时间T,在经过T时间后,可能原来的写指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。
- 读空判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是
“假读空”
。那么“假读空”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设某个FIFO,在读到还剩2个数据的时候就报了“读空”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计(安全)。- 写满判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了“写满”,却仍然数据被覆盖写入。所以这种情况就造成了FIFO的功能错误。这时候输出的是真满信号,但是会造成数据覆盖,所以也不可取。
所以总结一下,判断读空或写满都要在本时钟域进行,也就是说判断读空在读时钟域,判断写满在写时钟域。那么假读空会不会造成有数据在FIFO里面读不出来?答案是不会,同步时虽然延迟了两个信号,但是最终还是会同步到跨时钟域,所以假读空信号不会一直有效,还是会在延后几个周期把数据读出来。
将读指针同步到写时钟域来判断满;将写指针同步到读时钟域来判断空。既然是异步FIFO,那么读写时钟域的信号是不一致的,其中一个的频率快,另一个的频率慢。那么在两次同步过程中,一定是一次慢时钟采快时钟和一次快时钟采慢时钟。快时钟采慢时钟是不会有问题的,因为这符合采样定理。但是慢时钟采快时钟则会有问题,因为采样过程不符合采样定理。
那么会造成什么问题?答案是漏采。某些数值可能会被漏掉。例如原本是连续的0–1–2—3的信号,从快时钟同步到慢时钟后,就变成了离散的0–3,其中的1、2被漏掉了。那么这样一种现象会导致空、满的判断是准确的吗?答案是不准确,但没关系。
设想读慢写快与读快写慢两种情况:
(1)读慢写快
进行写满判断的时候需要将读指针同步到写时钟域,因为读慢写快,所以不会有读指针遗漏,同步消耗时钟周期,所以同步后的读指针滞后(小于等于)当前读地址,所以可能写满会提前产生,并非真写满。
进行读空判断的时候需要将写指针同步到读指针 ,因为读慢写快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,我们不用关心那到底会漏掉哪些写指针,我们在乎的是漏掉的指针会对FIFO的读空产生影响吗?比如写指针从0写到10,期间读时钟域只同步捕捉到了3、5、8这三个写指针而漏掉了其他指针。当同步到8这个写指针时,真实的写指针可能已经写到10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到FIFO去,这样在判断它是不是空的时候会出现不是真正空的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。
(2)读快写慢
进行读空判断的时候需要将写指针同步到读指针 ,因为读快写慢,所以不会有写指针遗漏,同步消耗时钟周期,所以同步后的写指针滞后(小于等于)当前写地址,所以可能读空会提前产生,并非真读空。
进行写满判断的时候需要将读指针同步到写时钟域,因为读快写慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,我们不用关心那到底会漏掉哪些读指针,我们在乎的是漏掉的指针会对FIFO的写满产生影响吗?比如读指针从0读到10,期间写时钟域只同步捕捉到了3、5、8这三个读指针而漏掉了其他指针。当同步到8这个读指针时,真实的读指针可能已经读到10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能从FIFO读了数据出来,这样在判断它是不是满的时候会出现不是真正满的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。
现在我们会发现,所谓的空满信号实际上是不准确的,在还没有空、满的时钟就已经输出了空满信号,这样的空满信号一般称为假空、假满。假空、假满信号本质上是一种保守设计,想象一下,一个深度为16的异步FIFO,在其写入14个数据时,即输出了写满(假满)标志,这会对我们的设计造成影响吗?会,会削弱我们的效率,我们实际使用的FIFO深度成了14,但是会使得我们的设计产生错误吗?显然不会。同样的,在FIFO深度仍有2时即输出了读空(假空)标志,也不会使得我们的设计出错,但是会降低效率,因为我们使用的FIFO深度又少了2。
2次幂深度的FIFO可以直接加一个标志位像现在这样比较,非二次幂需要重新设计格雷码比较空满的规则,空标志只用判断读、写指针是否全部相等即可,但是满标志就需要找其他规律了。实际上不管你设计FIFO用RAM还是直接调用IP也好,最终实现都是用的Block RAM资源,其生成的位宽肯定是2的幂次,所以基本不会出现非二次幂的情况。
`timescale 1ns/1ps
module async_fifo #(
parameter DATA_WIDTH = 32,
parameter DATA_DEPTH = 8,
parameter PTR_WIDTH = $clog2(DATA_DEPTH)
)(
input wire clk_rd_i,
input wire rst_n_rd_i,
input wire clk_wr_i,
input wire rst_n_wr_i,
// write interface
input wire wr_en_i,
input wire [DATA_WIDTH-1:0] wr_data_i,
// read interface
input wire rd_en_i,
output reg [DATA_WIDTH-1:0] rd_data_o,
// flags_o
output wire full_o,
output wire empty_o
);
reg [DATA_WIDTH-1:0] FIFO[0:DATA_DEPTH-1];
reg [PTR_WIDTH:0] rd_ptr;
reg [PTR_WIDTH:0] rd_ptr_d1;
reg [PTR_WIDTH:0] rd_ptr_gray_d2;
wire [PTR_WIDTH:0] rd_ptr_gray;
wire [PTR_WIDTH-1:0] rd_ptr_true;
reg [PTR_WIDTH:0] wr_ptr;
reg [PTR_WIDTH:0] wr_ptr_d1;
reg [PTR_WIDTH:0] wr_ptr_gray_d2;
wire [PTR_WIDTH:0] wr_ptr_gray;
wire [PTR_WIDTH-1:0] wr_ptr_true;
/*---------------------------------------------------\
------------- rd_ptr++ and wr_ptr++ --------------
\---------------------------------------------------*/
always @(posedge clk_rd_i or negedge rst_n_rd_i) begin
if(!rst_n_rd_i)
rd_ptr <= {(PTR_WIDTH+1){1'b0}};
else if(rd_en_i==1'b1 && empty_o==1'b0)
rd_ptr <= rd_ptr + 1;
end
always @(posedge clk_wr_i or negedge rst_n_wr_i) begin
if(!rst_n_wr_i)
wr_ptr <= {(PTR_WIDTH+1){1'b0}};
else if(wr_en_i==1'b1 && full_o==1'b0)
wr_ptr <= wr_ptr + 1;
end
assign rd_ptr_true = rd_ptr[PTR_WIDTH-1:0];
assign wr_ptr_true = wr_ptr[PTR_WIDTH-1:0];
/*---------------------------------------------------\
---------- data read and data write ---------------
\---------------------------------------------------*/
always @(posedge clk_rd_i or negedge rst_n_rd_i) begin
if(!rst_n_rd_i)
rd_data_o <= {(DATA_WIDTH){1'b0}};
else if(rd_en_i==1'b1 && empty_o==1'b0)
rd_data_o <= FIFO[rd_ptr_true];
end
integer i;
always @(posedge clk_wr_i or negedge rst_n_wr_i) begin
if(!rst_n_wr_i)
for(i=0;i<DATA_DEPTH;i=i+1)
FIFO[i] <= {(DATA_WIDTH){1'b0}};
else if(wr_en_i==1'b1 && full_o==1'b0)
FIFO[wr_ptr_true] <= wr_data_i;
end
/*---------------------------------------------------\
--------- binary to gray and pointer sync ---------
\---------------------------------------------------*/
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);
assign wr_ptr_gray = wr_ptr ^ (wr_ptr>>1);
always @(posedge clk_rd_i or negedge rst_n_rd_i) begin
if(!rst_n_rd_i) begin
wr_ptr_d1 <= {(PTR_WIDTH+1){1'b0}};
wr_ptr_gray_d2 <= {(PTR_WIDTH+1){1'b0}};
end
else begin
wr_ptr_d1 <= wr_ptr_gray;
wr_ptr_gray_d2 <= wr_ptr_d1;
end
end
always @(posedge clk_wr_i or negedge rst_n_wr_i) begin
if(!rst_n_rd_i) begin
rd_ptr_d1 <= {(PTR_WIDTH+1){1'b0}};
rd_ptr_gray_d2 <= {(PTR_WIDTH+1){1'b0}};
end
else begin
rd_ptr_d1 <= rd_ptr_gray;
rd_ptr_gray_d2 <= rd_ptr_d1;
end
end
/*---------------------------------------------------\
------------ full_o and empty_o ------------------
\---------------------------------------------------*/
assign full_o = (wr_ptr_gray=={~rd_ptr_gray_d2[PTR_WIDTH:PTR_WIDTH-1], rd_ptr_gray_d2[PTR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
assign empty_o = (rd_ptr_gray==wr_ptr_gray_d2) ? 1'b1 : 1'b0;
endmodule
测试文件:
`timescale 1ns/1ps
module async_fifo_tb;
reg clk_rd_i;
reg rst_n_rd_i;
reg clk_wr_i;
reg rst_n_wr_i;
// write interface
reg wr_en_i;
reg [31:0] wr_data_i;
// read interface
reg rd_en_i;
wire [31:0] rd_atao;
// flags_o
wire full_o;
wire empty_o;
initial begin
clk_rd_i = 0;
rst_n_rd_i = 1;
clk_wr_i = 0;
rst_n_wr_i = 1;
wr_data_i = 32'b0;
#10;
rst_n_rd_i = 0;
rst_n_wr_i = 0;
#10;
rst_n_rd_i = 1;
rst_n_wr_i = 1;
end
always #30 clk_rd_i = ~clk_rd_i;
always #50 clk_wr_i = ~clk_wr_i;
always #100 wr_data_i = {wr_data_i+1}%20;
initial begin
wr_en_i = 1;
rd_en_i = 0;
#800;
wr_en_i = 0;
rd_en_i = 1;
#300;
wr_en_i = 1;
rd_en_i = 0;
#1000;
wr_en_i = 0;
rd_en_i = 1;
#1000;
wr_en_i = 1;
rd_en_i = 0;
#300;
wr_en_i = 1;
rd_en_i = 1;
#1000;
repeat(100) begin
#200 wr_en_i = {$random}%2;
rd_en_i = {$random}%2;
end
$stop;
end
async_fifo u_async_fifo(
.clk_rd_i (clk_rd_i ),
.rst_n_rd_i (rst_n_rd_i ),
.clk_wr_i (clk_wr_i ),
.rst_n_wr_i (rst_n_wr_i ),
.wr_en_i (wr_en_i ),
.wr_data_i (wr_data_i ),
.rd_en_i (rd_en_i ),
.rd_data_o (rd_data_o ),
.full_o (full_o ),
.empty_o (empty_o )
);
endmodule