
简介一份面向Xilinx AX7015Kintex-7FPGA、基于Vivado 2017的AXI DMA散射聚集SG模式与FIFO数据流传输设计资源适合FPGA工程师、嵌入式开发者及高校学生用于学习高速数据传输原理或作为工程项目参考。压缩包共1174个文件大小约48.73MB包含HDL源码145个h、122个v、55个vhdl、C语言驱动96个c、约束文件36个xdc、工程配置脚本31个tcl、24个xci及仿真日志等可覆盖从设计综合到硬件验证的完整流程。该资源已有1272人学习下载。包内提供AXI DMA SG模式的数据搬运实现、FIFO缓存设计以及AX7015硬件约束配置其中SG模式可高效处理多个不连续数据缓冲区FIFO则用于缓存数据流、平衡不同模块间的速率差异。结合Vivado工程与SDK相关文件有助于开发者掌握非连续多缓冲区的DMA传输机制和软硬件协同设计方法同时为功能验证、时序收敛与性能调优提供可直接参考的工程范例。1. DMA SG FIFO这个压缩包也许正是你调试Zynq高速搬运时缺的那块拼图用Vivado调AXI_DMA时SG模式加FIFO是再常见不过的组合但也是一堆玄学问题的源头。我上手某AX系列Zynq开发板做高速采集时DMA总是跑一次就挂数据错位、中断丢失轮着来后来把DMA_SG_FIFO这个工程翻出来对着查才理清描述符、缓存一致性和FIFO深度之间的关系。如果你也在为AXI_DMA不上手、SG描述符链跑不起来而头疼这篇笔记就是从这份资源出发把SG DMA怎么配、FIFO怎么选、踩过的坑怎么排查一次说清。它适合用Vivado做AXI_DMA数据搬运的FPGA工程师也适合刚接触Scatter-Gather模式、想直接抄作业的开发人员。2. 解包与工程结构先搞清SG描述符和FIFO在链路上各干什么拿到ZIP文件后别急着打开Vivado跑综合。这个压缩包的核心价值不在代码量而在它把SG DMA和FIFO之间的配合方式固定成了可复现的工程。我习惯先解包把文件结构和意图摸清楚再决定是整体使用还是挑模块复用。2.1 压缩包里有什么文件清单与职责划分典型DMA_SG_FIFO工程包会按Vivado工程、RTL源码、嵌入式测试代码和文档四类组织。下面是我解包后整理的文件清单你可以按这个顺序去核对手里那份文件名可能略有出入但职责都围绕AXI DMA的SG传输和FIFO缓冲展开。路径内容作用vivado_prj/.xpr Vivado工程文件、Block Design tcl脚本、时序约束xdc提供可直接打开验证的完整Vivado工程省去从零连线的过程rtl/fifo_wrapper.v自定义FIFO接口包装含跨时钟域逻辑将AXI4-Stream从DMA侧适配到下游用户逻辑处理不同位宽转换rtl/axis_control.v发送端数据产生或接收端数据消费逻辑在测试模式下生成递增计数或固定pattern用于验证DMA搬移完整性sdk/dma_sg_test.cSDK下的SG模式驱动与测试代码初始化AXI DMA、构建描述符表、启动传输、处理中断doc/parameter.xlsxIP配置参数表和FIFO深度估算表记录DMA与FIFO的关键参数以及不同带宽下的FIFO容量计算方法建议先打开doc里面的参数表再对照vivado_prj里的Block Design不要先看代码。原因很简单SG DMA的绝大多数问题出在配置和地址对齐上而这两个信息都在参数表和约束文件里。文件里那个fifo_wrapper.v往往是最容易被替换忽略的模块但后面你会发现它才是决定DMA能不能持续跑的关键。2.2 SG模式与普通DMA的区别为什么这里必须有FIFO普通DMA搬运的是连续物理地址或固定地址硬件自动递增地址CPU只需要给定起始地址和长度。Scatter-Gather模式则维护一张描述符表每个描述符包含目标地址、字节长度、下一个描述符指针和状态位硬件自动读取描述符并依次搬运。这样做的好处很明显内存可以是非连续的碎片块多个描述符可以形成环形链一轮传输结束硬件直接取下一条不需要CPU频繁介入。那为什么要加FIFO直接DMA到用户逻辑不就行了吗问题在于AXI4-Stream接口是没有地址的它的数据流动靠tvalid和tready握手而DMA的S2MM接口希望数据持续稳定地被消费。如果下游逻辑的写入节奏不稳定或者存在时钟域不一致数据就会断流。FIFO在这里起两个作用一是作为缓冲吸收上下游的瞬时速率差二是作为跨时钟域的转换点比如DMA侧跑150MHz用户逻辑侧跑100MHzFIFO就能把两侧隔开。常见的做法是使用Xilinx AXI4-Stream Data FIFO IP也可以自己用Block RAM实现一个同步或异步FIFO关键是要正确处理复位和almost_full/almost_empty信号。2.3 AXI DMA IP配置要点数据宽度、突发长度、描述符缓存在Vivado里配置AXI DMA时我在AX系列开发板上用的参数如下这套参数适合大部分DMA图像采集、高速ADC或PCIE搬运场景。配置项推荐值说明Enable Scatter Gather Engine勾选使用SG模式Stream Data Width64位对应AXI4-Stream数据总线宽度按实际DMA位宽调整Max Burst Size256突发长度越大约满带宽但要求描述符地址对齐到突发边界Synchronize Reset Sources勾选让DMA和FIFO共享统一复位逻辑Number of Descriptors128环形描述符链长度越大越能容忍CPU响应延迟Address Width32位或对应PS地址位宽常见Zynq-7000用32位MPSOC用40位以上参数表里的每一项都不是随手填的。Max Burst Size选项同时决定了DMA在AXI总线上每次突发读写的字节数如果它和你的FIFO深度不匹配下游数据会瞬间拥堵almost_full信号拉高后DMA被迫频繁断开突发带宽就会打折。Number of Descriptors也不是越大越好它对应一块连续内存存放描述符表占用空间等于描述符数量乘以每条描述符的固定大小一般是64字节。128条描述符等于8KB内存在Zynq里微不足道但能显著减少中断和重提交的交互频率。3. Vivado里搭一条SG FIFO数据通路连接、约束与C代码骨架理论看懂了接下来就是动手。我建议先按照压缩包里自带的Block Design重建一次链路再逐步替换其中某个部分来理解每个模块的作用。不要直接拿现成工程去改否则遇到问题时你根本不知道是连线问题还是代码问题。3.1 在Vivado里创建IP核并将DMA与FIFO互联压缩包里的vivado_prj一般会有一个tcl脚本可以自动重建Block Design。下面是常见AXI DMA配置脚本的骨架打开Vivado后在Tcl Console里执行即可生成核心IP。实际参数以你的IP版本为准我的环境是Vivado较新版本AXI DMA版本对应7.1。# 创建Block Design并添加AXI DMA create_bd_cell -type ip -vlnv xilinx.com:ip:axi_dma:7.1 axi_dma_0 set_property -dict [list \ CONFIG.c_include_sg {1} \ CONFIG.c_sg_length_width {26} \ CONFIG.c_max_burst_length {256} \ CONFIG.c_include_s2mm {1} \ CONFIG.c_include_mm2s {1} \ ] [get_bd_cells axi_dma_0] # 添加AXI4-Stream Data FIFO create_bd_cell -type ip -vlnv xilinx.com:ip:axis_data_fifo:2.0 axis_data_fifo_0 set_property -dict [list \ CONFIG.FIFO_DEPTH {2048} \ CONFIG.HAS_TKEEP {1} \ CONFIG.HAS_TLAST {1} \ ] [get_bd_cells axis_data_fifo_0]上面的脚本里c_sg_length_width定义描述符里字节长度的位宽26位代表最大支持64MB长的单次描述符搬运。c_max_burst_length设成256AXI总线才能以满突发长度读写如果用默认的16带宽会掉得很明显。FIFO_DEPTH我填2048这是按DMA位宽64bit、下游消费速率慢一半时能提供至少两轮突发缓冲估算的具体深度根据实际速率差改成1024或4096都行。连线的时候有一个关键点AXI DMA的S2MM接口连接到FIFO的AXIS输入MM2S接口连接到FIFO的AXIS输出。这种背靠背结构下FIFO同时承担两个方向的缓冲如果你的系统只用一个方向就把不需要的DMA通道禁掉减少中断和资源占用。时钟方面所有AXI接口和AXI4-Lite配置接口必须是同一个时钟域否则XADC或调试模块会报时钟连接缺失导致综合不通过。3.2 自定义FIFO的选型与AXI Stream握手细节很多压缩包里的fifo_wrapper.v不是直接例化IP而是用BRAM手写了异步FIFO再在外面包一层AXI Stream握手逻辑。原因是AXI4-Stream的tready信号在跨时钟域场景下容易受复位释放顺序影响IP核自带的FIFO有时候在仿真里表现正常上板却异常。我一般会保留这个自写包装因为它让你能精确控制tlast信号的生成位置。// fifo_wrapper.v将内部FIFO接口适配为AXI4-Stream module fifo_wrapper #( parameter DATA_WIDTH 64, parameter DEPTH 2048 ) ( input wire aclk, input wire aresetn, // 写侧上游 input wire [DATA_WIDTH-1:0] s_axis_tdata, input wire s_axis_tvalid, input wire s_axis_tlast, output wire s_axis_tready, // 读侧下游DMA output wire [DATA_WIDTH-1:0] m_axis_tdata, output wire m_axis_tvalid, input wire m_axis_tready ); // 内部同步FIFO signals wire wr_en, rd_en; wire almost_full; // 当内部fifo未满且上游valid时拉高tready assign s_axis_tready !almost_full; assign wr_en s_axis_tvalid s_axis_tready; // 下游DMA只有在tvalid且tready时消耗数据 assign rd_en m_axis_tvalid m_axis_tready; sfifo_2048x64 fifo_inst ( .clk(aclk), .reset_n(aresetn), .din(s_axis_tdata), .wr_en(wr_en), .rd_en(rd_en), .dout(m_axis_tdata), .full(full), .empty(empty), .almost_full(almost_full) ); assign m_axis_tvalid !empty; endmodule这个模块的核心逻辑在于用almost_full提前阻断上游而不是等到full才阻断。这能给DMA端留出至少一整拍的反应时间避免推挤。如果你用Xilinx官方AXI Data FIFO那么almost_full的阈值可以在IP配置里调成半满即programmable_full_type效果等效。这里有一个容易踩的坑复位的释放必须与其他AXI接口同步如果FIFO复位释放得比DMA晚下游DMA会看到FIFO的tvalid在一段时间内是亚稳态导致第一个描述符数据错位。3.3 SDK里用描述符链驱动搬运C代码骨架硬件搭好后软件侧的核心工作就是构建描述符表并启动DMA。压缩包里的dma_sg_test.c一般会使用Xilinx BSP驱动库XAxiDma下面是一个简化版骨架演示初始化、描述符提交和中断处理的关键步骤。#include xil_cache.h #include xscugic.h #include xaxidma.h #define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID #define RX_BUFFER_BASE 0x10000000 #define RX_BUFFER_SIZE 0x4000 #define DESC_NUM 128 XAxiDma dma_inst; XAxiDma_BdRing ring; UINTPTR rx_buf[DESC_NUM]; void dma_sg_init(void) { XAxiDma_Config *cfg XAxiDma_LookupConfig(DMA_DEV_ID); XAxiDma_CfgInitialize(dma_inst, cfg, cfg-BaseAddress); XAxiDma_BdRingInitialize(ring, (UINTPTR)dma_inst, 0x2000000, DESC_NUM, XAXIDMA_BD_MINIMUM_ALIGNMENT); // 分配实际缓冲区并确保各缓冲区按DMA对齐要求分配 for (int i 0; i DESC_NUM; i) { rx_buf[i] RX_BUFFER_BASE i * RX_BUFFER_SIZE; } } int dma_sg_start(void) { XAxiDma_Bd bd; for (int i 0; i DESC_NUM; i) { XAxiDma_BdRingAlloc(ring, 1, bd); XAxiDma_BdSetBufAddr(bd, rx_buf[i]); XAxiDma_BdSetLength(bd, RX_BUFFER_SIZE, 0, 0); XAxiDma_BdSetCtrl(bd, XAXIDMA_BD_CTRL_ALL_MASK); XAxiDma_BdRingTxHeld(ring, bd); // 先hold最后一起提交 } // 刷新缓存确保描述符和缓冲区内容对DMA可见 Xil_DCacheFlushRange((UINTPTR)rx_buf, DESC_NUM * RX_BUFFER_SIZE); XAxiDma_BdRingStart(ring); return XST_SUCCESS; }这段代码里有三个必须注意的地方。第一XAxiDma_BdRingInitialize的第二个参数是描述符表所在的内存地址必须做到cache line对齐并映射为非cache或通过Xil_DCacheFlushRange刷新否则DMA读到的描述符可能是旧数据。第二XAxiDma_BdSetLength的最后一个参数是tx_sg标志在S2MM接收方向要填0填成1会导致描述符总被当成发送方向处理接收永远不启动。第三大段缓冲区的cache刷新按严格定义做是安全的性能上也可以接受真正要注意的是描述符表缓冲它必须单独对齐到XAxiDma驱动规定的对齐字节数通常最小是64字节错误对齐会直接触发总线错误。4. 避坑/常见问题SG FIFO链路跑不通的五个高频症状与排查顺序这部分是血泪经验的集合。以下五种症状我都在不同项目上遇到过每条按现象到原因再到解决方式写你可以直接对照排查。4.1 现象DMA卡死在第一个描述符状态寄存器不动DMA写完所有配置status寄存器停在1s2mm_introut不触发看起来就像DMA没有工作。原因绝大多数是描述符表所在内存的cache一致性出了问题。Zynq的PL端DMA通过AXI_GP口访问DDR而CPU默认配置下DDR是write-allocate cache描述符被CPU写入后先留在L2 cache里DMA直接读内存读到的是旧数据。解决方式是在提交描述符后用Xil_DCacheFlushRange刷描述符表并且确认描述符地址确实按64字节对齐。具体做法是检查BdRing初始化时是否把基地址传成了普通变量地址如果传的是一个局部数组首地址那几乎必挂。4.2 现象数据错位看起来像丢了半个FIFO接收到的数据整体向后偏移比如第N个字节跑到第N8的位置看起来像FIFO丢了一个完整的64bit字。原因一般有两个一是FIFO复位晚于DMA导致DMA上电后看到FIFO有残留而且读指针没有归零二是AXI Stream数据宽度不匹配比如DMA配置64位而FIFO逻辑内部按32位写入每次写入只更新半个字。解决方式是在复位流程里保证FIFO的复位释放晚于DMA并且在Vivado里将FIFO的位宽改成与DMA完全一致的64bit不要依赖自动转换。检查手段是在仿真里看fifo_wrapper的rd_en和dout的同时性如果dout改变发生在rd_en上升沿之后大概率是读时序没对齐。4.3 现象中断偶尔丢一帧系统跑几万帧不出错然后突然某一帧没有中断DMA状态寄存器显示已完成但CPU没有响应。原因通常是中断服务函数里花时间太长比如在s2mm中断处理中又调用了打印函数或重新分配大块内存导致描述符释放不及时。环形描述符链中硬件完成一个描述符后会写回状态但如果你没有及时调用XAxiDma_BdRingHeld和Free下一个描述符可能没有被硬件标记可写。解决方式是把中断服务函数减到最简只做状态读取、通知任务和重新提交描述符所有大数据搬运和日志处理都放到主循环或RTOS任务中。实践上我还会在中断里加一个wmb()写屏障确保状态读取不会被编译器乱序优化。4.4 现象实测带宽只有理论一半突发长度丢失用性能计数器测量S2MM吞吐只有理论可用带宽的50%到60%而且时延忽高忽低。原因常见有两个内存基地址没有按4096对齐导致AXI总线在每次跨页时无法保持突发被迫拆成窄突发或者DMA的Max Burst Size配置默认是16而不是你期望的256。解决方式是将接收缓冲区首地址对齐到4096字节并在Block Design中把AXI DMA的Max Burst Size改为256。另一个容易被忽略的点是S2MM和MM2S同时打开时共用了同一个AXI GP口读写会互相抢占单方向测下来当然少一半。如果只是接收场景关闭MM2S通道。4.5 现象FIFO状态位在复位后无规律变化系统上电后fifo_wrapper里的empty和almost_full在不工作的状态下乱跳直接导致下游DMA收到假valid信号。原因几乎都是异步复位没有同步释放。同步FIFO如果使用独立异步复位复位释放时如果离时钟上升沿太近内部指针寄存器会进入亚稳态。解决方式是在fifo_wrapper顶层加一个复位同步器或者使用Vivado的proc_sys_resetIP统一生成复位信号保证FIFO和DMA在同一个时钟边沿释放复位。我在自己的封装里会额外在fifo_wrapper内部打拍两级复位再接入所有内部信号这个习惯救过我很多次。5. 验证进阶用ILA和环形描述符把SG DMA的吞吐压到极限跑通只是第一步要确认SG FIFO方案稳定可靠还需要做系统性验证并在此基础上探索更高性能的配置。这里分享两个我常用的进阶手段。先谈ILA在线调试。在Vivado里给fifo_wrapper的AXI Stream接口添加ILA IP观察S2MM侧握手信号。若发现m_axis_tvalid拉高时m_axis_tready经常为低说明FIFO周期性满且DMA端带宽不足若m_axis_tvalid频繁抖动说明上游数据有空洞。通过这些波形可以一眼看出瓶颈在FIFO深度还是DMA突发设置。我一般会抓tlast信号tlast在每帧结束拉高一个周期如果tlast出现在错误位置说明下游逻辑打包的帧长度配置错了这比逐字节核对数据更高效。然后是环形描述符链的持续搬运测试。把描述符数量提高到512并在主循环里用XAxiDma_BdRingToBd获取已完成描述符校验缓冲区起始字节是否正确。如果每个缓冲区起始都写有固定的帧号只要帧号不连续就说明有丢帧。想要压榨极限带宽可以将多个缓冲区连接成连续物理内存并把描述符的地址离散化从而让DMA每次都以最大突发长度读写。实际操作时我还会在DMA中断里记录时间戳统计相邻两次中断的间隔抖动这比单纯测平均吞吐更能反映链路稳定性。缓存一致性是进阶中绕不开的课题。如果使用普通DDR时描述符表一直在被DMA更新CPU读取时要配合Xil_DCacheInvalidateRange写描述符要Xil_DCacheFlushRange。可以尝试把描述符放入CSU或OCM这样的非cache内存区域避开每一次刷新D-Cache的开销。我之前在某图像处理Demo里这么改建过一次中断延迟从几十微秒降到个位数微秒吞吐也有明显提升。从那以后我每次做Zynq类似项目都会强制走一遍这套流程先核对描述符表对齐和cache刷新再检查复位释放顺序最后用ILA确认握手信号这样能在半小时内定位大部分问题。希望帮到你。本文还有配套的精品资源点击获取