写在前面

        在上篇文章:同步FIFO的两种Verilog设计方法(计数器法、高位扩展法)中我们介绍了FIFO的基本概念,并对同步FIFO的两种实现方法进行了仿真验证。而异步FIFO因为读写时钟不一致,显然无法直接套用同步FIFO的实现方法,所以在本文我们将用Verilog实现异步FIFO的设计。


1、什么是异步FIFO

        异步 FIFO 有两个时钟信号,读和写接口分别采用不同时钟,这两个时钟可能时钟频率不同,也可能时钟相位不同,可能是同源时钟,也可能是不同源时钟。 在现代逻辑设计中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,多时钟域带来的一个问题就是,如何设计异步时钟之间的接口电路。异步 FIFO 是这个问题的一种简便、快捷的解决方案,使用异 步 FIFO 可以在两个不同时钟系统之间快速而方便地传输实时数据。

2、实现异步FIFO需要解决的关键点

        首先我们直到FIFO的设计要点是:读空信号如何产生?写满信号如何产生?

        在同步FIFO的设计中,我们提出了两种方法:计数器法和拓展高位指针的方法。那么这两种方法是否也适合异步FIFO的设计?

        首先来看计数器法:计数器法的关键是使用一个计数器来指示FIFO中数据的个数,然后根据FIFO进行的读写操作来增加、减少或不变。在异步FIFO(读写时钟可能相同,也可能不同,我们这里只讨论不同的情况)中,读写时钟不一致,那么这个计数器是属于哪个时钟的?或者说计数器在哪个时钟增加或者减少?显然这个问题不好解决。

        那么接下来看第二个方法--高位扩展法:将地址指针高位扩展1位,来作为指示位。通过对比读写指针的值(也就是比对读写指针的位置)来判断读空或者写满。乍一看这种方法好像行得通,当然了,实际上也行得通。只不过我们需要解决读写指针的跨时钟域问题。

        异步FIFO的读写时钟不一致的话也就说明读、写指针是不同时钟域的信号,那么肯定无法直接对比。这里多说一句:可能有的朋友会说直接对比读写指针的值不就行了吗?干嘛还要做跨同步域处理呢(我是真的见过有人这么问)。首先我们需要直到的是读写指针是一个信号,而信号的值在这里是相对时域来说的。打个比方:在10ns的时候,读指针是4;到了20ns,读指针就变成了8。同样的,写指针的值也是相对于时域来说的。既然两个指针相对的时域不一致,那么如何能直接对比?

2.1、读写指针的跨时钟域问题

        现在我们直到了,要比较读写指针的值需要将其同步到同一时钟域。那么这个时钟域是写时钟域、读时钟域或者是第三方时钟域?

        第三方时钟域:不难知道一个信号从一个时钟域同步到另一个时钟域(被同步时钟域)是需要时间的(这里仅考虑从满到快,也就是暂时不考虑漏采的问题),需要的时间取决于被同步时钟域的周期以及需要同步的个数。假设这个时间是T,那么经过T时间后,由于读写时钟不一致,原来的读写时针增加(也可能不变)的量是不一致。比如说实际上读写时针都指向4(且最高位相同),那么这种情况实际上是出现了读空的情况。但是同步到第三方时钟域后,可能写指针成了6,而读指针变成了8(读时钟比写时钟快),那么在这种情况下FIFO就不会报“读空”,从而造成功能错乱。所以该种方法不可取。

        同步到写时钟域读指针同步到写时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。

                现在来进行写满的判断:也就是写指针超过了同步后的读指针一圈。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是“假写满”。那么“假写满”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设一个深度为100的FIFO,在写到第98个数据的时候就报了“写满”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了FIFO的深度我少用一点点就是的。事实上这还可以算是某种程度上的保守设计(安全)。

                接着进行读空的判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了“读空”,却仍然有错误数据读出。所以这种情况就造成了FIFO的功能错误。

        同步到读时钟域: 写指针同步到读时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。

                现在来进行写满的判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了“写满”,却仍然数据被覆盖写入。所以这种情况就造成了FIFO的功能错误。

                接着进行读空的判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是“假读空”。那么“假读空”是一种错误的设计吗?答案是NO。前面我们说过异步FIFO设计的关键点是产生合适的“写满”和“读空”信号,那么何谓“合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设某个FIFO,在读到还剩2个数据的时候就报了“读空”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计(安全)。

        现在我们可以总结一下:

  • “写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
  • “读空”的判断:需要将写指针同步到读时钟域,再与读指针判断

        假读空示意如下:

        假写满示意如下:

