在准备笔试和面试时,发现“实现一个异步FIFO”几乎是必考题。虽然看过一些代码,但总觉得理解不够深入。想请教一下:1. 空满判断的具体逻辑和格雷码计数器如何设计?2. 指针同步时如何尽量减少亚稳态的影响?3. 深度不是2的幂次时该怎么处理?希望能得到一个清晰、可实现的思路解析,而不仅仅是代码。
经典的FPGA/数字IC笔试题:用Verilog实现一个异步FIFO,要注意哪些关键点?
19 个回答

异步FIFO这题确实老生常谈了,笔试面试总碰到。我自己也折腾过好几回,说几个实际做的时候得留神的地方吧。
空满判断这块,核心就是对比读写指针。但直接比二进制指针不行,跨时钟域会出问题。所以通常用格雷码计数器,每次只变一位,同步过去就算有亚稳态,指针也只会差一个值,不会出现彻底错乱的情况。空满信号生成都在各自时钟域里做,读侧用同步过来的写指针判断空,写侧用同步过来的读指针判断满。注意比较的时候,得把格雷码转回二进制再比,或者直接用格雷码比但理解清楚空满条件:读指针追上写指针就是空,写指针比读指针多一圈就是满。
指针同步为了减少亚稳态,常见的做法是打两拍。但这里有个细节,同步指针不是直接同步最新的那个,而是同步一个“延迟”的版本。比如写指针同步到读时钟域,先用写时钟把指针打一拍,再用这个打拍后的指针去同步,这样能降低同步时刻信号正好变化的风险。当然,两级触发器是基础,确保亚稳态不会传递下去。有些设计还会用更多触发器或者用握手,但 FIFO 里一般两级够了。
深度不是2的幂次的话,有点麻烦。因为格雷码那个一圈回环的特性要求深度是2的幂次。如果不是,常见做法有两种:要么就凑成2的幂次,浪费点空间;要么就用别的编码方式,比如用二进制指针加同步握手,但电路会复杂些。面试时如果被问到,可以提一下实际项目里尽量规划成2的幂次,省事。如果非得不是,得仔细处理指针回绕和比较逻辑,容易出错。
另外实际写代码时,记得把同步模块和空满判断模块分开,代码清晰点。仿真的时候多灌点数据,看看边界情况,特别是快写慢读和快读慢写这两种极端场景下,空满信号能不能及时拉起来。

异步FIFO的关键点,我觉得首先是空满判断。空满不能直接用二进制指针比较,因为读写时钟不同步,直接比会出错。所以要用格雷码计数器。写指针转成格雷码同步到读时钟域,和读指针的格雷码比较来判断空。读指针转成格雷码同步到写时钟域,和写指针的格雷码比较来判断满。格雷码相邻只有一位变化,能减少亚稳态概率。
深度不是2的幂次时,格雷码的循环特性就没了,不能直接用。常见做法是把深度补成2的幂次,但只用一部分地址。或者用别的编码,比如用两个二进制计数器,一个表示位置,一个表示绕圈次数,但同步起来复杂些。

我面试时被问过这个。除了楼上说的,指针同步要打两拍。比如写指针同步到读时钟域,先用两级寄存器打拍,再用来判断空。这样可以降低亚稳态传播的概率。但注意,打拍会导致指针延迟,所以实际FIFO深度要留点余量,防止真满了但还没报满。
空满判断的具体逻辑:满的判断是,写指针追上读指针一圈,但格雷码直接比较看不出差一圈。所以通常会把指针扩展一位,最高位表示是否绕了一圈。比如深度8,用4位指针,低3位是地址,最高位是绕圈标志。当写指针和同步后的读指针最高位不同,但低几位相同时,就是满。空是两者完全相等。
异步FIFO代码写起来不长,但细节容易错。建议自己动手写一遍,用VCS或Modelsim仿真下各种边界情况,比如同时读写、快写慢读、慢写快读。

从工程实现角度补充几点。
第一,异步FIFO的RAM最好用双端口RAM,一个口读一个口写,时钟独立。这是基本结构。
第二,格雷码计数器设计时,注意从二进制转格雷码是 binary ^ (binary >> 1)。转换逻辑要在各自时钟域完成,然后同步出去。
第三,深度非2的幂次时,我一般还是用格雷码,但把深度设为2^n,只是读写指针超过实际深度时归零。这样逻辑简单,但会浪费一点存储空间。比如深度12,我做到16,指针0~15循环,但只存12个数据。面试时可以说清楚这种取舍。
第四,亚稳态无法完全消除,所以同步链(打两拍)是标准做法。有些设计会打三拍,更安全但延迟更大。根据时钟频率和稳定性权衡。
最后,笔试时可能要画电路结构,记得标明时钟域,同步器放哪里。空满信号生成后,要寄存一拍输出,避免毛刺。

