异步FIFO其实没有想象中的那么简单,在实际业务中,FIFO可以说是最容易出现挂死的问题了。大家可能看着代码觉得逻辑清晰,怎么可能会出错,但是其实在很多设计中,稍微有加扰整个系统就挂死了,亚稳态只是一个最基础问题。可以再思考一下,如果FIFO的读写控制都由外部决定,是否会出现同时空满的情况?同时空满是否一定能得到恢复?如果FIFO同空同满很可能导致既不能写也不能读,还不能恢复正常。

本文比较全满介绍了异步FIFO 中格雷码的应用,但需要读者有一定的基础。

1.异步FIFO跨时钟域产⽣的问题

当我们创建异步FIFO的时候,跨时钟域是⼀个⽆法避免的问题,为了正确的在不同时钟域传递地址指针,⽤于空满判断以及实时⽔线计算,通常我们会采⽤寄存器打拍和格雷码编码两种⼿段降低亚稳态的影响。为什么要使⽤格雷码⽽不采⽤常规的⼆进制编码?简单的讲,就是格雷码每个时钟周期只会变化⼀位。⽽⼆进制码会在⼀个周期内变化多位。但是深究⼀下,为什么要求只变化⼀位呢?或者说,普通⼆进制变化多位会产⽣什么影响呢?

如上图所示,FIFO的满信号由写⼊控制模块产⽣,空信号由读取控制模块产⽣。读控制模块把当前时刻的读地址传递给写模块,但是由于中间传递需要时间,写模块接收到的读地址永远都是⼩于或者等于当前真实的读地址的。同时,由于FIFO中的数据个数永远是⼤于等于0,也就是说满⾜如下关系:

当前真实的写地址 ≥ 当前的读地址 ≥ 接收到的读地址

按照正常的思维,⽆论你使⽤什么进制传递数据,上⾯的不等式在任意时刻都应该成⽴。但是在真实的传输过程中,地址每个bit位由于和上级寄存器之间的⾛线距离不⼀样,翻转的速度不⼀样,在跨时钟采样时可能发⽣错误。

假设现在需要将 a时钟域的地址 dat 同步到 b时钟域上,dat是多bit位数据。假设dat上⼀时刻为1111,下⼀时刻将变为0000。由于每个bit位翻转的速度有差别,⽽dat的四个数据位并不⼀定是同时翻转的,有可能是存在1111->0111->0001->0000这个过程。dat通常只能保证第⼀个a时钟的上升沿是1111,在下⼀个a时钟的上升沿到来时,已经稳定在0000。

由于a_clk和b_clk之间相位、频率不⼀定相同,b时钟的上升沿可能在两个a时钟上升沿之间到来,此时dat不⼀定已经稳定,就会发⽣采样错误,最左侧的寄存器采样到了0111,并传递给下⼀个寄存器。虽然可能在下⼀个b时钟上升沿到来时能够正确采样,但是这个错误值已经被传递到另⼀端了。

如上图所⽰,虽然addr满⾜在下个clk_r上升沿完成从1111跳变到0000,但是clk_w对addr的采样可能像addr'所⽰那样,中间出现⼀个异常值,这个异常值不满⾜ 当前真实的写地址 ≥ 当前的读地址 ≥ 接收到的读地址这个不等式。进⽽影响另⼀端的空满状态判断,以及实时⽔线等,例如空满同时拉⾼,空满未告警,读写溢出等等。

具体来讲,如果中间的寄存器采样到了0001,并将地址传递给了读模块,会导致传递值⽐期望值⼤,这就会造成空满异常。⽐如明明读地址还很⼩,传了个⼤数,让写模块以为还可以继续写⼊,这就造成了溢出。

2. 为什么需要使用格雷码

格雷码⼀次只变化⼀位,即使采样错误,也只是没有更新当前的数值,格雷码在初始值和最终值之间没有中间状态,能够保证变化前后传输地址之间的⼤⼩关系不会突变。原本传输0001,结果传输了0000,这并不会影响FIFO的功能,只会让FIFO提前判断空满。

如上图所⽰,使⽤格雷码的话前后数据只有⼀位的差别,从0001->0000这个过程没有中间状态,不存在某个采样点出现不满⾜不等式的异常值。(⼤不了接收到的地址⽐当前的地址更⼩)

3. 读写时钟差异过⼤时,格雷码还有用吗?

这个话题来源于CSDN的⼏篇博客中的讨论:

异步fifo 读写时钟差别太大问题 (首先这个设计是不合理的)_异步fifo读写时钟相差太大-CSDN博客

关于FIFO的一些问题_异步fifo读写时钟相差太大-CSDN博客

他们提到的⼀个观点是,快时钟域的地址被慢时钟采样时,两个采样点之间可能有多bit变化,体现不出来格雷码的优点。具体意思是,设0时刻写地址为0000(0),读时钟⽐写时钟慢很多倍,可能等到采样时,写地址已经变化到了0011(2)。在这个过程中,即使使⽤的格雷码传递地址,也会发⽣多bit的跳变,和⼆进制的情形⼀样了,事实是这样吗?

我认为不是。

与上述⼆进制地址不同,格雷码每次的变化路径都是已知,中间的每个状态都是可以确定的。例如,从中间的寄存器来看,似乎数据从000直接变化到了011,在变化的过程中可以有很多情况。但事实上,可以选择变化的地址只有000-001-011这其中的⼀个,在任何时刻都不可能出现采样到010的这种情况,因为在任何⼀个时钟周期,第1个寄存器的输出只有两种可能,没有任何时刻会出现010的状态。格雷码每⼀次变化都是当前状态已经稳定了的情况。因此,⽆论何时采样,1中的不等式恒成⽴。但是读写时钟差异过⼤,会对FIFO的深度有要求。如果深度太浅,会出现空满同时拉⾼的情况。例如写时钟很快,当FIFO是空的时,空信号拉⾼。如果此时开始写⼊数据,写地址还没来得及同步到读时钟域上作⽐较,把空信号拉低,⽽在写侧实时的写地址已经到了FIFO深度,满信号也会拉⾼,FIFO出现同时空满的异常状态。

4. 只要使用格雷码就万无一失吗?

是不是只要使⽤了格雷码传递地址,FIFO就能正常⼯作呢?

第2节中提到了⼀个容易忽略的关键点,就是格雷码要求要更新状态时,上⼀次的状态已经稳定。如果说格雷码的某⼀bit位路径时延特别⼤,其他位都要进⾏下⼀次变化了,上⼀时刻的状态值还没更新过来,这就会出现问题。因此在设计异步FIFO的时候需要对格雷码的路径进⾏时序约束,传播的最⼤时延应该⼩于⼀个本时钟域的时钟周期。

set_max_delay [-datapath_only] -from [ node_list] -to [node_list] delay_value

⼀般⽽⾔,设计可以留⼀些裕量,有的公司会设置为周期的⼀半。

5. 其他约束方法

对格雷码地址做约束本质是什么?是为了控制时延大小吗? 其实不是

本质上是为了将格雷码的各个bit位对齐而已,并不在乎有没有下一个时钟周期及时传递过去,慢就慢了,但是必须得一起慢。

因此,当上述约束不能满足时钟频率时,可以同时约束max_delay和min_delay,放宽一点,只需要保证二者之差小于一个(最好半个)时钟周期即可。