ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA FIFO IP核详解:从同步异步原理到Vivado配置实战

FPGA FIFO IP核详解:从同步异步原理到Vivado配置实战 1. FIFO到底解决什么问题——先搞清楚同步跟异步做FPGA开发搞到一定程度一定会遇到FIFO这个东西。我最早接触FIFO是在做图像采集项目当时用的OV7670摄像头因为传感器不带FIFO像素数据按行连续往FPGA里灌。主控因为要同时处理图像算法和通信协议没法一像素一像素地实时消费数据不加缓存就丢帧。后来在Vivado里拖了一个FIFO的IP核出来把摄像头的输出先丢进FIFO主控这边按照自己的节奏去读问题一下就解决了。但FIFO不是万能的它解决的是特定的一类问题数据流速度不匹配时的缓冲、突发数据的暂存以及两个不同时钟域之间的数据传递。这三种场景分别对应FIFO的两种类型同步FIFO和异步FIFO这也是我每次给别人讲FIFO的时候都会先区分的点。1.1 同步FIFO生产消费速度匹配的搬运工同步FIFO的读时钟和写时钟是同一个时钟也就是说读端口和写端口都工作在同一时钟频率下。听起来简单那它有什么用举个例子。你有一个ADC模块每125个时钟周期采集出一次数据而协议解析模块每次突发需要连续读8个数据。两边时钟相同但是节奏不同一个稀疏一个密集如果不加缓冲协议模块就得等ADC慢慢采效率极低。加一个深度为16的同步FIFOADC把数据攒进去协议模块在需要的时候一下子连续读8个出来吞吐量就上来了。同步FIFO的另一个用处是做数据宽度转换。比如处理器核一次读32位外设一个时钟只能产生8位那就把8位数据连续写4次凑够32位再让处理器读一次这就是“窄写宽读”。也可以反过来宽写窄读。这类FIFO在总线桥、协议适配里用得非常多。从实现上看同步FIFO的读写指针更新逻辑简单因为同源时钟不涉及跨时钟域同步所以亚稳态问题少很多。用IP核生成的时候你只需要关注宽度、深度和几乎满空标志的配置不需要考虑双时钟方面的复杂细节。1.2 异步FIFO跨时钟域数据交接的可靠桥梁异步FIFO就复杂一些了。读写时钟不同甚至频率和相位都完全不同。典型的场景是ADC采样时钟是100MHz以太网MAC工作在25MHz或者FPGA内部有两个时钟域一个跑视频像素时钟一个跑DDR控制时钟两边要交换数据。直接把数据线从一个时钟域甩到另一个时钟域肯定出问题。因为接收边对数据采样的时机是不确定的一旦采样点落在数据翻转窗口上采到的就可能是一个既不是0也不是1的中间电平也就是亚稳态进而导致逻辑错误。异步FIFO就是解决这个问题的标准方案。它的写指针在写时钟域内产生读指针在读时钟域内产生。空满标志的判断需要用到对方的指针于是引入了两级触发器同步加格雷码转换。格雷码的特点是相邻两个数值之间只有一位发生变化即使同步采样时采到中间状态错误的也只是一位不会导致指针严重错位这就是异步FIFO能可靠工作的关键。用IP核生成异步FIFO的好处是这类格雷码转换、两级同步、空满判断逻辑都是经过厂商验证的不用自己重新发明轮子。你只需要选对模式设置好两边的位宽和深度工具就会帮你布线生成。而如果手写异步FIFO很多细节——比如复位释放时序、读指针同步的延时、空满标志的安全余量——一不小心就会踩坑。1.3 什么时候根本不该用FIFOFIFO不是什么地方都好用。我自己见过最典型的问题是把FIFO当成RAM来用。比如想做一个简单的寄存器堆或者随机访问的数据缓存结果有人图省事直接用FIFO。FIFO的读写顺序是严格先进先出的不支持随机寻址想读中间某个地址的数据根本做不到这种场景应该老老实实用Block RAM或者分布式RAM。还有一类场景是数据本身需要逻辑处理之后再输出比如滤波、重排序、协议状态机解析那也不适合单靠FIFO。FIFO只负责搬运和缓冲不做数据内容上的加工。另外如果你的数据速率匹配已经通过握手协议解决了——比如用valid/ready信号做反压读写双方能协商节奏——那FIFO就不是必须的。握手协议本身就有天然的缓冲能力加上FIFO反而会引入额外的存储资源和延迟。判断的核心标准是是否存在“无法协商节奏”的数据流。如果有就上FIFO如果没有握手就够了。2. IP核参数配置实操详解——以Vivado为参考市面上主流FPGA厂商都提供FIFO IP核Xilinx的Vivado里有FIFO GeneratorIntel的Quartus里有FIFO Intel FPGA IP。不同厂商界面不一样但核心参数和设计思路是共通的。我以Xilinx的Vivado为例把关键参数逐个拆开讲清楚。2.1 读写宽度与深度怎么定FIFO的读写宽度和深度是两个最基础的参数。读写宽度比较好理解就是读写端口一次能传输的数据位宽。要注意的是在Xilinx FIFO IP核里读写宽度可以不同比如写8位读32位这在异步FIFO里经常用到。如果读写宽度不同FIFO的深度计算方式会有点绕它是以“写端口宽度×写深度读端口宽度×读深度”这个容量守恒关系来约束的。这里有个容易踩的坑虽然有宽度转换功能但并不是任意比例都支持。FIFO IP核里读写宽度比通常是整数倍关系比如1:2、1:4、1:8反过来2:1、4:1。如果出现3:5这种非整数比例工具会直接不允许配置或者报错。遇到这种情况老老实实先做一次宽度转换FIFO再考虑其他逻辑别硬怼。深度参数也很讲究。同步FIFO的深度可以是任意大于2的数但异步FIFO的深度必须是2的幂次方这是由格雷码编码指针的特殊性决定的。不能设为2的幂时IP核会自动向上取整到最接近的2的幂你实际得到的资源会比预期大一点但取整后的标准深度能保证空满判断的可靠性。所以设计时要提前算好别想配100结果工具给你配成128。深度大小和资源占用成正比。FIFO在FPGA里有两种实现方式一种是基于Block RAM适合大容量但只在读或写时工作占用BRAM资源另一种是基于LUT和触发器适合小容量占用逻辑资源。IP核会根据你配置的容量自动选择实现方式。容量特别小比如16×8用BRAM反而浪费工具会默认用分布式RAM。2.2 标准模式与First-Word Fall-Through模式这是FIFO IP核里一个很实用但是新手经常搞不清楚的选项。Xilinx里对应“Standard FIFO”和“First-Word Fall-Through”通常简称为FWFT。Intel的IP里类似的概念叫“Show-Ahead模式”。标准模式下FIFO空的时候读数据线上是无效数据你必须先拉高读使能等待一个周期或几个周期之后数据才出现在输出总线上。读操作是“先请求后得到”的读延迟不可忽略通常有一个时钟周期的延迟。而FWFT模式下只要FIFO不空第一个数据就已经摆在输出总线上了不需要先拉读使能。你需要做的只是在读到有效数据后给出读使能让FIFO把下一个数据顶上来。这两个模式各有适用场景。标准模式逻辑简单不容易出错适合一般的数据缓冲。FWFT模式特别适合那种“数据流动持续不断、处理单元准备好就随时取数据”的流水线场景。我一个做图像的行缓存就用FWFT像素数据在输出上一摆算法模块直接拿减少了一个周期的等待。但FWFT也有麻烦。由于数据提前摆出来你得额外监控空信号确保拿到的确实是有效数据。如果逻辑处理不谨慎很容易把空状态的无效数据也读走了导致数据错位。2.3 满标志与almost full的取舍FIFO IP核默认提供full和empty标志这两个标志是精确标志对应FIFO真正写满或读空的状态。但是实际设计中你往往不希望等FIFO完全满或者完全空才做反应。比如你写数据的模块是一个持续不断的生产者等它发现FIFO满了再停下来数据其实已经丢了因为写请求已经发出去了。这时候需要提前通知生产者“马上要满了你准备停”这个提前的通知就是almost full。同理消费者这边可以用almost empty提前知道数据快没了。almost full和almost empty的可配置范围在IP核里是用“Full Threshold Value”和“Empty Threshold Value”来控制的。你可以精确设定一个阈值比如写深度是1024设almost full阈值为1000那么当FIFO里数据量到1000的时候almost full信号拉高你还有24个周期的余量来处理背压。这个阈值怎么设取决于你的通路延迟。数据源从收到almost full信号到真正停止写入通常需要若干个时钟周期这中间包括信号同步、状态机跳转、输入数据链路的清洗等。阈值至少要比这个停止延迟大否则不到位。我见过不少人把almost full阈值设得太靠近满值结果数据源来不及刹车就溢出。建议把这个停止延迟估算出来再乘以1.2~1.5的安全系数作为阈值距满值的间隔。2.4 复位FIFO IP核最容易被忽略的一环FIFO复位看似简单实际是事最多的一个环节。Xilinx的FIFO IP核支持异步复位而且对复位信号的释放时机有要求。因为FIFO内部有读写两个时钟域的指针同步逻辑如果你的复位释放不干净或者只复位了一个时钟域那FIFO的指针就会错乱。我在调试中实际遇到过一个问题板子上电后FIFO在第一个数据周期就出现读空标志异常后来定位到是复位信号只连到了写时钟域的复位端读时钟域的复位端悬空或没接好。还有一个关键点FIFO复位之后不能立刻写入。Xilinx的FIFO IP核在复位完成后内部还有一段初始化时间这段时间内FIFO不处于正常工作状态。虽然IP核在复位信号释放后很快拉低一些内部标志但从复位完成到读空、写满标志真正有效中间有一个窗口。稳妥的做法是复位释放后等待至少几个时钟周期再开始正常读写操作。这个等待时间在IP核数据手册里有说明不同型号略有差异。异步FIFO的复位尤其讲究。读写两侧的复位信号需要同时释放或者至少有足够的同步逻辑确保在各自时钟域内都能正确撤销复位。Xilinx的IP核内部已经做了这种处理但你的外部复位信号不能随便做一个全局复位按钮就接上去要看清楚时序要求再设计。3. 完整实操从建核到跑通一个同步FIFO理论讲再多不如动手做一次。下面以Vivado为例完整走一遍FIFO IP核的生成、例化、测试流程。3.1 在Vivado里生成FIFO IP核打开Vivado在左侧Flow Navigator找到IP Catalog搜索“FIFO Generator”双击后进入配置界面。第一步选接口类型。默认Native接口就够了如果你的设计要用到AXI Stream也可以选AXI4-Stream但我个人建议初学者先玩Native因为时序简单直观方便理解FIFO的工作原理。第二步配置读写宽度和深度。假设做同步FIFO读写都32位深度512。在“Read Data Count”和“Write Data Count”选项上建议都勾上。这两个选项会输出FIFO当前的数据计数器调试时看它比只看空满标志直观得多。第三步设置标志信号。除了基本的Full和Empty我建议加上Prog Full/Prog Empty也就是可编程满空标志这个对应的其实就是前文说的almost full/almost empty。在“Programmable Full Type”里选“Single Programmable Full Threshold Constant”然后在下面填入阈值比如480。Programmable Empty也同理设成32。第四步在Reset选项里选“Asynchronous Reset”这个跟设计里的复位方式匹配。生成以后IP核会给你一个例化模板。咱们直接进下一步。3.2 同步FIFO的例化与激励测试假设生成的是32位宽、512深的同步FIFO例化代码大概长这样fifo_32x512 u_fifo_32x512 ( .clk(clk), // 输入时钟 .rst(rst), // 异步复位 .wr_en(wr_en), // 写使能 .din(wr_data), // 写数据 .full(full), // 满标志 .almost_full(almost_full), .prog_full(prog_full), .wr_data_count(wr_data_count), // 写侧数据计数 .rd_en(rd_en), // 读使能 .dout(rd_data), // 读数据 .empty(empty), // 空标志 .almost_empty(almost_empty), .prog_empty(prog_empty), .rd_data_count(rd_data_count) // 读侧数据计数 );这里有个容易犯的错误同步FIFO的读数据在使能之后的时序是怎样的在标准模式下rd_en拉高后dout在下一个时钟沿才输出对应数据也就是读操作有一个时钟周期的读取延迟。很多新手第一次写测试激励rd_en和dout同时打拍比对结果发现数据对不上就是因为忽略了这拍延迟。写一段简单的测试激励流程是复位→写状态机连续写入16个数据→切换到读状态机连续读出16个数据→比对读出的数据是否正确。仿真结果里只要wr_data_count和rd_data_count的变化符合预期读出的数据与写入一致FIFO基本就没问题。然后把prog_full和prog_empty的触发条件也验证一下用仿真波形确认阈值是否在预期位置翻转。3.3 异步FIFO的跨时钟域例化要点异步FIFO的例化和同步FIFO最大的区别在于多了一个写时钟和读时钟。假设写侧时钟是100MHz的adc_clk读侧时钟是25MHz的mac_clk那么例化是这样fifo_async_16x256 u_fifo_async ( .wr_clk(adc_clk), .rst(rst), .wr_en(adc_wr_en), .din(adc_data), .full(adc_fifo_full), .rd_clk(mac_clk), .rd_en(mac_rd_en), .dout(mac_rd_data), .empty(mac_fifo_empty) );异步FIFO例化时有几个坑必须小心。一个是wr_en和rd_en信号的时钟域归属必须明确。wr_en必须由wr_clk域的逻辑产生rd_en必须由rd_clk域的逻辑产生。绝不能把读时钟域的信号接到写使能上或者反过来这会导致内部指针采样的时序混乱。这个问题在编译时不会报错仿真也偶尔能过但上板之后就是随机性故障非常难查。另一个是复位信号。异步FIFO的复位虽然是异步复位但复位信号的撤销需要分别满足两个时钟域的时序要求。用同一个外部复位信号接在rst上IP核内部会自己处理但如果外部复位本身没有做去抖或由某个时钟域同步建议在顶层设计里把复位信号统一处理一次再做全局分配。跨时钟域的数据数目问题进行仿真时还要注意一个现象wr_data_count和rd_data_count在异步FIFO里增量不同步。写侧到达一个数据rd侧的计数不会立刻变化中间存在同步延迟。这是正常的在异步FIFO里数据数目的实时一致性本来就不存在只需要确保空满标志的准确性就可以了。3.4 验证与约束别忘了时钟域检查异步FIFO用起来之后还有一道工序是很多新手容易忽略的——跨时钟域约束。在Vivado里异步FIFO属于典型的CDC路径。你应该在约束文件里把异步FIFO内部的跨时钟路径设为false path或者添加set_clock_groups约束告诉工具这些路径不需要做时序收敛。这里有个细节不要直接对FIFO内部节点设set_false_path更规范的做法是用set_clock_groups把两个异步时钟分组隔离开。比如set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks adc_clk] \ -group [get_clocks -include_generated_clocks mac_clk]这样做的好处是清晰明了后续时序报告里不会出现一堆因为异步跨时钟域导致的违例看着也舒服。另外在综合或实现后用Vivado自带的Report CDC检查一下看FIFO的同步逻辑是否被正确识别。正常的话你会看到工具把FIFO内的跨时钟路径标记为已同步路径不会报时序违例。4. FIFO IP核常见问题与排查技巧FIFO看似简单实际调试中遇到的问题五花八门。这里把我踩过和见过的典型问题整理成一个速查表并逐个展开讲。4.1 空满标志异常数据读不出来症状FIFO明明写入了数据但empty一直为高或者rd_en拉高却读不出数据。排查第一步看写使能是否真的被有效拉高了。很多人写了状态机但在某个时钟周期里wr_en和valid没有对齐导致数据压根没写进去。这个用wr_data_count就能看出来——如果写计数一直为0那肯定没写进去。第二步查复位后等待时间。IP核释放复位后有一段初始化时间如果你在复位结束后立刻写入可能写入动作被忽略。建议复位结束后空等8~16个时钟周期再开始操作。如果你用的时钟频率比较低多等一点没关系。第三步看你的读逻辑有没有真的判断空标志。有些人用FWFT模式读逻辑没判断empty直接拉rd_en去读结果读出的是空状态下的无效数据看起来就像是“数据读不出来但出来的都是坏的”。4.2 跨时钟域全是X态或不确定值异步FIFO仿真时出现大量X态大概率是仿真里复位没有正确初始化。检查复位信号是否有足够宽的脉冲而且在复位期间读写请求都应该被关掉。还有一个常见的坑读写时钟明明是异步的但你在仿真里用同一个always块同时触发了两个时钟。这虽然不是FIFO本身的问题但会导致仿真行为异常。异步FIFO仿真时读写侧的激励逻辑应该分开写最好用不同的initial块配合(posedge wr_clk)和(posedge rd_clk)来分别产生信号。另外如果仿真结果显示FIFO的full很快就拉高了而且数据量远小于深度那多半是写侧时钟和读侧时钟的频率关系设置不合理。比如读侧时钟远快于写侧而你的测试环境里读逻辑又没拉读使能FIFO自然会被填满。4.3 FIFO深度选错吞吐性能不达标这个不是错误而是设计问题。FIFO深度选多少有个常用公式在突发写入场景下FIFO最小深度等于突发长度减去突发期间能够被读走的数目。更严格一点深度要能覆盖“最坏情况下的写入量与读取量之差”。比如一个场景突发写入4096字节写入速率是一个时钟写1字节读取速率是每4个时钟读1字节不考虑其他因素简单计算突发期间写了4096字节读了4096/41024字节那么FIFO至少需要4096-10243072字节深度取整到2的幂就是4096。如果实际应用中读取还有暂停那要把暂停时间内新写入的数据也算进去。我做接口设计的时候习惯在理论值基础上再加1.5倍余量宁可多占一点BRAM也不要因为突发节奏抖动导致溢出丢数据。4.4 例化后资源占用比预期高如果发现FIFO的资源占用比预想中高不少先检查是不是不小心配了带有“Dout Reset Value”或“Output Register”的选项。Output Register会在输出端额外打一拍改善时序但增加一个寄存器和相关逻辑不是必须的话可以去掉。还有一个资源大头是“Data Count”信号。读取wr_data_count和rd_data_count需要额外的逻辑来统计计数如果你只是调试用不需要长期保留尽量在调试完成后注释或配置掉这些端口能省不少LUT。4.5 常见问题速查表问题现象可能原因快速排查方法empty一直为高写使能未生效 / 复位后过早写入检查wr_data_count是否增长full提前拉高读使能未拉 / 读侧时钟太慢检查rd_data_count模拟突发节奏读出数据错位FWFT模式下未判断empty增加empty判断逻辑跨时钟域仿真出现X态复位时序不对 / 激励跨时钟域触发分开读写时钟域激励块资源占用过高开了Output Register / Data Count按需裁剪配置5. 用IP核还是手写FIFO这个问题被问过无数次。我的答案取决于场景和你的开发阶段。5.1 什么时候必须用IP核产品级项目、时间紧、对可靠性要求高的场景直接用IP核是最稳妥的。IP核经过厂商的充分验证内部处理了异步FIFO的格雷码转换、两级同步、空满判断等关键逻辑这些都是经常出错的地方。厂商还会帮你处理好复位时序、初始化逻辑你拿到手里例化即可用极大缩短验证时间。另外IP核经过厂商的物理实现优化能帮助你的设计更好地满足时序要求。特别是异步FIFO跨时钟域的路径规划不是随便写个Verilog就能轻易收敛的IP核内部的结构经过专门优化时序表现更好。用IP核还能享受厂商工具链的额外支持。比如在Vivado里FIFO IP核的仿真模型和综合实现结果一致能直接在仿真里查看内部状态调试体验非常好。5.2 手写FIFO什么时候反而更好但IP核也有它的不便之处。它的接口固定有些特定需求它适应不了。比如你想在FIFO里做带优先级的读取或者需要支持动态修改深度或者想把多个FIFO合并成一个特殊结构IP核的固定接口就很难搞。教学和原型验证阶段手写一个简单的同步FIFO或者用一个标准模板的异步FIFO能帮你更深入地理解FIFO的工作原理调试起来也更灵活。很多面试官问“给出一个FIFO的Verilog代码”其实就是想看你有没有理解指针、空满判断和格雷码同步这些核心知识。对于深度较浅的小FIFO手写代码生成的分布式RAM和IP核生成的逻辑差距不大。这种情况下手写反而省去调用IP核的流程工程更清爽。手写异步FIFO时我习惯直接套用业界成熟的两级同步加格雷码架构。关键点在于读写指针都要转换成格雷码后再做跨时钟域同步空满判断要分别使用写时钟域和读时钟域同步后的指针写满判断用的是写指针和同步后的读指针读空判断用的是读指针和同步后的写指针这一点很多初学者容易搞反。5.3 我的最终建议实际项目开发我默认用IP核。IP核省下来的时间和精力用来看时序报告、调接口更值。跨时钟域这种事交给验证过的模块比交给手写代码让人放心得多。如果你是非生产环境或者想加深理解手写FIFO是一个特别好的练习完成一个经过时序验证的异步FIFO之后你对整个跨时钟域设计、CDC同步、格雷码这些概念的理解会上一个台阶。另外提一下不同厂商IP核在细节上各有差异。Xilinx的FIFO Generator、Intel的FIFO Intel FPGA IP、高云、易灵思等国产FPGA厂商也都有自己的FIFO IP核界面名字可能不同但核心参数和设计思路大同小异。换平台的时候不要照搬旧配置把数据手册的关键时序图认真读一遍尤其是复位和读使能的时序关系能帮你省下后面一整周的调试时间。就我的经验来说FIFO IP核是个典型的“会用不难用好很难”的东西。很多问题不是你写代码写错了而是对IP核的工作细节理解不到位。做任何一个FIFO相关设计多花一点时间把配置页面里每个选项的含义搞清楚再看一遍对应厂商的Product Guide里“时序图”的章节后面调试能少走很多弯路。
返回列表