异步FIFO的关键点确实挺多,我面试时也被问过。空满判断的核心是比较读写指针,但因为是异步时钟,直接比二进制指针会出问题,所以要用格雷码。格雷码的特点是每次只变一位,这样即使同步时采到亚稳态,指针值也是相邻的,不会出现跳变多个bit导致空满误判。具体设计时,读写指针各自用格雷码计数器,然后同步到对方时钟域去比较。满标志是写时钟域判断(同步后的读指针和本地写指针比较),空标志是读时钟域判断(同步后的写指针和本地读指针比较)。深度非2的幂次时,格雷码循环的特性会打破,一般建议用2的幂次深度,或者用其他同步方法。

我主要从亚稳态角度说说。指针同步肯定要用两级触发器打拍,这是基本操作。但光打拍不够,因为格雷码指针在同步过程中可能还是会出现亚稳态,导致采样值不是正确的当前值。不过格雷码的好处是,即使采样值错了,也只会是前一个或者后一个相邻值,不会出现一个完全无关的值。这样在比较空满时,最坏情况也就是把“快要满”判断成“满”,或者把“快要空”判断成“空”,属于保守判断,不会导致数据溢出或读空。这算是格雷码带来的安全边际。另外,同步链的长度要足够,确保MTBF满足要求。

楼上两位把原理讲得挺清楚了,我补充点实际实现的细节。1. 格雷码计数器:通常用二进制计数器转格雷码(gray = bin ^ (bin>>1))。注意指针宽度要比地址多一位,最高位用来区分写指针是否超过了读指针一圈。比如深度8,地址用3bit,指针用4bit。满的条件是:最高位不同,低3位相同。空的条件是:所有位相同。2. 深度非2的幂次:确实麻烦。一种方法是还是用2的幂次深度,但逻辑上只使用一部分地址。比如需要深度12,就用深度16的FIFO,但读写指针比较时,对深度取模比较。不过这样格雷码的循环特性没了,同步指针时可能要多加处理。另一种思路是直接用二进制指针加同步FIFO来同步指针,但复杂度高。面试时一般会假设深度是2的幂次。3. 代码风格:记得把同步模块和逻辑判断模块分开,清晰易懂。

异步FIFO的关键点确实挺多,我面试时也被问过。空满判断的核心是比较读写指针,但因为是异步时钟,不能直接比二进制数,要用格雷码。格雷码计数器每次只变一位,同步过去即使有亚稳态,也只会错一个周期,不会出现灾难性错误。满标志是写指针追上了读指针(考虑绕回),但要注意区分是真满还是空。深度非2的幂次时,格雷码的对称性会破坏,一般建议用2的幂次深度,或者用别的同步方法。

我来说说我的理解。异步FIFO最麻烦的就是亚稳态和空满判断。1. 格雷码计数器设计:读写指针都用格雷码表示,比如深度8,指针宽度4位(因为要区分满和空,需要多一位)。写时钟域生成写指针格雷码,同步到读时钟域用于空判断;读指针类似。2. 减少亚稳态:打两拍同步器是基本操作,但格雷码本身已经降低了亚稳态传播的风险。3. 深度非2的幂次:强烈建议避免,因为格雷码要求循环特性。如果必须,可以考虑用二进制指针加握手同步,但性能会下降。空满逻辑:满=(wptr_gray同步后与rptr_gray比较,高位相反,其余相同);空=(同步后的rptr_gray与wptr_gray完全相等)。注意比较的是同步后的指针。

这个问题我实际做过。除了楼上说的,还有几个细节:指针宽度要比地址多一位,最高位用来指示是否绕回了一圈。比如深度8,地址用3位,指针用4位。满的判断是:写指针比读指针多了一圈(即最高位不同,低地址位相同)。空的判断是:写指针和读指针在同一圈(最高位相同,低地址位相同)。同步时,一定要把格雷码指针同步到对方时钟域,而不是二进制码。深度非2的幂次时,格雷码序列不闭合,可能得用双寄存器同步二进制码,但空满判断会更复杂,容易出错。笔试时通常假设深度是2的幂次。
发表回答
登录后可在本页底部提交回答