异步FIFO的设计 verilog

文章目录

  • 一、 结构框图
    • 1.1 整体结构
    • 1.2 整体结构分解
  • 二、FIFO读空写满判断
    • 2.1 使用二进制码来判断读空写满的方法
    • 2.2 使用格雷码来判断读空写满的方法
  • 三、格雷码的使用
    • 3.1 为什么二进制指针不适合做空满判断?
    • 3.2 怎么解决二进制带来的亚稳态问题?
    • 3.3 二进制数转格雷码
  • 四、跨时钟域读写指针同步
  • 五、常见问题
  • 六、源码
    • 6.1 异步FIFO源码
    • 6.2 测试testbench
    • 6.3 仿真波形
  • 七、最后

异步FIFO的基础是一个双时钟双端口RAM,读端口和写端口的时钟不一致。
FIFO的重点是空、满信号的判断,需要对读和写地址进行比较。而异步FIFO的读时钟域与写时钟域不同,因此异步FIFO的设计重点是跨时钟域的地址同步问题。因为只有在同一时钟频率下才能进行地址的比较。
跨时钟域地址同步:格雷码+打两拍同步

一、 结构框图

1.1 整体结构

异步FIFO的设计 verilog_第1张图片

1.2 整体结构分解

异步FIFO的设计 verilog_第2张图片

由上图可知,异步FIFO可以分为四部分:

  1. RAM存储区域构建;
  2. 读地址同步到写时钟域,进行写满判断;
  3. 写地址同步到读时钟域,进行读空判断;
  4. 跨时钟域地址指针同步。

二、FIFO读空写满判断

异步FIFO的设计 verilog_第3张图片

使用一位扩展位来指明读或写过的次数,进而可以通过比较地址就可以得到FIFO是否写满或者读空。
异步FIFO的设计 verilog_第4张图片

2.1 使用二进制码来判断读空写满的方法

读空:最高位和其余位相同。
写满:最高位不同,其余位相同。

2.2 使用格雷码来判断读空写满的方法

读空:最高位和其余位相同。
写满:最高位、次高位均不同,其余位相同。

三、格雷码的使用

3.1 为什么二进制指针不适合做空满判断?

二进制读指针在增减时,经常发生多位突变,比如六位地址111111会在下一时刻变成000000,在实际电路这,这个变化过程要持续很长一段时间,会由111111经历6个状态转移到000000,比如111111>101111>100111>100100>000100>000000。由于写时钟与读时钟域不同步​==异步的写时钟很可能会在状态不稳定的中间某个状态采样,这样就会得到错误的读地址指针,进而做出错误的状态判断,导致系统异常(亚稳态问题)==。而且由于多位同时突变,凭借概率论常识可知发生错误的可能性很大。**

3.2 怎么解决二进制带来的亚稳态问题?

显然,在中间状态采样,这个是不可避免的,这是异步系统天生的缺陷。我们的目标是:即使在中间态采样,也不能影响空满状态的判断。符合这个要求的编码方式是:每次只能有1bit发生改变。

为什么这么说?因为当只有一个bit发生改变时,即使在中间状态采样,其结果也不外乎两种:递增前原指针和递增后新指针。显然递增后新指针是最新情况的反映,如果采样到这个指针,那么和我们的设计预期是一致的。如果采样到递增前原指针,会有什么结果呢?假设现在写时钟域采样读指针,那么最坏的情况是把“不满”判断成了“满”,使得本来被允许的写操作被禁止了(虚满),但是这并不会对逻辑产生影响,只是带来了写操作的延迟。同样的,如果现在**读时钟域****采样写指针,那么最坏的情况就是把“不空”判断成“空”(虚空),使得本来被允许的读操作被禁止了,**但是这不会对逻辑产生影响,只是带来了读操作的延迟。

显然每次只变化1bit的编码方案可以有效解决中间态下空满状态的判断问题,格雷码就是这样一种编码。

采用格雷码编码(解决汇聚问题),因为格雷码每次跳转只会有一位发生变化,所以如果出现不确定状态也只会有两种状况,即正确变化了不变。因此在读写时钟不一样的情况下,纵使读写地址每bit同步过程中出现延时不一致,也不会使得FIFO在实际空或者满之后,FIFO却没有正确的产生出空满信号只有可能是实际没有空或者满,但产生了空满信号,但这对于FIFO的功能不会有影响,只会使得FIFO的读或者写操作暂停。
由此可见,异步FIFO通过一下两个途径来解决FIFO 结构中固有的异步时钟亚稳态现象:

  • 以格雷码编码表示读、写指针,用格雷码加法器来实现读/写地址的加一动作。**
  • 用同步器将同步读写地址到对应的时钟域,来产生空满信号。**

3.3 二进制数转格雷码

最高位不变,二进制的第n位和第n+1位异或作为格雷码的第n位。
异步FIFO的设计 verilog_第5张图片
异步FIFO的设计 verilog_第6张图片

四、跨时钟域读写指针同步

读空信号的产生:写时钟域的写地址指针二进制指针先转为格雷码,之后经过2级寄存器之后与读时钟域的读地址指针进行比较,来判断是否读空。

写满信号的产生:读时钟域的读地址指针二进制指针先转为格雷码,之后经过2级寄存器之后与写时钟域的写地址指针进行比较,来判断是否写满。

同步的方法:利用格雷码+2级寄存器打两拍,之后发生亚稳态的概率会很小。 寄存器同步消耗了时间,这个时间对功能没有影响,对性能有影响。(判断的满并非真满,判断的空并非真空,会浪费FIFO存储空间,但保证不溢出)

为什么不用握手机制来同步读写指针到对应的时钟域?因为握手中反馈需要跨过两次时钟域,对于效率很有影响。比如说push这一侧要等到反馈信号回来之后才能继续下一个push,哪怕FIFO里面还有很多空闲的单元。pop的这一侧也是一样。这样对于FIFO的整体性能影响太大。

五、常见问题

  1. 慢时钟域同步快时钟域格雷码时候,在慢时钟域的一个周期中,经历了两次或多次快时钟域的上升沿,那么对应的格雷码就会有两个或多个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的空满判断正确性无影响,只会产生虚空,虚满判断。
    “知其然而不知其所以然!”

  2. 如何判断FIFO是真空/真满呢?

    回答:判断假空假满刚好相反,在push side我们来判断空,在pop side来判断满?

  3. 设计一个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。源码和测试激励如下。

6.1 异步FIFO源码

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

6.2 测试testbench

`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

6.3 仿真波形

  1. 由下图可以看到,当写入256个数后(从0开始),写满信号full自动拉高。
    异步FIFO的设计 verilog_第7张图片

  2. 由下图可以看到,当读出256个数后(从0开始),读空信号empty自动拉高。

异步FIFO的设计 verilog_第8张图片

七、最后

参考:https://mp.weixin.qq.com/s/TR_5imTfUI2-LGbPOE7OkA

可能本设计会存在的有一些bug,目前我还没有发现!希望大家对我的设计多多提出批评!

你可能感兴趣的:(数字IC设计,硬件工程,fpga开发)