用verilog/systemverilog 设计fifo (2)

异步fifo实现中要解决的问题

异步fifo和同步fifo功能相似,但是它的读写由两个时钟信号控制,所以它的设计和同步fifo不同,需要考虑更多的因素。

信号同步到那个时钟域

我们知道,写fifo和写地址更新肯定在写时钟域,也就是在wr_clk的时钟上升沿用以下代码进行更新。

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	

同理,读fifo和读地址更新在读时钟域,也就是在rd_clk的时钟上升沿用以下代码进行更新。

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

但是fifo为空和为满的判断要同时用到读地址和写地址,这个时候应该在写时钟域还是读时钟域来做这个判断呢?如果在写时钟域判断,读地址必须同步到写时钟域,如果在读时钟域判断,写地址必须同步到读时钟域。

  • 同步到写时钟域
    读指针同步到写时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。
    • 现在来进行写满的判断:也就是写指针超过了同步后的读指针一圈。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是“假写满”。“假写满”不会造成功能错误,只会造成性能损失,大不了FIFO的深度我少用一些。事实上这还可以算是某种程度上的保守设计。
    • 接着进行读空的判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了“读空”,却仍然有错误数据读出。所以这种情况会造成FIFO的功能错误。
  • 同步到读时钟域
    写指针同步到读时钟域需要时间T,在经过T时间后,可能原来的读指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。
    • 现在来进行写满的判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了“写满”,却仍然数据被覆盖写入。所以这种情况就造成了FIFO的功能错误。
    • 接着进行读空的判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是“假读空”。“假读空”不会造成功能错误,只会造成性能损失,大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计。

综合起来就是:
“写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
“读空”的判断:需要将写指针同步到读时钟域,再与读指针判断

读写指针转化为格雷码

跨时钟域传输的一旦没处理好就会引起亚稳态问题,造成读写地址的值异常,从而引发FIFO的功能错误。那么应该如何将读写指针同步到对方的时钟域呢?
将二进制的读写地址转化成格雷码后再进行同步可以有效减少亚稳态问题。格雷码转化verilog实现可以参考
读写二进制地址转化为格雷码

格雷码表示的读写地址如何判断空满?

二进制表示的读写地址,我们可以扩展一位地址,用扩展的高位和其余位地址来比较。

  • 读写地址高位相同,其它位也相同,则fifo为空。
  • 读写地址高位不同,其它位相同,则fifo为满。

但格雷码的特点决定我们不能用这种方法。

10进制数 2进制数 典型格雷码
0 4'b0000 4'b0000
1 4'b0001 4'b0001
2 4'b0010 4'b0011
3 4'b0011 4'b0010
4 4'b0100 4'b0110
5 4'b0101 4'b0111
6 4'b0110 4'b0101
7 4'b0111 4'b0100
8 4'b1000 4'b1100
9 4'b1001 4'b1101
10 4'b1010 4'b1111
11 4'b1011 4'b1110
12 4'b1100 4'b1010
13 4'b1101 4'b1011
14 4'b1110 4'b1001
15 4'b1111 4'b1000

比如下面图中,左边是空,写地址和读地址相等,无论二进制码和格雷码都是如此。
右边是满,写地址绕了一圈回来,其值是11,其对应的格雷码是4'b1110, 读地址3对应的格雷码是4'b0010,可见其高两位相反,其余位相同。

总结起来用格雷码判断空满的方法就是:

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

异步fifo verilog代码

文件名称: code4_43.v

`timescale 1ns/1ns	
 
module async_fifo_tb;
  
	logic	wr_clk;				
	logic	wr_rst_n;       		
	logic	wr_en;       		
	logic	[7:0] data_in;       	
 
	logic	rd_clk;			
	logic	rd_rst_n;       	
	logic	rd_en;						                                        
	logic  [7:0] data_out;			
	logic   empty;	
	logic   full;  

	initial begin

    	$display("start a clock pulse");
    	$dumpfile("async_fifo.vcd"); 
    	$dumpvars(0, async_fifo_tb); 
   		#600 $finish;
	end
 

	async_fifo
		#(
			.DATA_WIDTH	(8),			//FIFO位宽
    		.DATA_DEPTH	(8)			//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;					
//重复10次写操作
	repeat(10) begin
		@(negedge wr_clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
		end
	end
//拉低写使能	
	@(negedge wr_clk)	wr_en <= 1'b0;
	
//重复6次读操作,让FIFO读空 
	repeat(6) begin
		@(negedge rd_clk) rd_en <= 1'd1;		
	end
//拉低读使能
	@(negedge rd_clk) rd_en <= 1'd0;		
//再写2次
	repeat(2) 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 #5 rd_clk = ~rd_clk;		

always #10 wr_clk = ~wr_clk;			
 
endmodule

//异步FIFO
module	async_fifo
#(
	parameter   DATA_WIDTH = 'd8,								
    parameter   DATA_DEPTH = 'd16							
)		
(		
//写数据		
	input	wire wr_clk,				
	input	wire wr_rst_n, 
	input	wire wr_en, 
	input	[DATA_WIDTH-1:0]		data_in,  
//读数据			
	input	wire rd_clk,
	input	wire rd_rst_n, 
	input	wire rd_en,				                                        
	output	logic	[DATA_WIDTH-1:0]	data_out,
//状态标志					
	output	logic	empty,	
	output	logic	full	
);                                                              
 

logic [DATA_WIDTH - 1 : 0]			fifo_buffer[DATA_DEPTH - 1 : 0];
	
logic [$clog2(DATA_DEPTH) : 0]		wr_ptr;		
logic [$clog2(DATA_DEPTH) : 0]		rd_ptr;	
logic	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d1;				//读指针格雷码在写时钟域下同步1拍
logic	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d2;				//读指针格雷码在写时钟域下同步2拍
logic	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d1;				//写指针格雷码在读时钟域下同步1拍
logic	[$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

在vscode中,使用下面命令编译,运行代码,然后用gtkwave 打开波形文件:

iverilog -o myrun -g 2012 -s TestMem code4_43.v
vvp myrun
gtkwave async_fifo.vcd

  • 在wr_clk时钟域,从第2个时钟上升沿到第9个时钟上升沿,写入8个数据,fifo full
  • 在rd_clk时钟域,从第24个时钟上升沿到29个时钟上升沿,读取8个数据
  • 在wr_clk时钟域,从第16个时钟上升沿开始,在每个时钟上升沿写fifo
  • 在rd_clk时钟域,从第34个时钟上升沿开始,在每个时钟上升沿读fifo,由于rd_clk更快,所以它很快就会读空fifo,然后写时钟域每写一个,读时钟域就读一个。

热门相关:大爱晚成,卯上天价老婆   农家小福女   特工皇后不好惹   逃妻鲜嫩嫩:霸道老公,吻够没   大唐扫把星