ARTICLE DETAIL

资讯详情

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

ZYNQ跨时钟域设计实战:AXI-Stream FIFO解决数据链路同步

ZYNQ跨时钟域设计实战:AXI-Stream FIFO解决数据链路同步 1. 问题到底出在哪ZYNQ数据链路上的跨时钟域1.1 一个典型的ZYNQ数据链路接触ZYNQ的人都知道这芯片最迷人的地方就是PS和PL能协同干活。但踏实干过几个项目后你就会发现ZYNQ项目里最磨人的往往不是功能逻辑本身而是数据在不同时钟域之间传递时导致的偶发错乱。我之前做过一个高速采集项目PL侧挂了ADC采样时钟是125MHzPS侧通过DMA把数据搬进DDR做后续处理而DMA所挂的AXI总线时钟是100MHz。ADC采集的数据需要经过一个简单的下采样、加包头、组帧逻辑再送到DMA的S2MM通道。一开始我想得挺简单ADC侧的逻辑是125MHzDMA侧是100MHz中间加个FIFO不就完事了吗可真等系统跑起来数据帧偶尔会错位一个字节有时候一帧数据里出现上帧的尾巴定位了整整两天才意识到问题出在跨时钟域同步这个看似“解决了”的环节上。这个场景其实非常典型。ZYNQ项目里跨时钟域几乎无处不在PL侧不同外设的接口时钟不同ADC、DAC、以太网PHY、视频像素时钟各有各的频率PS与PL之间的AXI接口时钟与PL逻辑时钟不同多个PL逻辑模块之间可能存在数据流从一个Clock Domain传入另一个Clock Domain的情况。数据只要跨一次时钟域就必然要面对两个问题一是亚稳态二是多bit数据在不同时钟沿下的位对齐。前者可能导致触发器采到不确定的电平后者则可能让一条32bit总线传过去之后高16bit是本次更新的数据、低16bit还是上一次的数据直接拼出一朵“奇葩”数据。1.2 为什么“直接连过去”行不通很多新手会问FPGA里跨时钟域寄存器打两拍不就行了打两拍确实是单bit信号的经典处理方式也就是两级寄存器同步用来降低亚稳态传播到后级逻辑的概率。但对多bit并行总线而言直接打两拍是行不通的。举个例子一个32bit计数器在125MHz下从0x0000FFFF加1变成0x000100008个bit同时翻转。如果接收端时钟是100MHz它可能在某个时钟沿抓到一部分新bit、一部分旧bit比如0x0001FFFF、0x00000000这类完全错误的中间值。不管你怎么打拍打出来的都是错值只是把错误稳定地传播了出去而已。所以对于多bit数据跨时钟域业界常用的思路无非三种异步FIFO、握手加数据保持、以及借助Xilinx原语或IP完成安全传递。其中异步FIFO是最通用、最稳妥的因为它内部用格雷码对读写指针做了跨时钟同步保证任意时刻指针变化只有一个bit翻转从而避免了多bit同时跳变导致的采样错误。ZYNQ里的AXI-stream FIFO正是这样一个自带跨时钟域处理能力的标准接口IP。我见过不少项目数据流本身是标准的AXI-Stream但做跨时钟域时非要手搓一个双口RAM加一坨握手逻辑最后调时序调到怀疑人生。后面我会详细解释为什么不建议这样做。2. 方案怎么选为什么是AXI-stream FIFO而不是手写异步FIFO2.1 三种常见跨时钟域方案的对比在ZYNQ PL逻辑里做多bit数据跨时钟域我经常被问到的问题就是XPM_FIFO、手写异步FIFO、FIFO Generator里面的AXI-Stream FIFO到底选哪个我自己的判断标准比较直接看接口协议和集成成本。方案接口类型跨时钟域支持集成成本适用场景手写异步FIFO自定义读写信号需要自己处理格雷码、空满标志、复位域高容易出错对资源极致敏感、接口自定义的项目XPM_FIFOXilinx参数化宏简单读写接口类似BRAM时序内置CDC已处理中比手写安全很多内部模块之间简单数据缓存FIFO GeneratorAXI-Stream模式标准AXI-Stream握手内置可配置Independent Clocks低即插即用需要对接AXI-Stream链路、DMA、VDMA的场景粗看之下XPM_FIFO似乎也够用。但如果你后续要把数据接给DMA、VDMA或者和Xilinx的其它AXI-Stream IP互联XPM_FIFO那套读使能、写使能接口反而要额外转换逻辑。与其自己封装一层标准握手不如直接用FIFO Generator的AXI-Stream模式接口天然就是s_axis_*和m_axis_*往DMA的S2MM通道一怼就能跑。再说手写异步FIFO。对于学习FPGA的人来说手写一个异步FIFO绝对是很不错的训练项目格雷码指针、空满判断、两级同步器每一步都值得亲手走一遍。但在工程项目里我强烈不建议动不动就手搓。原因很实在格雷码指针的二进制转换逻辑看着简单但空满标志的生成时机、读写指针比较的跨域延迟稍有不慎就引入死锁或溢出复位释放与两个时钟域的关系难处理异步复位、同步释放的方案不同复位释放时间会导致空满标志短暂错误你手写代码的验证时间和排错时间大概率超过项目排期能给你的余量。2.2 AXI-stream FIFO真正好用的几个点FIFO Generator的AXI-Stream模式本质上仍然是异步FIFO只不过Xilinx把所有容易出错的部分都替你处理好了同时对外开放的接口又恰好是AXI-Stream标准握手。它的几个优势在实际项目里非常值钱第一读写时钟完全独立。在配置界面里选择Independent Clocks写端口用s_axis_aclk读端口用m_axis_aclk两个时钟没有任何相位或频率关系要求FIFO内部自己完成指针域的转换。这个特性正好是我们解决PS-PL跨时钟域最需要的。第二标准握手信号天然支持反压。tvalid/tready这套逻辑本身就是设计给数据流做流量控制的。上游写入再快只要FIFO快满s_axis_tready就会拉低上游逻辑看到tready0就必须暂停发送。下游读取慢时m_axis_tvalid持续拉高、数据待在读端口不动绝不会丢。这套机制比自定义的fifo_wr_en/fifo_full明确得多因为tvalid/tready的握手规则在AXI-Stream协议里是严格定义的不会出现“数据写进去了但full标志还没拉高导致又写一拍”的临界问题。第三水位信息可以直接用于流控。FIFO Generator可以输出axis_wr_data_count和axis_rd_data_count分别反映两个时钟域中的数据水位。这个特性非常实用我可以用它做软流控比如当写侧水位超过阈值时不再向FIFO写数据而是先暂停采集逻辑或向CPU上报缓冲高水位事件。3. 动工前的功课AXI-Stream握手时序与FIFO Generator关键参数3.1 握手必须先讲透tvalid/tready的几种情况AXI-Stream的握手规则只有一条但这一条就能劝退不少人只有当tvalid和tready同时拉高时一个数据传输才算完成。具体来说发送方拉高tvalid表示数据有效接收方拉高tready表示可以接收。二者都高时数据被“锁定”传递只要有一个为低当前拍就不算传输。这里有几个边界情况我在项目里看到很多人踩过tvalid拉高后数据必须一直保持到tready拉高。发送方不能因为自己觉得“我数据早准备好了”就中途改变tdata否则接收方采到的可能是一拍混合数据。tready可以在任意时钟沿拉低。对于接收方来说这相当于反压信号所以发送方的状态机必须支持“tvalid已经拉高但tready还没来”的等待状态数据要一直拿着、保持到握手完成。AXI-Stream协议规定tvalid不能依赖tready来产生。也就是说发送方不能等到看见tready1之后才去拉tvalid这会产生组合逻辑环在时序上极其难看。在实际工程里我经常看到有人用assign tvaild tready这种写法仿真没问题上板直接时序违例或者行为诡异。FIFO的tready行为由IP内部逻辑产生你基本不用操心它会违反协议。真正需要上心的是你的上游发送逻辑和下游读取逻辑这两个逻辑如果不严格遵守握手规则FIFO再稳也会被外面的错误时序带偏。3.2 FIFO Generator配置项逐个过一遍在Vivado里添加FIFO Generator IP打开配置界面后会看到非常多的选项。很多第一次用的人会被这些选项唬住其实要紧的就那么几个配置项我的推荐值说明Component Nameaxi_stream_fifo_0叫什么都行但建议带项目语义FIFO InterfaceAXI-Stream核心选项FIFO ImplementationIndependent Clocks跨时钟域场景必须选这个Read Data Width按需如32读写数据位宽通常保持一致Write Data Depth512、1024等深度需结合带宽估算后面细说Read Data Count开用于水位监控Output Register开或者选Registered改善读端口时序代价是加一拍延迟Full Threshold可留默认或按需设置用于提前拉低tready的高水位阈值特别提一下Output Register选项。开启了输出寄存器后读端口相当于在FIFO内部RAM数据输出后再加了一级寄存器把读数据路径从RAM输出时序解放出来改善了对下游的时序收敛。代价是m_axis_tvalid会相对数据实际读出的时刻延迟一个周期左右同时空标志的响应也会变慢。在跨时钟域场景中这个延迟相对于系统的吞吐来说通常可以忽略所以我在大多数项目里都会开启。还有一个容易忽略的点AXI-Stream FIFO的tkeep信号。如果你配置成32bit数据宽度tkeep是4bit正常的整字传输时全部为高。但要注意FIFO Generator并不会帮你做“跨位宽”的字节使能映射如果上游只希望DMA接收有效字节比如某个包只有3字节你必须在发送时自己管理好最后一个word的tkeep然后原样送到FIFO。FIFO不解析、不修改tkeep它只是把这个信号当成随路控制信号从读端口原样呈递出来。这一点在组帧封装时非常关键。3.3 FIFO内部分层机制格雷码怎么帮你守住亚稳态既然是用异步FIFO思路做跨时钟域就绕不开内部的空满判断问题。FIFO Generator内部维护读写两组指针各自在自己的时钟域里累加。空满比较必须跨时钟域拿对方的指针过来比较而指针是多bit的直接跨域打两拍又会遇到多bit同时翻转的问题。Xilinx的做法是把二进制指针转成格雷码格雷码的特点是计数器加1时只有1个bit发生变化。这样把格雷码指针经过两级同步器传到对侧时钟域时即使采到的是变化前或变化后的瞬间值也不会采出“中间乱码”因为任意时刻只有一个bit在变化。对侧拿到同步后的格雷码指针再转回二进制与本地指针做比较从而判断空满状态。理解这个机制对排错很有帮助。比如你会看到FIFO读侧明明还有数据m_axis_tvalid却短暂拉低了一拍这很可能不是FIFO坏了而是写指针同步到读时钟域有延迟读逻辑在某一瞬间认为“可能空了”。这种延迟天然存在属于异步FIFO的正常行为。后面我会讲这也是反压设计时要注意的点。4. 完整实现一个采集链路跨时钟域设计的Verilog实例4.1 顶层设计框图和时钟域划分假设现在的项目背景是ADC以125MHz时钟输出连续采样数据组帧逻辑把连续采样打包成64字一帧每帧末尾附带tlast。DMA在100MHz的AXI总线时钟下通过S2MM通道读走FIFO里的数据到DDR。两个时钟域在这里交汇中间正好用AXI-stream FIFO做隔离。顶层模块可以这样划分写侧时钟域125MHzadc_clk驱动ADC接口和组帧逻辑组帧逻辑产生s_axis_tdata、s_axis_tvalid、s_axis_tlast送入FIFO的写端口。FIFO本体内部双口RAM/分布式RAM读写指针各自独立通过格雷码同步跨时钟域判断空满。读侧时钟域100MHzdma_clk驱动FIFO读端口m_axis_tdata、m_axis_tvalid直接输出到DMA S2MM通道DMA返回m_axis_tready。这里的要点是组帧逻辑和读取逻辑分别只跟自己时钟域内的信号打交道不要出现任何跨时钟域的直接寄存器访问。所有跨时钟域的“脏活”都交给FIFO完成。4.2 关键代码AXI-stream FIFO例化和组帧逻辑下面给出一段我在项目中使用的简化版代码覆盖FIFO例化、写侧组帧、读侧接口。这段代码基于FIFO Generator生成的IP核实例名为axi_stream_fifo_0。module cdc_fifo_example #( parameter DATA_WIDTH 32, parameter PKT_WORDS 64 )( input wire adc_clk, // 写侧时钟 125MHz input wire adc_rstn, // 写侧复位低有效 input wire [DATA_WIDTH-1:0] adc_data, // ADC采样数据 input wire adc_valid, // ADC数据有效 input wire dma_clk, // 读侧时钟 100MHz input wire dma_rstn, // 读侧复位低有效 output wire [DATA_WIDTH-1:0] dma_data, // 输出到DMA S2MM output wire dma_valid, input wire dma_ready, output wire [11:0] fifo_wr_count, // 写侧水位用于观察 output wire [11:0] fifo_rd_count // 读侧水位用于观察 ); reg [DATA_WIDTH-1:0] wr_data_buf; reg wr_valid_buf; reg wr_last_buf; reg [5:0] wr_word_cnt; wire s_axis_tready; wire wr_allow; // 高水位控制FIFO剩余容量不足1帧时暂停写入 assign wr_allow (fifo_wr_count (4096 - PKT_WORDS)); always (posedge adc_clk or negedge adc_rstn) begin if (!adc_rstn) begin wr_data_buf {DATA_WIDTH{1b0}}; wr_valid_buf 1b0; wr_last_buf 1b0; wr_word_cnt 6d0; end else begin // 默认清掉已经完成握手的valid if (wr_valid_buf s_axis_tready) begin wr_valid_buf 1b0; wr_word_cnt (wr_word_cnt PKT_WORDS - 1) ? 6d0 : wr_word_cnt 1b1; end // 产生新一拍的组合条件ADC有数据且FIFO的水位允许 if (adc_valid wr_allow !wr_valid_buf) begin wr_data_buf adc_data; wr_last_buf (wr_word_cnt PKT_WORDS - 1); wr_valid_buf 1b1; end end end axi_stream_fifo_0 u_fifo ( .s_axis_aclk (adc_clk), .s_axis_aresetn (adc_rstn), .s_axis_tvalid (wr_valid_buf), .s_axis_tready (s_axis_tready), .s_axis_tdata (wr_data_buf), .s_axis_tlast (wr_last_buf), .s_axis_tkeep (4b1111), .m_axis_aclk (dma_clk), .m_axis_aresetn (dma_rstn), .m_axis_tvalid (dma_valid), .m_axis_tready (dma_ready), .m_axis_tdata (dma_data), .m_axis_tlast (dma_last), .m_axis_tkeep (dma_tkeep), .axis_wr_data_count (fifo_wr_count), .axis_rd_data_count (fifo_rd_count) ); endmodule这段代码里值得注意的有几点。wr_valid_buf的清除逻辑我在每个时钟周期开始先判断上一拍的wr_valid_buf是否已经和tready握手成功如果成功就清除valid。这样当前拍如果有新的ADC数据才能继续发起下一次写入。这个处理方式保证了每个ADC数据只进FIFO一次不会因为tready长时间拉低而反复写入同一份数据。wr_last_buf的生成时机tlast必须在当前帧最后一个word发出时伴随该word一起拉高。这里用wr_word_cnt判断当前word在帧内的位置当wr_word_cnt PKT_WORDS - 1时说明当前要写入的是第64个word因此wr_last_buf拉高DMA看到tlast后就会结束当前描述符对应的传输。4.3 读侧与DMA对接为什么说tready反压是福利读侧逻辑相比写侧简单很多因为AXI-stream FIFO已经帮我们把FIFO返回值转换成了标准的握手机制。DMA S2MM通道本身就是AXI-Stream从机它会根据内部缓冲状态拉高或拉低dma_ready。我在这块踩过一个不大不小的坑第一次做ZYNQ采集系统时我在FIFO和DMA之间又加了一层寄存器打拍来同步tdata结果因为打拍导致tvalid沿数据多延迟了一拍DMA总是少收到数据。后来才想明白AXI-stream FIFO比起普通异步FIFO好的地方就在于它把握手协议内化了你根本不需要担心数据路径上的打拍问题直接把m_axis_tdata接到m_axis_tdata就行。如果你非要在这中间做点处理比如数据加密、格式转换、Byte交换我建议用AXI-Stream Data Width Converter、AXI-Stream Subset Converter这类官方IP而不要自己手写comb逻辑插在中间。自己手写的逻辑一旦在握手时序上有疏忽表现出的故障会比跨时钟域本身还难查。5. 板级实测避坑tready假低、复位不同步、tlast丢失的根因5.1 现象一FIFO明明没满s_axis_tready却长时间拉低有段时间我的采集系统出现了莫名其妙的吞吐下降测下来125MHz的采样时钟DMA搬运速度却连一半都跑不满。ILA抓FIFO写侧信号发现s_axis_tready经常长时间拉低但axis_wr_data_count显示FIFO里根本没有多少数据。这其实是我自己代码造成的写侧水位控制条件fifo_wr_count (4096 - PKT_WORDS)里我把4096写成了1024这个配置值而FIFO实际深度是4096。本意是防止FIFO满时写入导致数据丢失结果因为阈值设得太低FIFO里还剩大几百个word时就开始反压。别笑这种“配置深度和代码参数不一致”的错误比逻辑错误更隐蔽因为仿真里未必会跑到这么满。这也引出一个排查经验遇到tready异常拉低第一步是确认数据计数信号的位宽和FIFO深度配置是否一一对应。axis_wr_data_count的位宽由Vivado根据FIFO深度自动生成深度4096时位宽是12bit最大计数4095。如果你的比较逻辑里手工写了一个大于实际深度的常数或者把12bit信号和16bit常量比较很容易出现逻辑永远不成立的情况。5.2 现象二数据偶发错位一帧数据会串到下一帧这个问题的表现比5.1隐蔽得多。系统大多数时候工作正常但偶尔会有一帧的开头混入上一帧结尾的几个字节。刚开始我怀疑是DMA描述符配置问题查了半天无果。后来用ILA同时抓FIFO写侧和读侧的tdata、tvalid、tlast对比后发现写侧产生的tlast和下一帧第一个word之间存在一个时钟周期里wr_valid_buf并没有完全复位的情况。根因在于我最初写的组帧逻辑里tlast拉高后立刻清零wr_word_cnt清零后的下一个ADC valid到来时由于状态机的分支覆盖不全对wr_valid_buf的处理多了一拍。FIFO接受了本应作为下一帧首个word的数据读侧自然就把上一帧tail和下一帧head连在一起了。解决方法是把wr_last_buf的生成与wr_word_cnt更新放在同一个分支里并且强制在tlast握手完成后让wr_valid_buf至少保持一个周期的低电平确保下一帧和上一帧在时间上彻底划清界限。5.3 现象三DMA通道卡死等不到tlast还有一种板级故障让我印象很深DMA搬运偶尔会整个通道卡死描述符停在Partially Transferred状态不再前进。用ILA抓读侧信号看到m_axis_tvalid一直为高m_axis_tready也一直为高但tlast就是不来DMA只好一直等。这个问题的根源还是tlast没有和帧内最后一个数据对齐。在我的实现里wr_last_buf依赖wr_word_cnt的值但如果wr_word_cnt在tready拉低期间被意外清零或者上游数据源的adc_valid在帧边界处恰好断了一拍就会导致当前帧最后一个word发出时wr_last_buf仍然是0而下一帧第一个word反而带了tlast。DMA等不到tlast自然不结束传输。从这里我总结出的工程习惯是像tlast这种帧边界信号必须在写入数据改变时一起锁存不能用“先更新计数器、下一拍再产生tlast”的方式。最好是数据、last、valid三者在同一个时序逻辑里同时输出保证进入FIFO的每一拍都是完整自洽的。5.4 排查链路实录从ILA抓信号到定位复位的坑除了协议上的时序问题跨时钟域FIFO还有一个很容易被忽略的点就是复位信号的释放。FIFO Generator要求s_axis_aresetn和m_axis_aresetn各自在自己的时钟域内同步释放否则上电或软复位之后FIFO内部状态可能出现一侧已经离开复位、另一侧还在复位中的情况导致空满标志短暂乱跳。我遇到过最典型的故障是系统刚上电时第一次DMA传输偶尔正常偶尔读到的全是0。抓信号看到m_axis_tvalid一直没拉高但axis_rd_data_count又显示里面是有数据的。最后定位到是CPU软复位流程问题PS只复位了PL侧的读时钟域而写时钟域没有复位FIFO内部的两个域状态不一致读侧认为FIFO为空。从那之后我在设计里约定了一条铁律FIFO两侧的复位信号必须都来自同一个复位源且分别在各自时钟域内做同步释放。在ZYNQ里我一般用proc_sys_resetIP为每个时钟域生成独立的peripheral_aresetn保证所有FIFO写侧复位由写时钟域的reset模块统一产生读侧复位由读时钟域的reset模块统一产生。这样虽然复位信号不同名但来源是一致的只要两个reset模块都输出释放两侧就能对齐。6. 设计之外的工程心得深度估算、水位反压与仿真验证6.1 FIFO深度不是拍脑袋定的很多工程师定FIFO深度就一个原则越大越稳。这句话在资源不紧张时有一定道理但在ZYNQ里AXI-stream FIFO如果使用BRAM实现深度越大占用的BRAM块越多可能挤占图像缓存、数据缓冲的BRAM预算。FIFO深度应该按最坏情况算出来而不是随手填1024或4096。带宽估算的核心是在任意足够长的时间窗口内写侧平均速率不能超过读侧平均速率否则FIFO必然溢出深度只能吸收短时突发差异。以一个具体例子算一遍写侧时钟125MHz数据32bit理论带宽500MB/s读侧时钟100MHz数据32bit理论带宽400MB/s如果写侧连续发送读侧也持续接收长期来看写入速率大于读出速率深度再大也扛不住但如果写侧是突发性的比如采集逻辑每1ms产生一次64-word的突发帧那么突发期间写入500MB/s但一个64-word突发只有256字节在1ms周期内平均速率只有0.256MB/s远低于读侧带宽。此时FIFO深度只要能装下几次突发之和即可。我的经验公式是FIFO深度 ≥ (写入突发总字节数 ÷ 数据位宽字节数) × 安全系数安全系数我一般取2~4主要考虑到读时钟域的响应延迟、DMA通道重配延迟、以及复位后第一次传输时读侧可能还没就绪等情况。如果系统里不只有一路数据流还有多个模块共享同一DMA通道深度还要再放大因为DMA可能在处理别的描述符你的FIFO要等更久。6.2 水位监控与反压不要把FIFO用到100%满在5.1里我提到水位控制写错参数的事故反过来说水位控制本身是非常必要的。工程上我不建议让FIFO跑到接近满的状态再触发反压因为满标志的跨时钟同步有延迟很可能你看到还有十几个空位但实际已经过冲了。我的做法是引入一个High Watermark比如深度4096的FIFO把高水位阈值设在3840左右也就是预留约256个word的缓冲。当写侧计数超过阈值时上游逻辑主动暂停生产新的帧或者通过中断通知PS侧降低注入速率。这样可以保证FIFO永远在离满标志还有足够余量的状态下运行跨时钟域的同步延迟再多也有地方消化。这里要注意axis_wr_data_count本身属于写时钟域用它做水位判断时没有任何跨域问题可以直接用组合逻辑比较。这个信号在FIFO Generator配置里可以选不生成省一点LUT但代价是失去精确水位。我建议始终开启性价比非常高。6.3 仿真阶段就要把跨时钟域场景构造出来最后聊一个问题很多人写仿真时习惯只给一个时钟源甚至直接把写侧和读侧接到同一个时钟上跑仿真快速验证功能之后就宣布“FIFO没问题”。但实际上AXI-stream FIFO的跨时钟域正确性只有在两个异步时钟下才能真正体现。我现在的仿真策略是写侧用100MHz时钟读侧用150MHz时钟两个时钟用不同的always块独立生成并在初始化时故意错开相位模拟真实异步关系用计数器产生多帧数据每帧64个word通过校验和或CRC值来判断读侧恢复的数据是否完整在写侧随机插入反压也就是周期性拉低tready验证读侧出来的数据顺序依然正确在读侧随机插入反压验证FIFO不会写溢出、DMA不会丢帧。仿真阶段把这些极端情况测透板级的排错时间会成倍减少。跨时钟域问题最怕的不是难而是“偶尔复现”和“无法稳定复现”仿真能稳定复现定位就等于完成了一半。6.4 一个小习惯让计数标志也参与状态上报在使用AXI-stream FIFO的这些项目里我养成了一个习惯把axis_wr_data_count和axis_rd_data_count接到GPIO或AXI-Lite寄存器上让PS侧能实时读取FIFO的水位。这个习惯在联调阶段救过我很多次。比如DMA吞吐异常时通过读取这两个水位寄存器我可以立刻判断瓶颈在写侧还是读侧如果写侧计数长期接近满说明写入速率大于读出速率或DMA反压剧烈如果写侧计数长期很低但读侧也读不到数据故障大概率在更上游的数据源。把FIFO水位当成一个天然的“流量仪表盘”比上ILA抓波形更快定位系统级问题。回到最开头那个125MHz ADC加100MHz DMA的项目最后真正稳定跑起来靠的并不是什么高深技巧而是把这些最基础的东西都落实到位FIFO选型用标准AXI-Stream接口、写入侧严格遵循握手机制、复位统一来源并同步释放、水位监控留足余量。跨时钟域这个老生常谈的话题做深了之后你会发现难点从来不在于“知道要加FIFO”而在于把FIFO周边所有看似简单的细节都处理干净。希望这篇实战记录能帮你少踩几个我踩过的坑。
返回列表