ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA FIFO IP核深度解析:从跨时钟域到配置调试

FPGA FIFO IP核深度解析:从跨时钟域到配置调试 做FPGA开发的工程师应该都有被FIFO折磨过的经历。刚接触FPGA时我以为FIFO就是一块能存数据的RAM直到做第一个跨时钟域项目发现数据老是丢、空满标志乱跳才意识到这个看起来简单的模块背后有太多门道。FIFO IP核作为FPGA开发中使用频率最高的基础模块几乎是所有数据流系统的必经之路从UART串口到以太网、从图像采集到高速ADC只要有数据缓冲、速率匹配、跨时钟域传递背后都离不开FIFO。这篇内容我打算系统梳理一下FIFO IP核相关的东西。不是说会双击IP核生成一个实例就够了而是要搞清楚为什么要用IP核而不是自己写RTL同步FIFO和异步FIFO怎么选配置界面里那些参数到底该不该勾深度怎么算以及调试时遇到空满标志不正常、首数据丢失这类问题该怎么排查。适合刚入门FPGA但已经能完成一个简单工程的读者也适合那些用了好长时间IP核却一直没想通原理的工程师。1. 为什么用IP核而不是自己写FIFO1.1 FIFO是什么从排队说到环形缓冲区FIFO的全称是First In First Out先进先出。用生活里的例子理解就是奶茶店排队先来的先取餐后来的跟在后头。在FPGA里FIFO就是一块用RAM搭建的队列存储区写端口把数据放到队尾读端口从队头取数据读写互不干扰各走各的指针。这里的核心是指针管理。写过RTL的都知道FIFO内部通常是一个双端口RAM加两个地址计数器一个写地址指针一个读地址指针。数据写进去写指针加一数据读出来读指针加一。两个指针相等时要么是空要么是满怎么区分这两个状态就得额外加一个计数寄存器或者用最高位做标志位。这看起来不难但真正在工程里落地的时候情况比这个复杂得多。我自己第一次写FIFO是在学习Verilog的时候用计数器加双口RAM拼了一个同步FIFO仿真通过以后还挺得意。后来把它放到一个跑着100MHz和150MHz两个时钟的工程里配合异步逻辑使用立刻出问题读出来的数据有时是错的空满信号偶尔还会冒出毛刺。从那以后我明白一个道理FIFO这种模块看起来简单但要在各种时钟频率、各种读写节奏下都稳定工作需要处理的边界情况非常多。1.2 手写FIFO的真正成本在哪手写一个同步FIFO确实不难几行代码就能搞定。困难的是写一个经得住工程检验的FIFO尤其是异步FIFO。异步FIFO需要在两个时钟域之间传递读写指针这里马上会碰到两个经典问题亚稳态和指针一致性。二进制指针跨时钟域最大的坑在于多位同时翻转。比如读指针从0111变到1000四位全部翻转写时钟域在同步这个指针时刚好采到中间状态1011那就不是偏差一两个数的问题了直接会导致空满判断错误数据错乱几乎不可避免。解决思路是使用格雷码每次计数只会有一位翻转。但格雷码的生成和二进制转换又带来额外逻辑判断空满也得多想几步。这个后面详细讲。除了指针跨时钟域手写FIFO还要处理的关键问题包括读写同时进行时计数器的竞争、深度不为2的幂时地址回绕逻辑的复杂度、空满信号输出时要不要打拍去毛刺、复位后初始状态的确定等等。这些问题单独拎出来都不至于难倒人但组合在一起而且要在不同频率、不同FPGA器件上都能时序收敛工作量就不小了。关键还是验证成本。自己写的FIFO你得写一套完整的testbench覆盖各种读写节奏、边界条件、跨时钟域场景。即使功能仿真过了上板之后跑在真实环境中谁也不敢保证100%没隐患。而厂商IP核是经过大量项目检验、有完整验证报告和仿真模型的在时间就是成本的工程项目里用厂商验证过的IP核明显更划算。1.3 厂商IP核替你解决了哪些核心问题Xilinx和Altera也就是Intel都有自己的FIFO IP核Vivado里叫FIFO GeneratorQuartus里叫FIFO Intel FPGA IP。这些IP核本质上做的是同一件事把上面说的那些棘手问题用厂商验证过的电路逻辑解决掉再给你一个配置界面。以Xilinx的FIFO Generator为例你可以在里面选择同步还是异步、设置位宽和深度、选择标准读时序还是First-Word Fall-ThroughFWFT模式、配置almost full和almost empty标志、选择是否使用寄存器输出。底层的存储器可以用块RAMBRAM、分布式RAM或者专用的FIFO硬核不同选择对应不同的资源占用和最高工作频率。厂商IP核的价值在于边界情况处理。比如异步FIFO的格雷码转换逻辑、空满信号产生后的同步打拍、复位后内部状态机的初始化这些细节全部的底层RTL已经封装好了。你拿到的是一个被验证过的稳定模块只需要关注接口时序不需要把精力耗在这些基础逻辑上。有人觉得用IP核是偷懒是没水平。我觉得恰恰相反会用IP核、能根据场景选对IP核的参数是FPGA工程师的基本功之一。能把时间花在系统的核心逻辑上而不是重复造轮子这才是成熟的工程思维。2. 同步FIFO与异步FIFO的选型2.1 同步和异步的本质差异同步FIFO和异步FIFO的差别一句话就能说清楚同步FIFO的读写时钟是同一个异步FIFO的读写时钟不同。但这个差别带来的设计难度差异非常大。同步FIFO主要用于同一个时钟域内的缓冲。比如CPU要往DDR写数据写数据的模块一次发射32个数据但DDR控制器带宽有限一次只能接收8个这中间就需要一个FIFO先缓存下来再慢慢吐出去。这种场景下读写时钟相同指针比较可以直接做不存在跨时钟域的问题逻辑很简单用IP核主要是图省事。异步FIFO则完全是另一个难度级别。它承担的是跨时钟域数据传递的任务比如ADC采集时钟域的数据要传给处理时钟域低速串口接收的数据要传给高速系统总线这些都需要异步FIFO做桥梁。异步FIFO的难点在于写指针和读指针分属两个时钟域判断空满时必须把对方的指针同步过来。而指针同步一旦做不好就会产生错误的空满信号后果就是该读的时候没数据或者FIFO已经满了还在往里写数据被覆盖。2.2 异步FIFO的格雷码设计为什么可靠异步FIFO处理指针同步的核心方案就是格雷码加两级同步器。格雷码的特点是相邻两个编码之间只有一位变化。比如二进制从7跳到8对应的格雷码是从0100跳到1100只有最高位变了。跨时钟域同步时即使遇到亚稳态采样结果最多是“前一拍的值或者后一拍的值”绝不会跳到一个完全无关的数字上。放在空满判断里就是最多让空满标志早一点或晚一点出现但不会产生方向性错误系统最终会自洽恢复。两级同步器解决的是亚稳态本身的传播问题。单级触发器在数据变化时采样输出端可能会出现一段不确定的电平漂移再接一级触发器后如果第一级还在抖动第二级采样的概率就极低了。两级同步器会把亚稳态出现的概率压低到工程上可以接受的范围三级同步器更低但多一级就多一个周期的延迟。这里要注意一个细节同步FIFO的空满标志和异步FIFO的空满标志产生方式不同。异步FIFO中空标志必须把读指针同步到写时钟域去比较满标志必须把写指针同步到读时钟域去比较。所以异步FIFO的空满标志天生就有几个时钟周期的延迟这意味着你看到empty拉低时里面可能还有几个数据等着被读走看到full拉低时写端还能再往里塞几个数。这个延迟特性在写逻辑的时候必须预留余量否则就会踩坑。如果你在一个工程里必须跨时钟域传数据且数据量大于几个字节、对延迟又有要求直接用厂商的异步FIFO IP核几乎是唯一合理的选择。自己写不是不行但得花时间把格雷码转换、两级同步、空满余量全部验证清楚多数项目等不起这个时间。2.3 典型场景对照表使用场景FIFO类型原因同频模块间做数据缓冲同步FIFO读写时钟相同逻辑简单ADC数据跨时钟域传给处理模块异步FIFO采样时钟和处理时钟独立高速串口收发数据缓存同步FIFO或异步FIFO取决于收发模块是否同源时钟图像传感器输出数据流缓冲异步FIFO像素时钟和系统总线时钟不一致CPU指令队列、微操作缓存同步FIFO同源时钟内做流水缓冲多核SoC总线桥接数据转储异步FIFO两个总线域时钟频率和相位都无保证有没有同步FIFO和异步FIFO结合用的场景有比如说一个模块先用同步FIFO做速率匹配再用异步FIFO做跨时钟域传递两段各管各的问题逻辑清晰也好调试。3. IP核配置那些选项到底要不要勾3.1 位宽、深度与存储器类型用Xilinx Vivado的FIFO Generator举例打开IP核配置界面后第一个要填的就是组件名称和数据位宽接着是FIFO深度。这两个参数决定了FIFO的容量。位宽很好理解就是你一条数据有多少bit深度表示最多能缓存多少条数据。深度通常要求是2的幂次方比如16、32、64、128、512这是为了地址回绕逻辑简单也利于存储器资源的利用。接下来要选存储器类型常见的有Block RAM、Distributed RAM和专用FIFO硬核。Block RAM是FPGA里的大块片上存储容量大但数量有限适合深度较大的FIFO。Distributed RAM由查找表拼出来适合深度较小的FIFO比如不到64不占用BRAM资源但一个LUT只能存少数bit深度大了布线压力很大。专用FIFO硬核是厂商在芯片内部做好的物理FIFO电路比如Xilinx 7系列里的FIFO18速度快、功耗低但是容量固定、配置不灵活。选存储器类型主要看两件事一是深度二是速度。深度深用BRAM深度浅用分布式RAM频率要求特别高可以考虑专用FIFO硬核。我个人的习惯是深度小于16选分布式RAM深度16到512选BRAM更高频的场景看资源表再定。3.2 Standard模式与First-Word Fall-Through模式这个选项搞错的人非常多。Standard模式和FWFT模式的区别说白了就是读数据输出时机不同。Standard模式下你拉高rd_en数据在下一个时钟沿出现在dout上。也就是说读使能到数据有效有整整一个周期的延迟。这个模式符合大多数人对RAM读操作的直觉也是默认常用模式。FWFT模式下FIFO顶部的数据会主动出现在dout上不需要先拉rd_en。你拉高rd_en表示“把这筆数据取走”同时下一笔数据已经在路上了。这个模式省掉了一个周期的读延迟对于需要降低延迟的场景很有价值比如做高速缓存行填充、流水线级数敏感的模块。容易踩坑的地方在这里FWFT模式下FIFO为空时dout上可能残留着上一次的数据如果你直接采dout会在FIFO为空时拿到一个伪数据。正确写法是用empty信号做门控empty为高时丢弃dout只有empty为低时才认为dout有效。我见过不少工程师在FWFT模式下不检查empty就采数导致读到了空数据现象就是数据流里每隔一段出现一个错误值非常难排查。3.3 余量选项与两大厂商配置差异FIFO IP核还有不少额外选项almost full/almost empty可编程标志、读数据计数rd_data_count、写数据计数wr_data_count、同步使能、异步复位等。almost full标志在深度还剩少量余量时提前拉高主要用来做反压或流控。比如FIFO深度是1024设置almost full在还剩64时拉高写端看到这个信号就暂停发送给后端处理留下足够的重排空间避免真的写满才停止导致突发期数据溢出。这个在高速接口设计中非常实用优先建议使用。Xilinx和Quartus两家的配置界面差异其实不大思路基本一致。Quartus里FIFO IP核在配置时多了一项“输出寄存器”的选择。打开输出寄存器后dout上会多打一拍时序更好但多了一个周期延迟。如果你的设计频率比较高比如超过200MHz建议勾上低频场景不勾省掉那一个周期延迟。这里还是以Xilinx比Quartus更常见的rd_rst_busy信号为例说明Vivado的FIFO IP在复位后会拉高rd_rst_busy和wr_rst_busy一段时间这个信号默认需要关注后面专门讲。Quartus里也有类似行为叫rst_busy。两个工具叫法不同但本质都是在告诉你复位完别急着操作等内部状态稳定再说。4. 读写时序与复位实操4.1 读时序Standard和FWFT差在哪Standard模式下读操作的波形可以这样理解rd_en为高时MUX把RAM地址切换到读指针对应的位置下一个时钟上升沿数据被锁到dout寄存器上。所以从发出读请求到拿到数据天然有一个周期的latency。这个周期不是浪费而是RAM本身的读延迟加上输出寄存器的开销。FWFT模式下FIFO内部会在没有读请求时就把头部的数据预取出来放到dout上读使能只是一个“确认取走”的动作。这时latency几乎为零代价是内部多了一组预取逻辑面积稍大而且dout在FIFO为空时没有惰性保持需要外部逻辑用empty做保护。实际使用中我把两种模式的选型总结成一个简单的判断标准如果读端对延迟敏感比如链路级数紧张的高带宽设计用FWFT如果读端本身有握手协议、不差这一个周期用Standard更稳妥。毕竟Standard模式的行为更直观也更容易做时序收敛。4.2 rd_rst_busy和wr_rst_busy的真正含义这个信号是新版Xilinx FIFO IP核引入的很多工程师用起来一脸懵。简单说rd_rst_busy表示读时钟域还处于复位释放后的不稳定状态wr_rst_busy表示写时钟域处于相同状态。这两个信号都是高电平有效只有它们都拉低之后FIFO才能正常读写。最坑的情况是异步复位释放时如果读时钟域和写时钟域没有对齐rd_rst_busy和wr_rst_busy不会同时拉低。复位同时释放但两个时钟域看到复位信号的时间本来就不同再加上各自内部状态机恢复需要的时间两个busy信号释放可能有几拍到几十拍的偏差。在这段时间里如果贸然往FIFO写数据或读数据轻则丢首数据重则FIFO内部指针错乱后面所有数据都是错的。正确的做法是把rd_rst_busy和wr_rst_busy同时用逻辑拉低作为“FIFO可操作”的总门控。两个busy信号取或为低才允许读写。这在复杂时钟系统里是性价比最高也最保险的方案成本就是几行逻辑。4.3 复位释放后等多久才能操作具体等多久手册上是这么写的复位释放后内部会先进行一段初始化序列所有指针归位、空满标志拉高、输出数据寄存器清零这段时间长短和时钟频率、器件型号都有关一般从几拍到几百拍不等。最好的办法不是硬编码等固定周期而是直接检查rd_rst_busy和wr_rst_busy。我调试时习惯写一个小状态机上电或复位后先等待两个busy信号都拉低然后进入正常工作状态。有些工程师偷懒复位结束直接开始读写前几个数据不对还以为是别的问题查了半天。其实只要规规矩矩等busy释放这类问题基本都能避免。对异步FIFO而言还有一个额外注意点复位信号必须在两个时钟域都被正确回收。Xilinx官方推荐的做法是使用同步后的复位或者让IP核的复位输入同时送进读写时钟域并保证释放同步。实际工程里我一般会在顶层例化一个异步复位同步释放模块给FIFO提供干净的复位信号保证FIFO内部不会出现复位释放时采集到亚稳态的情况。5. FIFO深度计算的完整思路5.1 最坏突发法深度选多大是FIFO配置里最影响系统稳定性的参数。选小了突发写入时数据被覆盖数据丢失选大了浪费宝贵的片上存储资源和成本。深度的计算业界常用的是最坏突发法。最坏突发法的核心思路是找出系统在最坏情况下一个突发周期内写入的数据量减去同一时间内读出的数据量这个差值就是FIFO必须容纳的最低深度。公式可以写成FIFO深度 ≥ 突发长度 B - (突发期间读出的数据量)假设写时钟频率为Fwr读时钟频率为Frd一个突发周期内写入B个数据那么写满这B个数据需要的时间是B/Fwr秒。在这段时间里读端能读出的数据量是(B/Fwr)×Frd个。所以最坏残留就是D_min B - B × Frd / Fwr计算出来是多少就留多少再乘以一个安全系数一般1.5到2最终取2的幂次。需要注意这个公式成立的前提是读端一直在连续读、没有停顿。如果读端也有突发行为比如内存控制器一次性要攒满若干数据才发一次DDR写请求那就要把读写双方的突发窗口都考虑进去情况会更复杂。工程上比较省心的做法是把突发长度定为双方支持的最大突发值再用上面的公式算下限然后往大取整。5.2 一个采集系统深度计算实例举一个我实际做过的例子参数略作修改方便说明。一个ADC采集系统ADC以100MHz时钟连续输出采样值突发长度是128个点也就是每个触发周期会连续传128个采样点。后端有一个图像处理模块平均处理能力只能按80MHz的时钟连续读走数据。突发间隔相对很长有足够的时间让FIFO排空到接近空。代入公式B128Fwr100MHzFrd80MHz。最坏情况下128个数据全部写入需要128/1001.28微秒。这1.28微秒内读端可以读走1.28×80102.4个数据取整是102个。残留数据量为128-10226个。所以理论最小深度是26考虑标志延迟和FWFT模式的提前预取余量我实际选了64深度的FIFO跑起来完全没问题留了充足的余量。如果突发间隔很短怎么办比如上一次突发刚结束还没排空下一次突发马上又来了。那你需要把FIFO深度提高到“容纳两次甚至三次突发”的水平。这种情况在视频流中很常见我一般的做法是先用公式算出单次突发残量再乘以连续突发次数最后再上浮到2的幂。宁可多留一点也不要等出问题再改改深度意味着IP核重新生成、综合布局重新跑一遍代价很大。5.3 深度选择的其他约束深度除了满足最小容量外还有几件事要留意。一是物理资源的限制BRAM的容量是分块的深度定得不好会导致资源利用率骤降。比如BRAM是36Kb一块你选了一个17位宽的FIFO深度512总共8704bit看起来一块BRAM就够了但BRAM最小物理块可能按18Kb或36Kb对齐17位宽不一定能塞进去实际可能吞掉一整块36Kb。选位宽深度时要下意识地按BRAM最优化组合来定。二是深度和时序的权衡。深度太大意味着RAM的地址位宽更大内部译码和MUX逻辑更复杂最高工作频率会下降。如果你要做500MHz的FIFO深度一般不建议超过1024。深度不够可以级联多个FIFO或者用多通道并行方案而不是一味加大深度。三是非2的幂深度。严格来说Xilinx的FIFO Generator支持非2的幂深度比如96、160。但它会把底层RAM切成多块或者加额外的控制逻辑面积和时序都会受影响。没有特殊需求时深度老老实实取2的幂简单高效没必要特立独行。6. 常见问题与调试心得6.1 问题速查表现象可能原因解决方向复位后前几个数据丢失复位释放后没等待rst_busy拉低就操作用rd_rst_busy/wr_rst_busy做门控FIFO没满但wr_en拉低时数据被覆盖几乎满时没及时反压写端持续写入使用almost full提前流控读出的数据偶发错误FWFT模式下empty时误采dout用empty信号对dout做门控异步FIFO空满标志有毛刺指针同步延迟期间标志抖动正常现象逻辑里预留余量仿真正确但上板丢数布线时序问题或者复位信号劣化检查复位同步降低频率验证dout一直不出数读时钟没有启动或复位一直没释放检查wr_rst_busy/rd_rst_busy释放状态FIFO实际容量比配置小配置了几乎满/几乎空可用窗口缩小重新评估almost标志滞后深度6.2 仿真时怎么验证FIFO行为FIFO的仿真验证有一条标准思路先做“写满读空”测试再按真实读写节奏做混合测试。写满读空测试的目的很明确把FIFO的边界行为全部暴露出来。连续写入深度加50的数据记录哪些被丢弃连续读取直到empty拉高检查dout的数据是否和写入一致。这一步能在仿真阶段发现大部分深度配置和时序问题。然后做混合突发测试模拟真实工作环境。写端每隔一段发一个突发读端按处理模块的速率读取观察wr_data_count和rd_data_count的变化曲线确认FIFO内的数据量始终保持在下限和上限之间不会触底也不会溢出。这一步比单纯检查空满标志更有价值能看出余量是否充足。我习惯在测试平台里加一个自动比较器写入时把数据同时存进一个参考队列读取时自动比对一旦发现值不一致就报错。这样就不用盯着波形一个个数数既省时间又不容易漏。6.3 几个容易忽略的细节与个人体会最后写几个项目里容易忽略的细节。第一个是复位信号的同步。FPGA的复位信号如果直接接在FIFO IP核上必须确保它是同步释放的最稳妥的做法是例化厂商提供的复位同步模块而不是在顶层用assign rst 按钮信号。异步复位、同步释放是标准做法别嫌麻烦。第二个是读写时钟的相位关系。异步FIFO要求读写时钟必须是独立的、没有相位关系的时钟源如果你把同一个时钟的上升沿和下降沿分别用作读写时钟虽然时钟频率一样但也算异步FIFO因为沿之间没有确定性关系。这种用法能做但必须是免费的时钟资源否则时序收敛很痛苦。第三个是数据有效性能。FIFO只是存储数据的容器它不关心数据本身是否有效。有些设计需要传递数据的同时传递数据有效标志、帧起始、帧结束等控制信息这些要么用另一路并行的异步FIFO同步传递要么把控制位打拼到数据位宽里面一起传。我更喜欢后者把数据位宽从8位扩到10位高两位用来表示valid和帧同步这样一个FIFO全搞定了整个系统干净很多。根据我个人的经验FIFO IP核用得好不好关键在三点读透官方手册关于busy信号的章节、在仿真里把空满边界跑透、在系统设计初期就想清楚深度和余量。只要这三点做到位FIFO很少会在调试阶段给别人添乱。如果后续你在项目里遇到FIFO相关的奇怪问题不妨回头检查一下复位时序和almost full的使用绝大多数问题都出在这两个地方。
返回列表