异步FIFO设计方法(Verilog)

异步FIFO设计方法(Verilog)

  • 1、什么是异步FIFO
    • 1.1、异步FIFO接口
  • 2、设计异步FIFO重难点
    • 2.1、空满状态的判断
      • 2.1.1、读写指针同步到哪个时钟域中
      • 2.1.2、格雷码的使用
    • 2.2、空满判断
  • 3、Verilog实现
  • 4、仿真结果分析

1、什么是异步FIFO

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

1.1、异步FIFO接口

接口名称 释义
wr_clk 写数据时钟
rd_clk 读数据时钟
wr_rst 写复位
rd_rst 读复位
wr_en 写数据使能信号,高电平有效
din 数据输入
rd_en 读数据使能信号,高电平有效
dout 数据输出
full 写满信号,高电平有效
empty 读空信号,高电平有效

2、设计异步FIFO重难点

   异步FIFO设计的难点在于:空信号和满信号如何产生?
   在同步FIFO设计中,我们使用了计数器对FIFO内部的数据进行计数,用于空满信号的判断,这种方法是基于读写时钟一致的情况下,但是在读写时钟不一致的情况下,采用计数器计数,究竟是采用读时钟进行计数还是使用写时钟进行计数?答案是,谁的时钟都不能用,因为读写时钟不一致的情况下,其计数器计数是不准确的,同时也会造成亚稳态的产生,从而导致设计的异步FIFO无法使用。
   因此,我们想到,使用高位扩展法来设计。那么什么是高位扩展法呢?高位扩展法就是在高位扩展以为宽数据。举例在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3’b000-3’b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4’b0000-4’b1111。假设不看最高位的话,后面3位的表示区间仍然是3’b000-3’b111,也就意味着最高位可以拿来作为指示位。

2.1、空满状态的判断

   读指针在读电路中维持着一个指针地址,写指针在写电路中维持着一个指针地址,简单而言,将这两个指针地址进行比较,就能得出FIFO当前状态是空还是满,但是,这牵扯到跨时钟域的问题,在进行读写指针的地址比较之前,还需要了解一下关键点:

2.1.1、读写指针同步到哪个时钟域中

   显而易见,如果要判断是否写满,就需要将读指针同步到写时钟域中与读指针进行比较判断,为什么判断写满状态需要将读指针同步过来呢?因为写满状态是写电路所关心的问题,判断是否写满当然要在写时钟域下进行判断。同理,如果判断是否读空,需要将写指针同步到读时钟域下与读指针进行比较判断。
   现在总结一下:

  • 判断写满状态,将读指针同步到写时钟域;
  • 判断读空状态,将写指针同步到读时钟域;

2.1.2、格雷码的使用

   在异步FIFO设计中,还是因为时钟域的问题,我们不得不使用格雷码来表示读写指针,为什么要将读写指针的指针地址转换成格雷码呢?举个简单的例子,当写指针由7到8的递增时,其二进制码由4’b0111向4’b1000变化,这样同步到读时钟域时需要变化四位,这样会导致亚稳态的发生,如果发生了亚稳态问题,那么势必会造成写指针的不确定性,进而导致FIFO设计失败。
   而格雷码面对数据变化每次只有其中某一位发生变化,这将大大减小了亚稳态发生的概率。比如写指针由7跳变为8时,其格雷码从0100跳转为1100,只有最高位发生了跳变。下图为十进制数与格雷码的对照。
异步FIFO设计方法(Verilog)_第1张图片

2.2、空满判断

   那么如何判断空满状态呢?
   前面介绍了格雷码,我们当然使用格雷码作为读写的指针地址参与空满状态的判断。
   首先判断读空信号。读空信号的判断很简单,就是将同步到读时钟域的写指针地址与读指针地址进行比较,读写指针地址重合,完全相同就是读空状态,只要有一位不同就是不空,即:相同为空
   再判断写满信号。我们仔细观察格雷码,发现4位的格雷码的0-7和8-15除了最高位不同外,其余成镜像对称关系,也就关于红线对称;所以我们就不能仅仅依靠判断最高位来判断写满了。在这举个例子:假设一个深度位8的异步FIFO,其内部的RAM地址位0~7,使用高位拓展法将最高位拓展一位,那么读写指针的格雷码就有4位宽;在某一时刻读指针指向5(格雷码为0111),此时此刻写指针指向6(格雷码为0101),这种情况说明地址0-4中的数据均已读出,那么FIFO写满的情况是写指针地址加上8,由于高位拓展了一位,所以写指针地址指向13,即图中虚线的箭头处,其格雷码为1011,我们可以发现,此时写指针地址和读指针地址除了最高位和次高位不同外,其余为均相同,因此我们可以得到写满的判断条件。即:读指针地址的最高位和写指针地址的最高位不同,读指针地址的次高位和写指针地址的次高位不同,其余位相同
异步FIFO设计方法(Verilog)_第2张图片

3、Verilog实现

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

  • 分别构造读、写时钟域下的读、写指针,指针位数需拓展一位。举例,设计的FIFO深度为16,16个地址需要4位二进制数表示,同时扩宽一位作为指示位,所以指针的位宽共需要5位。
  • 分别将读、写指针从二进制码转换成格雷码
  • 将格雷码形式的读指针同步到写时钟域;将格雷码形式的写指针同步到读时钟域
  • 在写时钟域判断“写满”:格雷码形式的读写指针高2位相反,其余位相等
  • 在读时钟域判断“读空”:格雷码形式的读写指针高2位相等,其余位也相等–即全部相等
// An highlighted block
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、仿真结果分析

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

  • 例化1个深度为8,位宽为8的异步FIFO;读时钟是写时钟的2倍,即读快写慢
  • 先对FIFO进行写操作,直到其写满,写入的数据为随机数据
  • 然后对FIFO进行读操作,直到其读空
  • 然后对FIFO写入4个随机数据后,同时对其进行读写操作
    异步FIFO设计方法(Verilog)_第3张图片
    注:本设计参考了各位大佬的博客之后做的笔记,仅供学习使用,如遇侵权,联系我速删。

你可能感兴趣的:(fpga开发)