2.2、二进制码与格雷码

        现在我们知道了判断FIFO的空、满需要将读写指针分别同步,而跨时钟域传输的一旦没处理好就会引起亚稳态问题,造成指针的值异常,从而引发FIFO的功能错误。那么应该如何将读写指针同步到对方的时钟域呢?答案是将二进制的指针转化成格雷码后再进行同步。

        格雷码是一种非权重码,每次变化位数只有一位,这就有效的避免了在跨时钟域情况下亚稳态问题发生的概率。举个例子,二进制的7(0111)跳转到8(1000),4位都会发生变化,所以发生亚稳态的概率就比较大。而格雷码的跳转就只有一位(从0100--1100,仅第四位发生变化)会发生变化,有效地减小亚稳态发生的可能性。

        有关格雷码的介绍可参考:Verilog实现的格雷码与二进制码的互相转换

        四位二进制码从0111变为1000的过程中,这两个数虽然在数值上相邻,但它们的每个比特都将发生改变,采样的值就可能是任意的四位二进制数(发生亚稳态的情况),这会给空满标志的判断带来问题,如果错误触发空满标志还好,但如果在空满成立时没有触发,就会导致数据被覆盖掉或者重复读出;

        如果使用格雷码,每次只改变一位信号,就不会出现上述的问题。例如,格雷码从0001递增到0011时,即便没有采集到变化后的0011,也会采集到变化之前的0001,这只会导致“不该报空满而报了空满”,但并不会导致“该报空满而未报”的情况。详细来说,如果是读指针从0001递增到0011,假设写时钟域采到的是0001,那么也只是会报写满(因为写时钟域不知道读时钟域已经读到下一个地址了),从而停止写入,这是安全的;同理,如果是写指针从0001递增到0011,假设读时钟域采到的是0001,那么也只是会报读空(因为读时钟域不知道写时钟域已经写到下一个地址了),从而停止读出,这也是安全的。

        如何用格雷码判断空满?

        首先我们需要将指针向高位拓展一位,这是为了判断写指针是否超过读指针一圈。然后通过对比除了最高位的其余位来判断读写指针是否重合。这种方法判断二进制的指针是没有问题的,但是这不适合格雷码形式的指针,因为格雷码是镜像对称的,若只根据最高位是否相同来区分是读空还是写满是有问题的,如下图:

        图中是0--15的格雷码,7-0和8-15的格雷码除了最高位是关于中线镜像对称的:

  • 7 -- 8 ,格雷码 0100 -- 1100 ,只有最高位发生变化其余位相同
  • 6 -- 9  ,  格雷码 0101 -- 1101 , 只有最高位发生变化其余位相同
  • ····
  • 0--15, 格雷码 0000 -- 1000 , 只有最高位发生变化其余位相同

        假如仅仅通过判断读写指针除了最高位的余下位的话,那么势必会出现判断错误而引发误报的写满和读空信号。举例:读指针指向0,写指针指向15。0的格雷码与15的格雷码的最高位不同,其余位相同,所以判断出为写满--这就出现误判了。

        因此用格雷码判断是否为读空或写满时应使用理论 2,看最高位和次高位是否相等,具体如下:

  • 当最高位和次高位相同,其余位相同认为是读空
  • 当最高位和次高位不同,其余位相同认为是写满

        当然还有一种办法就是将同步后的格雷码再转换成二进制码进行比较。

        快时钟域的信号同步到慢时钟域造成的漏采   

        快时钟踩慢时钟可以直接采(打拍)这没问题,但是快时钟信号同步到慢时钟域却有可能发生漏踩的问题(在单bit的应用中需要展宽快时钟以便能被慢时钟采集到)。那么造成的漏采问题怎么解决?答案是不需要解决。

        读慢写快:

                进行写满判断的时候需要将读指针同步到写时钟域,因为读慢写快,所以不会有读指针遗漏,同步消耗时钟周期,所以同步后的读指针滞后(小于等于)当前读地址,所以可能写满会提前产生,并非真写满。

                进行读空判断的时候需要将写指针同步到读指针 ,因为读慢写快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,我们不用关心那到底会漏掉哪些写指针,我们在乎的是漏掉的指针会对FIFO的读空产生影响吗?比如写指针从0写到10,期间读时钟域只同步捕捉到了3、5、8这三个写指针而漏掉了其他指针。当同步到8这个写指针时,真实的写指针可能已经写到10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到FIFO去,这样在判断它是不是空的时候会出现不是真正空的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。

        读快写慢:

                进行读空判断的时候需要将写指针同步到读指针 ,因为读快写慢,所以不会有写指针遗漏,同步消耗时钟周期,所以同步后的写指针滞后(小于等于)当前写地址,所以可能读空会提前产生,并非真读空。

                进行写满判断的时候需要将读指针同步到写时钟域,因为读快写慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,我们不用关心那到底会漏掉哪些读指针,我们在乎的是漏掉的指针会对FIFO的写满产生影响吗?比如读指针从0读到10,期间写时钟域只同步捕捉到了3、5、8这三个读指针而漏掉了其他指针。当同步到8这个读指针时,真实的读指针可能已经读到10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能从FIFO读了数据出来,这样在判断它是不是满的时候会出现不是真正满的情况,漏掉的指针也没有对FIFO的逻辑操作产生影响。

