异步FIFO详解

在学习科研的闲暇之余,打算记录一些FPGA基础知识,方便后续自己的查阅复习,如果能帮助到其他同学那就更好了。本文主要介绍异步FIFO的实现原理,内容如有不妥之处,欢迎评论区指正,共同学习进步。

1、FIFO概述

FIFO(First In First Out)是一种先进先出的数据存储、缓存器。我们知道一般的存储器是用外部的读写地址来进行读写,而FIFO这种存储器的结构并不需要外部的读写地址而是通过内部的读写指针自动的加一操作来控制读写,这也就决定了FIFO只能顺序的读写数据。FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。

FIFO 有两种类型:同步 FIFO 和异步 FIFO。同步 FIFO 使用同一个时钟信号来控制数据的读写,而异步 FIFO 有两个时钟信号,一个用于读操作,另一个用于写操作。这两个时钟可能时钟频率不同,也可能时钟相位不同,可能是同源时钟,也可能是不同源时钟。异步 FIFO 实质上也是基于中间的双口RAM,外加一些读写控制电路组成的。

同步 FIFO 的作用一般是做一个数据缓冲,也就是一个 buffer。异步 FIFO 有两个较为重要的作用:一个是实现数据在不同时钟域进行传递,另一个作用就是实现不同数据宽度的数据接口。

2、异步FIFO的实现

和同步 FIFO 一样,对于异步 FIFO 的空满信号的产生,也能通过写读指针的比较来实现。同样也是在读指针追上写指针时,FIFO 为空;在写指针追上读指针时,FIFO 为满。但是异步 FIFO 的读写指针是由两个时钟控制的,时钟不同步,无法直接比较,只能将读时钟域的读指针转换到写时钟域,与写指针进行比较,判断 FIFO 是否为满;将写时钟域的写指针转换到读时钟域,与读指针进行比较,判断 FIFO 是否为空。

以上操作将会涉及到跨时钟域问题,而跨时钟域传输的一旦没处理好就会引起亚稳态问题。亚稳态不可完全避免,只能通过一些手段如:引入同步机制(打2拍)以及格雷码等来降低亚稳态出现的机率。

2.1、同步到写时钟域

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

当我们认为写指针追上了同步后的读指针时,实际上读时钟域的读指针是大于等于同步后的读指针的,所以这个时候写指针不一定刚好追上读指针(可能还差一点点),也就是说这种情况是“假写满”。那么“假写满”是不会造成功能错误,只会造成性能损失,事实上这还可以算是某种程度上的保守设计(安全)。

2.2、同步到读时钟域

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

当我们认为读指针追上了同步后的写指针时,实际上写时钟域的写指针是大于等于同步后的写指针的,所以这个时候读指针不一定刚好追上写指针(可能还差一点点),也就是说这种情况是“假读空”。那么“假读空”是不会造成功能错误,只会造成性能损失,事实上这还可以算是某种程度上的保守设计(安全)。

2.3、格雷码

将一个时钟域上的指针同步到另一个时钟域,如果数据用二进制的方式进行同步的话就会出现多位数据同时跳变的问题,比如3'b011到3'b100即3到4跳变会引起多位数据的改变,这样会大大增加出错的概率。Gray 码就很好的解决了上述问题,gray码相邻数据只有一位跳变,这样就大大降低了数据出错的概率,有效的避免了在跨时钟域情况下亚稳态问题发生。

引入格雷码同时也引入一个问题,就是数据空满标志的判断不再是二进制时候的判断标准,这时由格雷码是镜像对称而造成的。

格雷码镜像对称
  • 如果是空状态的话,无可厚非,仍然是要满足读指针写指针每一位都相等。

  • 如果是满状态的话,要满足读指针写指针的高位和次高位相反,其余各位相等。

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

格雷码是二进制码右移1位再与原码相异或的结果,如下图所示[1]

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

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

快时钟采慢时钟可以直接采(打拍),但是快时钟信号同步到慢时钟域却有可能发生漏采的问题(在单bit的应用中需要展宽快时钟以便能被慢时钟采集到)。

在异步 FIFO 中,这样的漏采问题是不影响 FIFO 的逻辑操作。[2]分析如下:

读时钟域慢——写时钟域快:

在进行写满判断的时候:需要将读指针同步到写时钟域,因为读时钟域慢、写时钟域快,所以在写时钟域中采集到的读指针,是不会有遗漏的情况出现,同步会消耗时钟周期,所以同步后的读指针会小于等于当前读地址,所以可能写满会提前产生,并非真写满。

进行读空判断的时候:需要将写指针同步到读时钟域 ,因为读时钟域慢、写时钟域快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,漏掉的指针并不会对 FIFO 的读空产生影响。比如写指针从0写到10,期间读时钟域只同步捕捉到了3、5、8这三个写指针而漏掉了其他指针。当同步到8这个写指针时,真实的写指针可能已经写到10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到 FIFO 去,所以可能读空会提前产生,并非真读空。

读时钟域快——写时钟域慢:

在进行读空判断的时候:需要将写指针同步到读时钟域,因为读时钟域快、写时钟域慢,所以在读时钟域中采集到的写指针,是不会有遗漏的情况出现,同步会消耗时钟周期,所以同步后的写指针会小于等于当前写地址,所以可能读空会提前产生,并非真读空。

进行写满判断的时候:需要将读指针同步到写时钟域 ,因为读时钟域快、写时钟域慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,漏掉的指针并不会对 FIFO 的写满产生影响。比如读指针从0读到10,期间写时钟域只同步捕捉到了3、5、8这三个读指针而漏掉了其他指针。当同步到8这个读指针时,真实的读指针可能已经读到10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能读了数据到 FIFO 去,所以可能写满会提前产生,并非真写满。

3、异步FIFO的设计

  • 读、写时钟域下的读、写指针,指针位数需拓展一位。

  • 将读、写指针从二进制码转换成格雷码。

  • 将格雷码形式的读指针同步到写时钟域;将格雷码形式的写指针同步到读时钟域。

//将读指针的格雷码同步到写时钟域,来判断是否写满
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)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 是否为空、满。
//当所有位相等时,读指针追到到了写指针,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;

4、代码下载

博客中涉及到的代码均在我的GitHub仓库中,欢迎大家下载学习。

5、参考资料


异步FIFO详解
https://yao-jiangyu.github.io/async_fifo/
作者
小姚
发布于
2025年3月9日
许可协议