
直接说结论在Xilinx FPGA上做高速数据传输如果你还在用中断方式一个字节一个字节地搬数据或者用DPRDedicated Peripheral Register方式低效读写那带宽和CPU占用率一定都很难看。AXI DMA配合Scatter-GatherSG模式是目前Zynq、Zynq UltraScale以及MicroBlaze软核系统中吞吐能力最强、也最灵活的数据搬运方案之一。这篇文章我会从DMA在系统里到底解决了什么问题开始讲把SG模式的工作原理拆开揉碎再给你一套可以直接参考的Block Design搭建流程、驱动代码片段和实测性能调优手段最后把我这些年踩过的几个典型坑一并交代清楚。1. 为什么数据搬运会成为系统瓶颈从CPU搬运到DMA的切换逻辑先从一个最简单的场景说起假如你的FPGA里挂了一个ADC采样率是100MSPS位宽12bit按照1.25MB/s的数据量估算不考虑协议开销如果用MicroBlaze或者Zynq的ARM核去直接搬运那就意味着每搬一次数据CPU都要参与。而CPU参与搬运的本质是“读-写-读-写”的循环任何一个通用处理器的总线访问效率都不如专用DMA控制器来得高。早期不少工程师的习惯做法是FPGA侧把数据写进BRAM或者简单的FIFO然后CPU通过AXI GP口把数据读走。这种方式在数据量小的时候没问题一旦出现连续高速数据流CPU就被困在搬运上根本没时间做协议解析、图像处理逻辑或系统调度。我之前做过一个Camera Link接口的图像采集项目就是用这种方式结果ARM核占用率直接飙到70%以上再加一点网络协议栈开销整个系统就快转不动了。Axi DMA的出现本质上是把“在内存和FPGA逻辑之间搬数据”这件事从CPU手里完全接管过来。数据通路从CPU的Load/Store变成了DMA控制器的专用总线突发Burst传输CPU只需要在描述符准备好之后给DMA写一个启动寄存器剩下的搬运过程全部由硬件完成。等DMA搬完通过中断通知CPU结果。这个切换带来的收益非常直接CPU占用率大幅下降实测同一套系统中CPU占用率可以从70%降到10%以下。总线利用率提升AXI DMA支持INCR突发Burst一次可以连续传输多个地址不需要像CPU那样每条指令只访问一个数据字。传输粒度更合理DMA适合传输块Block级的数据适合帧Frame、包Packet或大批量缓冲区。当然DMA也绝不是没有代价。它引入了描述符管理、地址对齐、Cache一致性、中断处理等一系列新的复杂度。很多刚上手的工程师就是栽在这些细节上。2. 寄存器模式还是SG模式两套工作逻辑的对比与SG描述符机制拆解AXI DMA IP核从功能上分为两个主要通道S2MMStream to Memory-Mapped从FPGA逻辑写入内存和MM2SMemory-Mapped to Stream从内存读出给FPGA逻辑。每个通道都支持两种模式寄存器模式Register Mode和Scatter-Gather模式。搞清楚它们的区别是选型前最重要的一步。2.1 寄存器模式简单但受限寄存器模式下每次搬运只对应一段连续内存地址。你需要在DMA寄存器里写入源地址MM2S、目的地址S2MM和要传输的字节数然后启动DMA。一次传输结束之后需要CPU重新配置地址和长度才能发起下一次搬运。寄存器模式的好处是简单适合数据块结构非常规整、内存又恰好连续的场景。但它有两个明显短板一是内存必须连续。DMA不关心虚拟内存它操作的是物理地址。如果你的系统用的是操作系统比如Linux一块大缓冲区在物理内存中往往不是连续的你会发现找一块满足要求的大块连续内存非常困难。二是CPU要频繁参与。每传输一次CPU都要重新写寄存器。在高速、频繁传输的场景下即使不需要搬数据CPU也要花不少时间在配置上。所以我一般只在DDR带宽需求不高、传输次数也不频繁的项目里用寄存器模式比如几条低速UART的收发、偶尔一次性搬一块启动数据等。2.2 SG模式描述符驱动的批量传输Scatter-Gather模式的核心是引入了一张“描述符表”。每个描述符BDBuffer Descriptor记录了四类关键信息下一跳描述符地址、本次传输的内存缓冲地址、传输控制字是否开始/结束、突发类型等和状态字传输了多少字节、是否出错等。这一串描述符通过Next Descriptor指针连成一个链表DMA控制器会沿着链表依次处理每个描述符对应的缓冲区。我画过一张很朴素的对比表直接解释为什么SG模式可以解决连续内存不足的问题对比项寄存器模式SG模式内存要求单次传输必须连续支持多个不连续缓冲区拼接启动方式每次CPU写寄存器只需启动一次DMA自动遍历描述符链表CPU参与度每块数据都要参与只在链表处理和中断应答时参与适用场景低速、单块高速、流式、帧/包出错处理检查寄存器状态每个BD都有独立状态字定位更精确SG模式下的数据流是这样的CPU先在内存中构建好一串描述符每个描述符的Buffer Address指向实际数据缓冲区的物理地址Control字段里填写本次需要搬运多少字节、是否SOP/EOPStart of Packet/End of Packet。然后CPU把链表头地址写给DMA的CurDesc_Ptr寄存器启动DMA。DMA会自己加载第一个描述符完成Buffer传输再自动读取下一个描述符继续传输。直到遇到某个标记为结束的描述符或者链表被设置成循环模式一直跑下去。很多人第一次接触SG模式时最困惑的是“缓冲区和描述符表到底放在哪”。答案很简单都在DDR里并且必须放在DMA能访问到的物理地址空间内。在实际的Linux驱动中描述符表用dma_alloc_coherent分配保证物理地址连续且CPU和DMA访问的Cache视图是一致的数据缓冲区可以用dma_map_single或dma_alloc_coherent取决于你是要长时间复用缓冲区还是每次临时映射。这里有个关键概念值得单独说透描述符链表有“运行模式”和“非运行模式”之分。非运行模式就是处理完最后一个描述符就停下来适合单帧或单包传输如果做的是连续视频流、连续采集通常采用循环模式Cyclic Mode即链表的最后一个BD的Next Descriptor又指回头部DMA不停歇地循环搬运。循环模式下CPU和DMA通过BD的Status字段协作DMA写完一个Buffer就把Status里的Completed位置1CPU轮询或收到中断后处理该Buffer然后清掉Completed位。这个机制一定要在脑海里建立清晰的模型后面排查很多问题都要用它。2.3 为什么SG模式更契合“高效数据传输”所谓“高效”本质是让DMA持续跑满总线而不是频繁等待CPU配置。SG模式下CPU可以一次性把N个描述符写进链表然后去忙别的事。DMA每处理完一个描述符通过硬件自动跳到下一个无需软件介入。实际项目中SG带来的最大红利是它的“批量提交异步完成”机制。举个例子一帧1080p的RGB888图像在60fps下大约一秒有186MB的数据量如果DMA单次传输时CPU都要参与配置那整个系统就退化成“半DMA”状态。用了SG模式之后CPU可以在DMA传第N帧数据的同时预先准备好第N1帧的描述符形成一种类似流水线的效果吞吐自然就上去了。而SG的另一个隐藏优势是错误定位。在寄存器模式下一旦传输失败你只能看到DMA状态寄存器里一个笼统的错误标志很难判断是地址越界还是总线超时。在SG模式下每一个BD都有独立的状态字软件只要遍历描述符表就能定位是哪一个缓冲区出了问题。这一点在长时间跑系统稳定性测试时会救你半条命。3. Vivado Block Design搭建与驱动代码实现一套可以直接跑的工程配置3.1 Block Design里怎么把一个AXI DMA通道完整连起来在Vivado里搭建AXI DMA的工程说简单也简单说复杂也复杂关键是理解你所做的每个连线是在干什么。以Zynq-7000为例一般你会这样做在Block Design里拖入Zynq PS核配置好DDR控制器和HP端口High Performance Port。HP端口是Zynq里专门为高带宽数据访问设计的接口它们不经过CPU的一致性管理直接通向DDR控制器。做AXI DMA时尤其要注意DMA的S2MM和MM2S通道必须挂在HP端口上或者挂在互联矩阵的从端口上。很多人图省事把DMA接到GP口上跑高速结果是CPU占用高、带宽上不去、DMA传输频繁超时最后只能回头改连线。然后拖入AXI DMA IP核。关键配置项如下Enable Scatter Gather Engine必须勾选这是SG模式的核心开关。Width of Buffer Length Register建议设为26bit因为默认的配置只能支持到2^23字节对大数据块来说不够用。Address Width与你的系统总线宽度保持一致Zynq上一般64bit或32bit都行取决于DDR地址空间。Read Burst Size / Write Burst Size这两个值影响DMA对AXI总线的突发能力Zynq上一般可以配到16即一次突发传16个数据节拍beat。Stream Data Width根据FPGA侧数据位宽决定常见的配置为32bit、64bit或128bit。注意S2MM和MM2S的方向相反流位宽可以单独设置。连线方面AXI DMA的S_AXI_LITE接口接PS的GP口用于CPU访问DMA寄存器M_AXI_SG接口接HP口用于DMA访问描述符表M_AXI_MM2S和S_AXI_S2MM接口接HP口用于读写数据缓冲区。为什么SG接口和高带宽数据接口要分开这样可以让“控制面的描述符访问”和“数据面的缓冲区访问”各自占一条总线通道避免描述符的读操作把数据通路堵死。FPGA侧的流接口AXI Stream则要根据你的数据源做适配。如果FPGA逻辑本身产生符合AXI Stream协议的帧那直接连DMA的S_AXIS_S2MM即可。如果数据源是简单的FIFO或自定义状态机你需要在FPGA内做一个AXI Stream协议转换模块生成TVALID、TREADY、TLAST等信号。TLAST尤其重要它标志着帧的结束DMA收到TLAST之后会生成一个EOP标志并通过中断通知CPU当前描述符完成了。每一条连线我都建议你在Connection Automation里仔细看一下生成的选项不要只是点Auto Connect。Auto Connect有时会把DMA的数据接口连到不需要的GP端口上或者给某条AXI总线段附加了不必要的时钟域转换这些看不见的坑会在Vivado时序和布线阶段爆发出来。3.2 驱动侧的核心代码描述符初始化和环形缓冲区管理硬件搭完之后真正决定性能的是驱动代码。以Xilinx官方驱动库xildma或Xilinx的embeddedsw为例你至少需要完成以下几个关键步骤。第一步分配描述符表和缓冲区的物理内存。在裸机环境下你可以直接用Xil_DCacheDisable关掉Cache来省去一致性麻烦但代价是CPU读写性能下降在Linux环境下建议用DMA API来保证一致性而不是简单关Cache关Cache只是把问题往后推了。下面这段裸机代码演示了SG模式下描述符表的初始化和DMA启动过程#include xdma.h #include xil_cache.h #define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID #define BD_COUNT 64 #define BD_BYTES (BD_COUNT * sizeof(XAxiDma_Bd)) #define BUFFER_SIZE 4096 #define BUFFER_BYTES (BD_COUNT * BUFFER_SIZE) static XAxiDma AxiDma; static XAxiDma_Bd *BdRing; static UINTPTR BdRingPhys; static UINTPTR BufferPhys; static U8 *BufferPtr; int DmaInit(void) { XAxiDma_Config *CfgPtr; int Status; CfgPtr XAxiDma_LookupConfig(DMA_DEV_ID); Status XAxiDma_CfgInitialize(AxiDma, CfgPtr); if (Status ! XST_SUCCESS) return XST_FAILURE; // 要求DMA支持SG模式 if (!XAxiDma_HasSg(AxiDma)) return XST_FAILURE; // 分配描述符表和缓冲区要求物理内存连续且是Cacheline对齐的 Status XAxiDma_BdRingCreate(AxiDma, (UINTPTR)BdRing, (UINTPTR)BdRingPhys, BD_COUNT, BD_BYTES); if (Status ! XST_SUCCESS) return XST_FAILURE; // 这里使用的是Xilinx内存分配接口实际工程可能需要自己实现连续内存分配 BufferPtr (U8 *)MEM_ALLOC(BUFFER_BYTES); BufferPhys (UINTPTR)BufferPtr; return XST_SUCCESS; }第二步设置传输描述符。在S2MM循环传输的经典场景里你通常需要先“挂载”多个BD到DMA的环形缓冲中让DMA知道数据应该写到哪。int PrepareBufs() { XAxiDma_Bd *BdPtr; UINTPTR CurPhys; int i; CurPhys BdRingPhys; for (i 0; i BD_COUNT; i) { BdPtr (XAxiDma_Bd *)CurPhys; XAxiDma_BdClear(BdPtr); // 设置缓冲区地址 XAxiDma_BdSetBufAddr(BdPtr, BufferPhys i * BUFFER_SIZE); // 设置缓冲区长度 XAxiDma_BdSetLength(BdPtr, BUFFER_SIZE, 0); // 标记为主机可写HostReadyDMA处理完会写回状态 XAxiDma_BdSetHw(BdPtr, 1); // 设置硬件可用的下一描述符 XAxiDma_BdSetNext(BdPtr, CurPhys sizeof(XAxiDma_Bd)); CurPhys sizeof(XAxiDma_Bd); } // 循环指向表头 XAxiDma_BdSetNext(BdPtr, BdRingPhys); return XST_SUCCESS; }第三步启动DMA的S2MM通道。启动之后DMA会从环形链表的头开始依次接收FPGA侧送来的AXI Stream数据写入对应的Buffer。当FPGA侧TLAST信号到来当前BD完成状态寄存器被DMA更新触发中断。// 接收当前完成的BD地址并处理 XAxiDma_Bd *ProcessRx(XAxiDma_Bd *BdPtr) { UINTPTR PhysAddr; U32 Length, Status; // 获取BD状态 Status XAxiDma_BdGetSts(BdPtr); if ((Status XAXIDMA_BD_STS_ALL_MASK) 0) { // BD还没有被DMA完成跳过 return BdPtr; } PhysAddr XAxiDma_BdGetBufAddr(BdPtr); Length XAxiDma_BdGetLength(BdPtr); // Flush Cache保证CPU能看到DMA写入的数据 Xil_DCacheFlushRange(PhysAddr, Length); // 在这里对BufferPhys中的数据进行处理 ProcessPacket((U8 *)PhysAddr, Length); // 清除状态重新交还给DMA硬件 XAxiDma_BdClear(BdPtr); XAxiDma_BdSetLength(BdPtr, BUFFER_SIZE, 0); XAxiDma_BdSetHw(BdPtr, 1); return BdPtr; }写驱动代码时有几个细节特别容易出错。第一BD表的内存必须是DMA可访问的物理内存且建议Cacheline对齐。如果BD表本身被Cache缓存了DMA写状态、CPU读状态就会发生Cache一致性问题直观表现就是中断触发后读到的状态值不对或者永远看不到Completed位被置位。第二S2MM模式下缓冲区在DMA写入数据之前CPU不能碰。同理在MM2S模式下CPU在DMA读取完数据之前不能修改缓冲区内容。这个“所有权交接”逻辑一定要通过BD状态位严格控制不要图省事在中断之外直接访问缓冲区。第三中断例程里做的事情要尽量少。通常只做三件事找到哪个BD完成了、把数据地址提交给上一层的协议处理队列、把BD重新挂回环形缓冲然后清中断。如果你在中断里直接做图像处理中断服务时间一长DMA就可能出现下溢或溢出流式数据就断了。3.3 数据处理去抖循环与处理流程建议针对流式传输我通常会在驱动层设计一个“生产者-消费者”模型DMA中断是生产者应用线程或主循环是消费者。中断服务函数里只把完成的BD挂到待处理链表并置一个标志位主循环里检测到标志位后再批量处理所有待处理缓冲区。这样做的好处是减少了中断上下文切换的开销适合高吞吐应用。伪代码大致是这样的// 中断上半部 void IsrHandler(void) { XAxiDma_Bd *Done; // 从DMA取得当前完成BD指针 Done XAxiDma_BdRingGetDone(AxiDma); // 把Done追加到待处理队列 AppendToPendingQueue(Done); gPendingFlag 1; } // 主循环 while (1) { if (gPendingFlag) { ProcessPendingQueue(); gPendingFlag 0; } }在实际工程里半中断半轮询的方式可以大幅降低单包延迟也能防止中断风暴嵌套导致系统hang住。具体是否需要完全用轮询替代中断可以根据实测的中断频率来决定通常中断频率超过10kHz就建议考虑批处理了。4. 性能实测与调优方向描述符深度、突发长度与数据位宽的权衡4.1 实测数据对比不同配置对吞吐的影响下面这组数据是我在一块Zynq-7045开发板上测出来的FPGA侧用AXI Stream总线持续灌入伪随机数据DMA使用SG模式搬运到DDR再用PS侧定时统计搬运总量。测试环境为Vivado 2018.3驱动为裸机。配置组合数据位宽(bit)Burst Size调优BD数量实测吞吐(MB/s)寄存器模式32不生效1约210SG模式BD16321616约680SG模式BD64321664约725SG模式BD64641664约950SG模式BD12812816128约980从中可以看出数据位宽从32bit提升到64bit或128bit带来的收益最显著因为同样的时钟频率下数据面带宽翻了倍。BD数量从16提升到64能带来一些收益但继续增加收益就很小了说明DMA在没有存储瓶颈的时候描述符的加载不是主要瓶颈。一个很值得关注的点是寄存器模式跑到210MB/s左右就上不去了。原因很简单每次传输CPU需要重新配置寄存器中断和配置开销把总线利用率拖下来了。SG模式下CPU几乎不参与真正的限制在于AXI总线本身的突发效率和DDR控制器的刷新开销。4.2 调优的四个方向地址对齐、突发长度、描述符深度、中断合并地址对齐是最容易被忽略的性能杀手。AXI DMA对Buffer Address有对齐要求通常要求4字节对齐如果PS侧分配缓冲区时没有对齐DMA会报地址错误或者退化成非突发传输性能暴跌。实际的Linux驱动里dma_alloc_coherent默认返回Cacheline对齐的地址但裸机工程里如果malloc分配的内存地址只对齐到8字节可能就存在隐患。建议每次分配后打印物理地址检查或者直接使用XDMA库提供的专用分配函数。突发长度方面Zynq的AXI DMA一般建议设为16。不过突发长度越大对DDR的连续访问要求越高如果实际的Buffer地址或BD表地址由于其他原因被打散到不同Page过大的突发反而会造成总线效率下降。灵活场景下可以先按16配再用实测带宽决定是否调整。描述符深度建议至少能满足DMA在“最慢完成一轮中断处理”期间不被饿死。换句话说如果FIFO深度有限而CPU中断处理需要10us那么描述符数量要保证在这10us里DMA有足够的BD可以继续跑。一个粗略的工程公式是BD数量 中断处理延迟时间内DMA能传输的数据量 / 单个BD缓冲区大小举例DMA带宽1GB/s中断延迟10us那么10us内大约传10KB数据如果你的每个BD缓冲区是4KB那至少需要3个BD打底。但实际考虑到链路中可能有多个传输都在排队工程上建议至少保留8个以上。中断合并方面如果DMA支持Interrupt Coalescing有些AXI DMA版本或DMA驱动支持或者你使用的是带计数功能的SG中断可以把“每完成一个BD就中断一次”改成“完成若干BD或超时后再中断”。实测这样能显著降低CPU中断次数代价是单包延迟升高。对于追求吞吐、不追求超低延迟的场景非常合适。4.3 关于Cache一致性最容易让吞吐“虚高”或“归零”的部分说到性能调优还有一个绕不开的老大难问题Cache一致性。在裸机工程中如果你把DMA缓冲区访问权限交给了硬件却忘了在CPU访问之前做Cache维护就会出现CPU读到的是Cache里的旧数据或者Cache里的数据还没被刷到DDR就被DMA覆盖了。在Linux工程中这一块更是事故高发区。最简单的做法是使用dma_alloc_coherent分配缓冲区它本身就保证了CPU和DMA视角的一致性不需要手动刷Cache。但缺点是这种缓冲区CPU访问效率不是最高因为走无Cache映射。如果你要用流式映射就要手动维护ownershipCPU写数据给DMA时要调用dma_map_single(..., DMA_TO_DEVICE)把Cache里的数据刷到内存。DMA写数据给CPU时要调用dma_map_single(..., DMA_FROM_DEVICE)使Cache失效让CPU重新从内存读取。我在一个PCIe DMA驱动项目里见过一个非常隐蔽的问题驱动在处理完数据后忘了调dma_unmap_xxx导致下一次DMA映射时报错或读到旧数据最终呈现出来的现象就是“每隔几百帧丢一帧”。这种“人群中丢包”的问题是Cache一致性问题里最折磨人的因为不是每次都会出现出现时又非常随机。后来我把所有缓冲区映射和反映射函数都梳理了一遍确保每次DMA传输完成后都正确调用unmap问题才彻底消失。5. 实战中高频踩坑记录与排查链路5.1 描述符地址未对齐导致的“假死”这个坑我印象非常深刻。当时是在Zynq上做网口数据转发DMA初始化后第一次传输正常第二次传输卡死CPU再也收不到完成中断。反复查驱动逻辑都没发现问题最后用在线逻辑分析仪抓DMA的S_AXI_SG接口发现DMA在读取描述符地址时出现了地址错位。原因其实很朴素描述符表在内存中的起始地址不是4字节对齐的而DMA硬件在读取描述符时会按照AXI地址对齐规则把低两位强制清零。这样它读到的“第一个描述符”和你CPU构建的描述符根本不在同一个位置链表就乱了。解决办法非常简单——分配描述符表时强制对齐。裸机环境中可以自己写一个对齐分配函数Linux中直接用dma_alloc_coherent。从那以后我在所有工程的初始化代码里都会加一个地址对齐断言宁可启动时多一分检查也不要在运行几小时后再爆出奇怪问题。5.2 中断永远不会来的S2MM阻塞问题还有一个常见问题是S2MM传输启动后DMA一直不产生中断也不报告错误。一般原因是FPGA侧没有产生TLAST信号。AXI Stream协议里TLAST表示一个包的结束。如果数据源源不断地发送数据但永远不拉高TLASTDMA会一直认为当前这个包还没有结束它就不会把Buffer的最后一个字节写回也不会更新BD状态自然不会有完成中断。当时那个项目的数据源是我自己写的一个状态机从FIFO里读数据后直接发到AXI Stream。最初我只在FIFO空时拉低TVALID完全没管TLAST。结果DMA初始化后一个包都收不到。排查时用ILA抓AXI Stream总线发现TLAST一直为低才定位到问题。修复后我在系统中对所有能产生流式数据的模块都加了约束要么在包结束位置产生TLAST要么在DMA侧配置“忽略TLAST”选项如果IP核支持。另外对于不产生TLAST的纯流式数据如ADC连续采样需要确认IP核是否可以在无EOP的情况下定时封包。如果硬件不支持就只能用定时器中断的方式定期“强行”处理已收到的数据但这种方式会对描述符管理和时序分析带来额外开销。5.3 环形BD指针绕回时的“幽灵描述符”SG模式的循环链表在运行时间长了以后偶尔会出现一种现象CPU明明已经把BD重新交给硬件了但DMA在遍历链表时却跳过了它或者重复处理了上一个BD导致数据错位、丢包。这类问题通常和BD状态位的清置顺序有关。SG模式下的BD状态字段是被CPU和DMA共享的。CPU侧在把BD“交还”给DMA时必须先把Ready位和Ownership位置成正确的值然后再修改Next指针。如果顺序反了DMA可能在你更新Next指针之前就开始处理这个BD读到半新半旧的状态行为就不可预测了。我处理这类问题时的标准做法是所有BD状态的更新都先清零再写长度再写地址最后写所有权和下一跳指针并确保这段代码序列不会被编译器和CPU乱序执行打乱。裸机环境中使用__sync_synchronize或Xil_DCacheFlushRangeLinux中使用dma_wmb/wmb屏障。5.4 时钟域不一致造成的偶发数据错乱AXI DMA的S_AXIS接口和S_AXI_LITE接口通常可以在不同时钟域。如果你的FPGA逻辑运行在75MHz而DMA配置总线是100MHz没做跨时钟域处理就可能出现偶发的字节丢失或状态错乱。工程上的做法是在Block Design里给AXI Stream接口添加异步FIFOaxis_data_fifo或者由上游IP自己处理好跨时钟域握手。我当时调试一块高速ADC板卡时就是ADC采样逻辑跑在250MHzDMA的Stream接口跑在125MHz中间加了一级axis_data_fifo带宽损失很小稳定性却大幅提升。5.5 用ILA定位SG引擎卡死位置的方法最后谈谈排查手段。遇到SG模式下的异常行为不要一上来就怀疑驱动也不要先怀疑IP配置。第一步是抓DMA内部的寄存器状态查看S2MM和MM2S通道的DMACR、DMASR、CURDESC、TAILDESC等值。如果CURDESC一直停在某个地址不动说明DMA正在等待该BD的某些条件如果TAILDESC和CURDESC差距很大说明链表里还有大量BD没被处理。第二步是用Vivado的ILA抓S_AXI_SG接口上的请求。正常情况下DMA会持续对SG接口发起读传输。如果ILA显示一段时间内完全没有SG接口请求而DMA也没有中断那大概率是DMA处于错误状态或等待TLAST。第三步交叉验证在驱动中打印当前BD的状态字段在硬件侧观察Stream接口的数据流向。两边同时比对一般都能快速缩小问题范围。6. 写在最后几个经验性的建议做AXI DMA的SG模式硬件配置和驱动开发只是门槛真正决定项目成败的是你对数据所有权转移的理解深度。每一条总线上流动的数据都要弄明白它此刻归谁管被谁写被谁读什么时候交棒。把这张图在脑子里画清楚了大多数疑难杂症都能一眼看穿。有一点值得反复提醒在用Linux时给DMA分配缓冲区不要随便用kmalloc大块内存凑数一定要走DMA API或者预留CMA区域。很多网上教程为了省事直接用物理连续内存一旦在设备树中预留的CMA区域规划不合理就会触发其他模块内存分配失败。我后来都是专门划一块DMA内存池只给DMA使用避免互抢。最后再分享一个小技巧在驱动的调试阶段别急着写复杂的数据处理逻辑先让DMA从固定模式数据源比如FPGA侧产生0x1、0x2、0x3递增序列搬运然后对比内存中的数值。如果序列完全正确再接入真实数据。这一步能帮你把“DMA功能问题”和“业务数据处理问题”彻底分开调试效率会高很多。