最近在刷FPGA校招笔试题,发现异步FIFO几乎是必考题。我按网上的教程写了一个,但仿真时总丢数据,尤其是跨时钟域时格雷码指针同步总出问题。请问大佬们,格雷码指针的二进制转换和同步打拍到底怎么处理才能保证不漏读、不写满?有没有标准的代码模板可以套用?急求,下周就要笔试了!
2026年FPGA校招,Verilog笔试题常考异步FIFO设计,格雷码指针怎么算才不丢数?
提问
回答 12

异步FIFO丢数最隐蔽的坑往往不在格雷码转换本身,而在你把二进制指针打拍到另一个时钟域之后,又用同步后的值去比较空满。标准做法是:写指针先转格雷码,再打两拍到读时钟域,然后用这个同步后的格雷码写指针去和本地格雷码读指针比较,产生空标志;读指针同理。如果你直接用同步后的二进制指针去比较,或者同步格雷码后又转回二进制再比较,跨时钟域时多位同时变化的风险就会导致误判。格雷码转二进制的公式是 binary[MSB]=gray[MSB],然后 binary[i]=gray[i]^binary[i+1],这个组合逻辑要小心延时,但笔试题一般不会卡时序,重点是逻辑正确。另外,很多教程给的模板里空满判断用了 n-1 位比较,那是针对同步FIFO的;异步FIFO得用格雷码的 MSB 和次高位来区分全满和全空,具体是:满标志在格雷码写指针比同步后的格雷码读指针高两位且低位相同,空标志是同步后的写指针和本地读指针完全相等。你仿真丢数,先检查是不是把同步后的指针又打了一拍才去比较,或者忘记在比较前把格雷码对齐。下周笔试的话,建议手写一个简洁版,重点把双口RAM、双指针、格雷码同步和空满逻辑四部分写清楚,别纠结深度参数化,面试官更看重跨时钟域的理解而不是代码量。你用的是单口RAM还是双口RAM?这个在仿真时容易搞混。

丢数问题本质上是同步后的指针跟实际指针存在两个时钟域的相位差,这个差在笔试题里通常可以容忍,但你的代码里如果踩了下面两个雷,仿真就会丢数。第一个雷:空满标志生成时用了组合逻辑直接比较,但格雷码本身是时序逻辑产生的,组合比较的结果可能在时钟沿附近跳变,导致读使能或写使能误触发。正确的做法是让空满标志也寄存一拍输出,虽然会多一个时钟周期的延迟,但能避免亚稳态传播到控制逻辑。第二个雷:你用格雷码比较空满时,把写指针同步到读时钟域后,跟读指针比较产生空标志,这个比较是组合逻辑没错,但空标志通常用来控制读使能,读使能又控制读指针递增,这形成了组合环路。仿真模型里如果读指针和空标志的更新顺序不对,就会丢数据。解决办法是在读使能路径上插入一个寄存器,或者在状态机里让空标志只影响下一拍的操作。至于格雷码递增,记住规律:格雷码最低位每两个周期翻转一次,次低位每四个周期翻转一次,以此类推,但写代码时直接用二进制加一然后转格雷码就行,不用手动推导翻转序列。笔试题的深度一般不大,比如16或32,你注意满标志判断时写指针要领先读指针一圈,格雷码下判断条件是(wptr_gray_sync == rptr_gray) && (wptr_gray[MSB] != rptr_gray[MSB])才表示真的满,而不是单纯相等。最后给个速成建议:去网上搜一个经过验证的异步FIFO模板,逐行注释搞懂,然后手敲三遍,第一遍照着抄,第二遍默写,第三遍改参数(比如深度改成8或64)重新仿真通过。笔试时如果时间紧,直接写那个模板,面试官更关心你能否解释清楚为什么格雷码同步能降低出错概率,而不是代码是否原创。你仿真用的工具是Vivado还是Modelsim?不同工具的默认仿真精度会影响跨时钟域采样行为。

丢数据最容易被忽略的其实是读使能和写使能信号自己的时序。很多人花大力气把格雷码同步对了,空满标志也寄存了,结果发现读使能是从空标志取反直接连过来的组合逻辑——读时钟沿来的时候,空标志刚刚变低,读使能沿没吃准,读指针根本没动,数据就被下一个写操作覆盖了。一个保险的做法是让读使能也打一拍,变成类似 read_en_reg 的信号,再用这个寄存后的使能去控制读指针递增。这样虽然多耗一个周期,但能保证读指针和空标志的更新不会出现组合环路里的竞争。另外,格雷码递增有个小技巧:你可以在二进制域里做加一操作,再转格雷码,而不是直接在格雷码上找递增规律。用公式 gray_next = binary ^ (binary >> 1) 就可以,这个写法在笔试题里很少出错。同步打拍的时候,记得把格雷码打两拍,第一拍输出可能亚稳态,第二拍基本稳定,再拿去跟本地的格雷码比较。你仿真丢数据,可以先把读使能和写使能寄存一拍试试,大概率能解决。你现在用的是单时钟域的测试激励吗?如果测试时钟频率差得太大,丢数可能不光是同步问题,还有深度不够导致的满写覆盖。

笔试题里格雷码指针的同步,你就记住一句话:先转格雷码再打两拍,同步完再和本地格雷码比较,千万别同步二进制指针。模板网上那一大堆,你挑一个把空满标志也寄存的版本就行。

