由上图可知,异步FIFO可以分为四部分:
使用一位扩展位来指明读或写过的次数,进而可以通过比较地址就可以得到FIFO是否写满或者读空。
读空:最高位和其余位相同。
写满:最高位不同,其余位相同。
读空:最高位和其余位相同。
写满:最高位、次高位均不同,其余位相同。
二进制读指针在增减时,经常发生多位突变,比如六位地址111111会在下一时刻变成000000,在实际电路这,这个变化过程要持续很长一段时间,会由111111经历6个状态转移到000000,比如111111>101111>100111>100100>000100>000000。由于写时钟与读时钟域不同步,==异步的写时钟很可能会在状态不稳定的中间某个状态采样,这样就会得到错误的读地址指针,进而做出错误的状态判断,导致系统异常(亚稳态问题)==。而且由于多位同时突变,凭借概率论常识可知发生错误的可能性很大。**
显然,在中间状态采样,这个是不可避免的,这是异步系统天生的缺陷。我们的目标是:即使在中间态采样,也不能影响空满状态的判断。符合这个要求的编码方式是:每次只能有1bit发生改变。
为什么这么说?因为当只有一个bit发生改变时,即使在中间状态采样,其结果也不外乎两种:递增前原指针和递增后新指针。显然递增后新指针是最新情况的反映,如果采样到这个指针,那么和我们的设计预期是一致的。如果采样到递增前原指针,会有什么结果呢?假设现在写时钟域采样读指针,那么最坏的情况是把“不满”判断成了“满”,使得本来被允许的写操作被禁止了(虚满),但是这并不会对逻辑产生影响,只是带来了写操作的延迟。同样的,如果现在**读时钟域****采样写指针,那么最坏的情况就是把“不空”判断成“空”(虚空),使得本来被允许的读操作被禁止了,**但是这不会对逻辑产生影响,只是带来了读操作的延迟。
显然每次只变化1bit的编码方案可以有效解决中间态下空满状态的判断问题,格雷码就是这样一种编码。
采用格雷码编码(解决汇聚问题),因为格雷码每次跳转只会有一位发生变化,所以如果出现不确定状态也只会有两种状况,即正确变化了和不变。因此在读写时钟不一样的情况下,纵使读写地址每bit同步过程中出现延时不一致,也不会使得FIFO在实际空或者满之后,FIFO却没有正确的产生出空满信号。只有可能是实际没有空或者满,但产生了空满信号,但这对于FIFO的功能不会有影响,只会使得FIFO的读或者写操作暂停。
由此可见,异步FIFO通过一下两个途径来解决FIFO 结构中固有的异步时钟亚稳态现象:
最高位不变,二进制的第n位和第n+1位异或作为格雷码的第n位。
读空信号的产生:写时钟域的写地址指针二进制指针先转为格雷码,之后经过2级寄存器之后与读时钟域的读地址指针进行比较,来判断是否读空。
写满信号的产生:读时钟域的读地址指针二进制指针先转为格雷码,之后经过2级寄存器之后与写时钟域的写地址指针进行比较,来判断是否写满。
同步的方法:利用格雷码+2级寄存器打两拍,之后发生亚稳态的概率会很小。 寄存器同步消耗了时间,这个时间对功能没有影响,对性能有影响。(判断的满并非真满,判断的空并非真空,会浪费FIFO存储空间,但保证不溢出)
为什么不用握手机制来同步读写指针到对应的时钟域?因为握手中反馈需要跨过两次时钟域,对于效率很有影响。比如说push这一侧要等到反馈信号回来之后才能继续下一个push,哪怕FIFO里面还有很多空闲的单元。pop的这一侧也是一样。这样对于FIFO的整体性能影响太大。
慢时钟域同步快时钟域格雷码时候,在慢时钟域的一个周期中,经历了两次或多次快时钟域的上升沿,那么对应的格雷码就会有两个或多个bits发生变化,这个不会产生多个bits同步的问题吗?
关键是理清一点:多个bit发生变化其实是针对source clock的每一个edge来说的,因为不同bit之间发生翻转的时间不能严格对齐,所以会导致destination clock可能看到不同的值,导致最后synchronizer输出会出现错误的值,从而影响FIFO的空满判断。而gray code在每个source clock的沿只会有一个bit发生翻转,其余bit保持稳定,这样每个destination clock edge来的时候最多也只可能碰到1bit在翻转,这个翻转的bit可能会给synchronizer的第一级引入metastable,但是最后synchronizer的输出无非就是保持前值或者是更新后的值,而这两个值都是合理的值,不会出现一个错误的值从而导致FIFO空满判断逻辑错误。虽然慢时钟域同步过来的值可能和之前的值相比有多个bit发生变化,但是这些bit的翻转不是同时发生的,这是回答这道题的关键。
即由于源端使用格雷码编码,每次只会变化一位,导致在目的端采样源端数据的时候,只会采样到源端数据的两种状态:变化前的状态或者只变化一位的状态。采样到这两种状态中的哪一种,都不会对空满判断功能产生错误判断,最多是虚空,虚满判断,对功能正确性没有影响。
即格雷码保证了,在dst_clk采样时刻,只有一bit是在变化中,由于时钟或者路径的延迟可能会采样到源端数据变化前、后的值,也可能采样到亚稳态状态(采样时输入正在变化),但是其他n-1位的数据都是稳定不变的可以通过顺利通过两级触发器采样到。目的端采样到变化前、后的值或者不确定的值,都对FIFO的空满判断正确性无影响,只会产生虚空,虚满判断。
“知其然而不知其所以然!”
如何判断FIFO是真空/真满呢?
回答:判断假空假满刚好相反,在push side我们来判断空,在pop side来判断满?
设计一个depth=1的异步FIFO
回答:这个大家就是要活学活用了,不能死板套用。只需要考虑一个问题,只有1个entry,那么需要几位的address 或者pointer呢?当然是1位就够了,那我们真的还需要一个pointer吗?因为只有一个entry,当一次push,FIFO就满了,一次pop,FIFO就空了。1个bit用来表示满和空就足够了。其实这样的FIFO我们已经见过了,带反馈的asynchronous load其实就是depth=1的异步FIFO!
设计一个深度为256的异步FIFO,数据宽度为16bit。源码和测试激励如下。
module fifo_async
#(
parameter data_width=16,
parameter data_depth=8,
parameter ram_depth=256)
(
input rst_n,
input wr_clk,
input wr_en,
input[data_width-1:0] data_in,
output full,
input rd_clk,
input rd_en,
output reg[data_width-1:0] data_out,
output empty
);
//ram的实际读写地址
wire [data_depth-1:0] wr_addr;
wire [data_depth-1:0] rd_addr;
//ram扩展一位的读写地址指针
reg [data_depth:0] wr_addr_ptr;
reg [data_depth:0] rd_addr_ptr;
//读写地址的格雷码及地址格雷码跨时钟域转换之后的地址码
wire [data_depth:0] wr_addr_ptr_gray_w;
// reg [data_depth:0] wr_addr_ptr_gray_reg;
reg [data_depth:0] wr_addr_ptr_gray1;
reg [data_depth:0] wr_addr_ptr_gray2;
wire [data_depth:0] rd_addr_ptr_gray_w;
// reg [data_depth:0] rd_addr_ptr_gray_reg;
reg [data_depth:0] rd_addr_ptr_gray1;
reg [data_depth:0] rd_addr_ptr_gray2;
integer i;
// 双时钟双端口ram
reg [data_width-1:0] ram[ram_depth-1:0];
assign wr_addr=wr_addr_ptr[data_depth-1:0];
assign rd_addr=rd_addr_ptr[data_depth-1:0];
always @(posedge wr_clk or negedge rst_n ) begin
if(!rst_n) begin
for(i=0;i<ram_depth;i=i+1)
ram[i]<=0;
end
else if(wr_en && (~full)) begin
ram[wr_addr]<=data_in;
end
else ram[wr_addr]<=ram[wr_addr]; //写满后不能继续写
end
always @(posedge rd_clk or negedge rst_n ) begin
if(!rst_n) begin
data_out<=0;
end
else if(rd_en && (~empty)) begin
data_out<=ram[rd_addr];
end
else data_out<=0; //读空后,读出0
end
// 生成下一周期的读写地址指针
always @(posedge wr_clk or negedge rst_n ) begin
if(!rst_n) begin
wr_addr_ptr<=0;
end
else if(wr_en && (~full)) begin
wr_addr_ptr<=wr_addr_ptr+1;
end
else wr_addr_ptr<=wr_addr_ptr; //写失败,写指针不变即可
end
always @(posedge rd_clk or negedge rst_n ) begin
if(!rst_n) begin
rd_addr_ptr<=0;
end
else if(rd_en && (~empty)) begin
rd_addr_ptr<=rd_addr_ptr+1;
end
else rd_addr_ptr<=rd_addr_ptr; //读失败,读指针不变即可
end
// 读写地址二进制地址转格雷码
assign wr_addr_ptr_gray_w=(wr_addr_ptr>>1)^wr_addr_ptr;
assign rd_addr_ptr_gray_w=(rd_addr_ptr>>1)^rd_addr_ptr;
// 跨时钟域地址转换:r2w w2r
always @(posedge wr_clk or negedge rst_n ) begin
if(!rst_n) begin
rd_addr_ptr_gray1<=0;
rd_addr_ptr_gray2<=0;
end
else begin
rd_addr_ptr_gray1<=rd_addr_ptr_gray_w;
rd_addr_ptr_gray2<=rd_addr_ptr_gray1;
end
end
always @(posedge rd_clk or negedge rst_n ) begin
if(!rst_n) begin
wr_addr_ptr_gray1<=0;
wr_addr_ptr_gray2<=0;
end
else begin
wr_addr_ptr_gray1<=wr_addr_ptr_gray_w;
wr_addr_ptr_gray2<=wr_addr_ptr_gray1;
end
end
//空满信号判断
assign empty=(rd_addr_ptr_gray_w==wr_addr_ptr_gray2);
assign full=((wr_addr_ptr_gray_w[data_depth]!=rd_addr_ptr_gray2[data_depth])
&&(wr_addr_ptr_gray_w[data_depth-1]!=rd_addr_ptr_gray2[data_depth-1])
&&(wr_addr_ptr_gray_w[data_depth-2:0]==rd_addr_ptr_gray2[data_depth-2:0]));
endmodule
`timescale 1ns / 1ps
module asyn_fifo_tb;
parameter data_depth=8;
reg rst_n;
reg wr_clk;
reg wr_en;
reg [15:0] data_in;
wire full;
reg rd_clk;
reg rd_en;
wire [15:0] data_out;
wire empty;
fifo_async asyn_fifo_inst
(
.rst_n (rst_n),
.wr_clk (wr_clk),
.wr_en (wr_en),
.data_in (data_in),
.full (full),
.rd_clk (rd_clk),
.rd_en (rd_en),
.data_out (data_out),
.empty (empty)
);
initial wr_clk = 0;
always#10 wr_clk = ~wr_clk;
initial rd_clk = 0;
always#30 rd_clk = ~rd_clk;
always@(posedge wr_clk or negedge rst_n)begin
if(!rst_n)
data_in <= 'd0;
else if(wr_en)
data_in <= data_in + 1'b1;
else
data_in <= data_in;
end
initial begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
#200;
rst_n = 1;
wr_en = 1;
#20000;
wr_en = 0;
rd_en = 1;
#20000;
rd_en = 0;
$stop;
end
endmodule
参考:https://mp.weixin.qq.com/s/TR_5imTfUI2-LGbPOE7OkA
可能本设计会存在的有一些bug,目前我还没有发现!希望大家对我的设计多多提出批评!