3、Verilog实现

        根据以上可以设计异步FIFO的实现:

  • 分别构造读、写时钟域下的读、写指针,指针位数需拓展一位。举例,设计的FIFO深度为16,16个地址需要4位二进制数表示,同时扩宽一位作为指示位,所以指针的位宽共需要5位。
  • 分别将读、写指针从二进制码转换成格雷码
  • 将格雷码形式的读指针同步到写时钟域;将格雷码形式的写指针同步到读时钟域
  • 在写时钟域判断“写满”:格雷码形式的读写指针高2位相反,其余位相等
  • 在读时钟域判断“读空”:格雷码形式的读写指针高2位相等,其余位也相等--即全部相等
//异步FIFO
module	async_fifo
#(
	parameter   DATA_WIDTH = 'd8  ,								//FIFO位宽
    parameter   DATA_DEPTH = 'd16 								//FIFO深度
)		
(		
//写数据		
	input							wr_clk		,				//写时钟
	input							wr_rst_n	,       		//低电平有效的写复位信号
	input							wr_en		,       		//写使能信号,高电平有效	
	input	[DATA_WIDTH-1:0]		data_in		,       		//写入的数据
//读数据			
	input							rd_clk		,				//读时钟
	input							rd_rst_n	,       		//低电平有效的读复位信号
	input							rd_en		,				//读使能信号,高电平有效						                                        
	output	reg	[DATA_WIDTH-1:0]	data_out	,				//输出的数据
//状态标志					
	output							empty		,				//空标志,高电平表示当前FIFO已被写满
	output							full		    			//满标志,高电平表示当前FIFO已被读空
);                                                              

//reg define
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0]			fifo_buffer[DATA_DEPTH - 1 : 0];
	
reg [$clog2(DATA_DEPTH) : 0]		wr_ptr;						//写地址指针,二进制
reg [$clog2(DATA_DEPTH) : 0]		rd_ptr;						//读地址指针,二进制
reg	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d1;				//读指针格雷码在写时钟域下同步1拍
reg	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d2;				//读指针格雷码在写时钟域下同步2拍
reg	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d1;				//写指针格雷码在读时钟域下同步1拍
reg	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d2;				//写指针格雷码在读时钟域下同步2拍
	
//wire define
wire [$clog2(DATA_DEPTH) : 0]		wr_ptr_g;					//写地址指针,格雷码
wire [$clog2(DATA_DEPTH) : 0]		rd_ptr_g;					//读地址指针,格雷码
wire [$clog2(DATA_DEPTH) - 1 : 0]	wr_ptr_true;				//真实写地址指针,作为写ram的地址
wire [$clog2(DATA_DEPTH) - 1 : 0]	rd_ptr_true;				//真实读地址指针,作为读ram的地址

//地址指针从二进制转换成格雷码
assign 	wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);					
assign 	rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);

//读写RAM地址赋值
assign	wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//写RAM地址等于写指针的低DATA_DEPTH位(去除最高位)
assign	rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//读RAM地址等于读指针的低DATA_DEPTH位(去除最高位)


//写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)
		wr_ptr <= 0;
	else if (!full && wr_en)begin								//写使能有效且非满
		wr_ptr <= wr_ptr + 1'd1;
		fifo_buffer[wr_ptr_true] <= data_in;
	end	
end
//将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)begin
		rd_ptr_g_d1 <= 0;										//寄存1拍
		rd_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		rd_ptr_g_d1 <= rd_ptr_g;								//寄存1拍
		rd_ptr_g_d2 <= rd_ptr_g_d1;								//寄存2拍
	end	
end

//读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)
		rd_ptr <= 'd0;
	else if (rd_en && !empty)begin								//读使能有效且非空
		data_out <= fifo_buffer[rd_ptr_true];
		rd_ptr <= rd_ptr + 1'd1;
	end
end
//将写指针的格雷码同步到读时钟域,来判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)begin
		wr_ptr_g_d1 <= 0;										//寄存1拍
		wr_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		wr_ptr_g_d1 <= wr_ptr_g;								//寄存1拍
		wr_ptr_g_d2 <= wr_ptr_g_d1;								//寄存2拍		
	end	
end

//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;