异步FIFO丢数最深的坑不在格雷码本身,而在你把同步后的指针拿去做空满比较时,忘了考虑同步延迟带来的指针错位。举个例子:写指针从 0 变成 1,格雷码从 000 变 001,你把它同步到读时钟域需要两拍。在这两拍里,读时钟域看到的写指针还是 000,读指针如果已经走到 001,读逻辑会认为 FIFO 是空的,于是停止读。但实际上写指针已经变成 001 了,里面有两个数据。这就是典型的假空标志导致丢读。反过来,满标志也可能由于同步延迟产生假满,导致写侧不写,数据没存进去。所以笔试题里你设计的 FIFO 深度至少要大于同步延迟造成的指针差异,一般来说深度取 2 的幂次且大于等于 4 比较安全。还有一个更隐蔽的:格雷码的 MSB 和次高位用来区分空满时,如果深度不是 2 的幂,格雷码的连续性会被破坏,这时候直接用公式比较会出错。校招题一般默认深度是 2 的幂,但如果你自己写测试用例时改了深度,记得检查格雷码是否还能保持相邻位只变一位的特性。另外,代码模板里的空满生成逻辑,标准写法是用同步后的格雷码指针和本地的格雷码指针做按位异或,再判断 MSB 和次高位是否相等。很多人抄网上的模板时把异或顺序写反了,导致仿真时空满标志一直为真。你可以这样验证:在写时钟域,把同步后的读格雷码和本地写格雷码逐位异或,如果结果高两位相同且其余位全零,就是满;在读时钟域做类似操作判断空。最后,如果笔试时间紧,其实可以直接用二进制指针加 FIFO 深度计数器,然后把计数器的二进制值同步到另一个时钟域做空满判断——虽然面积大一点,但逻辑简单,不容易丢数。你倾向用哪种方案?如果只是应付笔试,后一种改起来更快。

我个人感觉你仿真丢数据的根本原因很可能不在格雷码本身,而在于你把空满标志生成和读/写使能的关系处理成了组合反馈环路。很多网上的模板为了看起来简洁,会把空标志直接赋给读使能,读使能又去控制读指针递增,读指针再回头影响空标志——这在仿真里会因为赋值顺序不确定而出现丢读或丢写。我的建议是:先不管格雷码的细节,你把空满标志都打一拍寄存输出,然后用这个寄存后的标志去控制读/写使能,使能再打一拍后再去驱动指针更新。虽然这样会让FIFO延迟一个周期,但能彻底切断组合环路。格雷码本身的计算其实很简单,二进制加一后右移一位再异或原值,这个公式你写对就行。跨时钟域打拍时注意先转格雷码再打两拍,千万不要同步二进制指针。另外,深度不是2的幂时格雷码的连续性会被破坏,笔试常见深度是4、8、16,你按2的幂取就安全。如果你仿真时丢数的场景是读写同时有效,那大概率是空标志或满标志在时钟沿附近跳变导致的,寄存一拍就能解决。你现在的仿真是在哪个工具里跑的?不同仿真器的delta cycle处理有差异,有时Modelsim不丢但Vivado仿真会丢,这个也要区分一下。

丢数问题你先把一个关键点想明白:空标志和满标志一定要用格雷码比较,且比较结果必须寄存一拍再输出。格雷码怎么算你可以背下来:gray = (binary_next) ^ (binary_next >> 1),binary_next 是你当前二进制指针加一。同步打拍用两级触发器,第一级可能亚稳态但第二级能稳定。你按这个流程走,大部分笔试题都能过。如果还丢数,检查一下读使能是不是从空标志直接组合出来的,改成寄存后再用。

格雷码打拍前先转,同步完再比较,空满标志寄存输出,使能也寄存——这三步做到位,笔试题的异步FIFO基本不会丢数。你下周笔试的话,把这段逻辑背熟,手撕代码时注意复位时指针全零、满标志初始为高就行。

丢数据你先把读使能路径查清楚:空标志是不是组合逻辑直接连到读使能,读使能又回去控制读指针?如果是,仿真时大概率因为赋值顺序丢读。加一级寄存器隔开就好了。格雷码本身公式就那么一个,别纠结。你当前仿真用的什么工具?Vivado还是Modelsim?时序设置对了吗?

个人感觉网上一半的模板为了显得短,空满标志都是组合逻辑直接输出,这在校招笔试里基本是扣分点。正确的做法是把空满标志用寄存器打一拍再输出,虽然这样会让FIFO晚一个周期响应,但能彻底切断组合反馈环,仿真才不会丢数。格雷码的计算你背熟 gray_next = (bin+1) ^ ((bin+1)>>1) ,指针递增前先在二进制域加1再转换,不要直接在格雷码上找规律。同步时注意一定要先转格雷码再打两拍,同步完用格雷码直接比较,绝对不要同步二进制指针。如果你写的是深度4或8的FIFO,按这个改完应该不会丢数了。另外笔试手撕代码时,记得复位时写指针和读指针都置0,满标志初始拉高,读使能先寄存一拍再用——这三行写对,面试官一般就不追问了。你下周笔试的话,现在赶快对着模板改一版,仿真跑几个随机读写序列验证一下。
发表回答
登录后可在本页底部提交回答