//当高位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满
//同步后的读指针格雷码高两位取反,再拼接上余下位
assign	full  = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
				,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;

endmodule

4、Testbench及仿真结果

        接下来编写脚本对源码进行测试:

  • 例化1个深度为8,位宽为8的异步FIFO;读时钟是写时钟的2倍,即读快写慢
  • 先对FIFO进行写操作,直到其写满,写入的数据为随机数据
  • 然后对FIFO进行读操作,直到其读空
  • 然后对FIFO写入4个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns	//时间单位/精度

//------------<模块及端口声明>----------------------------------------
module tb_async_fifo();

parameter   DATA_WIDTH = 8  ;		//FIFO位宽
parameter   DATA_DEPTH = 8 ;		//FIFO深度

reg							wr_clk		;				//写时钟
reg							wr_rst_n	;       		//低电平有效的写复位信号
reg							wr_en		;       		//写使能信号,高电平有效	
reg	[DATA_WIDTH-1:0]		data_in		;       		//写入的数据

reg							rd_clk		;				//读时钟
reg							rd_rst_n	;       		//低电平有效的读复位信号
reg							rd_en		;				//读使能信号,高电平有效						                                        
wire[DATA_WIDTH-1:0]		data_out	;				//输出的数据					
wire						empty		;				//空标志,高电平表示当前FIFO已被写满
wire						full		;               //满标志,高电平表示当前FIFO已被读空

//------------<例化被测试模块>----------------------------------------
async_fifo
#(
	.DATA_WIDTH	(DATA_WIDTH),			//FIFO位宽
    .DATA_DEPTH	(DATA_DEPTH)			//FIFO深度
)
async_fifo_inst(
	.wr_clk		(wr_clk		),
	.wr_rst_n	(wr_rst_n	),
	.wr_en		(wr_en		),
	.data_in	(data_in	),	
	.rd_clk		(rd_clk		),               
	.rd_rst_n	(rd_rst_n	),	
	.rd_en		(rd_en		),	
	.data_out	(data_out	),
	
	.empty		(empty		),		
	.full		(full		)
);

//------------<设置初始测试条件>----------------------------------------
initial begin
	rd_clk = 1'b0;					//初始时钟为0
	wr_clk = 1'b0;					//初始时钟为0
	wr_rst_n <= 1'b0;				//初始复位
	rd_rst_n <= 1'b0;				//初始复位
	wr_en <= 1'b0;
	rd_en <= 1'b0;	
	data_in <= 'd0;
	#5
	wr_rst_n <= 1'b1;				
	rd_rst_n <= 1'b1;					
//重复8次写操作,让FIFO写满 	
	repeat(8) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
		end
	end
//拉低写使能	
	@(negedge wr_clk)	wr_en <= 1'b0;
	
//重复8次读操作,让FIFO读空 	
	repeat(8) begin
		@(negedge rd_clk)rd_en <= 1'd1;		
	end
//拉低读使能
	@(negedge rd_clk)rd_en <= 1'd0;		
//重复4次写操作,写入4个随机数据	
	repeat(4) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
		end
	end
//持续同时对FIFO读
	@(negedge rd_clk)rd_en <= 1'b1;
//持续同时对FIFO写,写入数据为随机数据	
	forever begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
		end
	end	

end

//------------<设置时钟>----------------------------------------------
always #10 rd_clk = ~rd_clk;			//读时钟周期20ns
always #20 wr_clk = ~wr_clk;			//写时钟周期40ns

endmodule

        仿真结果如下:

         

        仿真过程分为4个阶段:

  1. 一直写直到写满(共8个随机数据)
  2. 一直读直到读空
  3. 仅写4个数据
  4. 同时读写

        阶段1、2的局部图如下:

        上图中,先是往FIFO中写入了8个随机数据后拉高了写满信号,然后连续读取8个数据后,拉高了读空信号,且读出的数据域写入的数据一致

        阶段3、4的局部图如下:

        上图中,先是往FIFO中写入了4个随机数据后,然后连续同时读、写,由于读时钟是写时钟的2倍,所以在一段时间后每隔1个读时钟周期,就会拉高了读空一次,读出的数据域写入的数据一致。可以看到在第一次读空出现时,此时读出的数据时ed,而在写时钟域此时已经将数据8c写到FIFO里了,就就是说此时出现的是假“读空”,假读空不会造成功能错误,只会造成性能损失。

5、总结

  • 此文实现了同位宽的异步FIFO的设计及分析验证,下一篇文章再介绍不同的位宽的异步FIFO的设计
  • 如果需要完整的工程文件请点这里:工程文件下载

  • 📣您有任何问题,都可以在评论区和我交流📃!
  • 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net
  • 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