
做过多年的FPGA嵌入式开发对Zynq/MPSoC平台上各种数据搬运的活儿并不陌生。早期做千兆以太网采集项目时我一直用的是裸机中断CPU搬运的方案CPU中断频率高得吓人好不容易把数据从MAC搬进DDR再要从DDR搬到CPU做业务处理时又发现内存带宽被来回拷贝蚕食殆尽。后来换到AXI DMA来做千兆以太网数据直传整套通路彻底变了MAC收到的帧直接写进DDR指定缓冲区CPU只在描述符和中断层面做轻量级介入读数据时也是从DDR直接拉走整个链路算是真正“直传”起来了。这篇博文就把我在该项目中的设计思路、AXI DMA工作机制、硬件搭建、驱动实现以及踩过的坑完整记录下来希望对正在做类似数据通路的朋友有实际参考价值。1. 需求分析与整体思路拆解1.1 数据直传要解决的核心问题首先要说明白千兆以太网在理论线速下每秒能吞吐约120MB数据按1518字节帧长估算纯靠CPU中断处理每个帧光是中断开销就足以让普通ARM核接近满载。更糟糕的是如果走“MAC寄存器轮询-CPU搬数据-CPU再写DDR”的传统路径每次帧到达都要占用CPU大量时间业务逻辑根本来不及跑。数据直传要解决的核心问题就是让MAC收到数据后能自动写到DDR里预定的内存区域整个过程不需要CPU搬运。与此同时CPU要发送数据时只需把内存里的数据准备好告诉DMA“发吧”硬件会自动从DDR把数据取出来交给MAC。这样一个设计下来CPU从“搬运工”变成“调度员”只处理描述符、中断、业务逻辑性能余量就出来了。这里的“直传”主要有三层含义第一层MAC到DDR的RX方向数据不经CPU直接写入内存。第二层DDR到MAC的TX方向数据从内存不经CPU直接交给MAC。第三层如果业务是“网口收进来立刻从另一个网口发出去”DMA可以在驱动里直接把收包缓冲区的物理地址交给发送描述符实现零拷贝转发。1.2 为什么选AXI DMA而不选其他方案我评估过几种替代方案下面这张对照表格能直观看出差异方案CPU开销实时性实现复杂度适用场景纯中断CPU搬数极高受CPU负载影响大最低低速、帧率低MAC内建FIFO轮询中中等低少量数据PL端自研FIFODMA低高高需定制协议AXI DMA低高中通用的高速数据通路AXI DMA是Xilinx官方提供的IP核对AXI4和AXI4-Stream协议的支持非常完善驱动模型有官方驱动和大量社区参考最关键的是它支持Scatter Gather描述符链解决了我担心的“大块连续内存分配”问题。嵌入式Linux里物理内存碎片化严重想一次性分配几百MB连续内存几乎不可能SG模式让DMA可以操作分散的物理页然后通过描述符把它们串成逻辑上连续的数据流。另外还有一个很实际的原因团队里后续要在这条数据通路上叠加TCP/IP硬件卸载、UDP过滤等逻辑AXI DMA作为标准化接口前后端都能平滑替换协议栈模块不会牵一发动全身。1.3 系统整体框架与数据通路整个系统以Zynq UltraScale MPSoC为例PL侧例化一个GMII-to-AXI4-Stream的MAC桥接逻辑也可以用Xilinx TEMAC但这里用自研或者第三方MAC核讲解逻辑更通用MAC的收发接口分别接到AXI DMA的两个数据通路MM2SMemory Map to Stream内存读到流和S2MMStream to Memory Map流写入内存。接收方向RGMII/PCS接收以太网帧 - MAC解析 - AXI4-Stream接口TVALID/TREADY握手 - S2MM通道 - 数据写入DDR指定缓冲区。发送方向MM2S通道读取DDR缓冲区 - AXI4-Stream接口 - MAC组帧 - RGMII/PCS发送。控制面CPU通过AXI-Lite接口配置AXI DMA寄存器读写描述符响应中断实现业务调度。这个架构最大的优势是“分两半”数据面Data Plane完全由硬件自动完成控制面Control Plane由CPU软件驱动控制。数据面和控制面解耦之后高速传输由硬件保证灵活性由软件保证是我个人最喜欢的折中方案。2. AXI DMA工作机制与关键技术点2.1 AXI DMA寄存器模型AXI DMAPG021里面的寄存器说多不多说少不少但真正要我们开发者写入的寄存器就那么几个。最核心的寄存器包括MM2S_DMACR偏移0x00MM2S通道控制寄存器关键是RSRun/Stop位、S2MM_DMACR偏移0x30里同样有RS位。MM2S_DMASR偏移0x04状态寄存器查IOC_Irq帧完成中断、DMAIntErr等。MM2S_CURDESC偏移0x08当前描述符指针写描述符地址前需要先写这个。MM2S_TAILDESC偏移0x10尾描述符指针这个寄存器非常关键写完它等于告诉DMA“这批描述符你拿去跑”。我见过不少人在调试时只写CURDESC不写TAILDESC结果DMA愣是不启动。实际上AXI DMA的描述符处理是“头尾指针”模型CURDESC指定当前要执行的描述符TAILDESC是收尾信号表示软件已经排好一批活儿硬件可以开始执行了。这个机制后续在软件设计中会非常有用。S2MM通道的寄存器结构与MM2S基本对称不再赘述。2.2 Scatter Gather描述符链的工作原理SG模式是AXI DMA的灵魂。每一个描述符占用固定大小的存储空间我用的State是8字即32字节描述符表本身放在DDR中描述符里的核心字段如下NXTDESC下一个描述符的物理地址。BUFFER_ADDRESS数据缓冲区的物理地址。CONTROL控制字包含缓冲区长度、TX/RX方向的关键位TX里还有TXSOF/TXEOF表示帧首帧尾。STATUS状态字由DMA硬件回写表示传输完成情况。RX方向描述符是由CPU预先分配好缓冲区、填好地址和长度然后排成链表交给DMA。DMA每收满一帧或收到SOF/EOF组合会将该描述符状态字置为“完成”并更新当前描述符地址然后继续取下一个描述符。CPU在中断里只需遍历描述符确认状态就能知道哪个缓冲区收到了完整一帧数据。TX方向则相反CPU把要发送的数据放好填描述符时指定长度和SOF/EOF位DMA取走数据后回写状态CPU回收描述符即可。为什么要用描述符链而不是简单的固定环形缓冲因为以太网帧长度不固定最小64字节最大1518字节如果用固定大小的缓冲区要么浪费空间要么无法容纳最大帧。描述符不同大小的缓冲块组合能灵活应对帧长变化而且天然支持零拷贝重排——我只要改变描述符的缓冲区地址就能把接收缓冲直接交给发送队列。2.3 与AXI4-Stream接口的握手AXI DMA两侧接口非常明晰内存侧是AXI4或AXI4-Lite接口数据流侧是AXI4-Stream接口。AXI4-Stream协议没有地址只有简单的握手TVALID表示数据有效TREADY表示对端可以接收两者同时拉高时完成一拍数据传递。实际操作中MAC出来的数据往往不是标准AXI4-Stream。比如我的MAC输出是32位数据位宽但以太网帧长并不是4字节整数倍最后几字节可能不足32位。这时TKEEP信号就派上用场了TKEEP拉高的字节才是有效数据。DMA会按照TKEEP标记把无效字节剔除只搬运有效内容。这个细节很容易被初学者忽视一旦没处理对收到的数据末尾会多出莫名其妙的字节。补充TLASTTLAST是AXI4-Stream上的帧结束信号在以太网帧最后一个数据拍拉高。MAC/DMA正是靠它来判断一帧结束。如果TLAST没有正确传递DMA会永远等下去或把多帧数据当一帧处理所以调试时务必抓波形确认。3. 硬件设计要点与工程实现3.1 IP配置和关键参数选择在Vivado里例化AXI DMA IP时有几个选项直接影响后续软件编程方式Enable Scatter Gather Engine必须勾选否则退化成简单DMA模式无法支持任意物理地址缓冲。Width of Buffer Length Register默认14位最大支持16383长度的字节传输对于1518字节帧足够但如果你希望一个描述符一次搬运更大块内存比如视频帧请加宽到26位或28位。Address Width根据DDR地址空间选择Zynq常用32位或40位MPSoC如果DDR地址超过4GB必须选40位以上否则高地址空间访问不到。Number of Streams一般选1个如果业务需要多通道并行收发可以选多个但软件复杂度会明显上升。这几个参数配置我在最初项目里吃过亏默认地址宽度只有32位结果我用的是MPSoC的4GB DDR高地址区域完全没法用后来改配置重综合才解决。建议在设计阶段就按“系统DDR到底有多大”来决定别用默认值。3.2 MAC到AXI DMA的接口桥接千兆以太网物理层接口是RGMIIMAC核输出的一般是GMII或RGMII都不直接是AXI4-Stream。需要在MAC后面加一个转换逻辑把GMII字节流转换成AXI4-Stream。这个转换逻辑本身并不复杂主要做把GMII的8位数据按32位拼装。产生TVALID/TREADY握手信号。根据帧边界产生TLAST。根据字节数产生TKEEP。如果要举一个代码层面的参考转换逻辑的状态机主要就四步IDLE等待帧起始、SOP帧起始发第一个数据拍、DATA数据中间拍、EOP帧结束拍组合TLAST和TKEEP。always (posedge clk) begin if (state IDLE gmii_rx_dv) begin state DATA; tvalid 1; tdata {gmii_rxd, next_data...}; // 具体拼装看位宽 end else if (gmii_rx_er || state EOP) begin state IDLE; tvalid 0; tlast 1; end end这段代码只是示意。实现时注意MAC输出的一个帧很可能跨多个AXI4-Stream数据拍SOP和EOP可能在同一拍出现短帧也可能相隔多拍长帧状态机处理时要同时覆盖这两种情况。3.3 时钟与复位设计AXI DMA的时钟输入必须和MAC的数据通路时钟保持同源否则跨时钟域处理会非常麻烦。我习惯把整个以太网数据通路放在同一个时钟域比如125MHz千兆位对应125M时钟MAC和DMA都工作在这个频率上。DDR侧的AXI4接口时钟则由PS端的内存互联提供不需要额外处理。复位方面AXI DMA和整个数据通路都采用异步复位、同步释放逻辑。复位对AXI DMA的状态机影响很大尤其是DMA正在搬运数据时复位可能会出现描述符状态没回写、状态寄存器报错等现象所以软件端复位DMA后一定要清掉状态寄存器里的错误位再重新初始化。3.4 地址映射与Lite总线AXI DMA的控制寄存器通常挂在PS端的AXI-Lite总线上地址映射由Address Editor自动分配。我认为这里最值得注意的一点是AXI DMA的MM2S/S2MM描述符地址和数据缓冲区地址必须是物理地址。在Linux下这意着驱动里必须用dma_alloc_coherent或者类似API去分配拿到的是IO虚拟地址和物理地址的配对而不是普通kmalloc的虚拟地址。裸机环境下更直接直接用物理地址访问即可但要注意DDR地址范围Zynq里DDR起始物理地址一般是0x00100000或者更高取决于你的DDR设定描述符地址不要落在DDR起始前的保留区域。4. 驱动设计与数据通路打通4.1 初始化流程的完整步骤驱动初始化是整个系统打通的重要一环。以裸机驱动为例我一般按以下顺序操作初始化AXI DMA先把两条通道的DMACR.RS清0确保DMA处于停止状态。分配接收缓冲区集合为每个RX描述符分配独立的缓冲区每次默认最大长度设为2048字节覆盖最大以太网帧1518字节再留余量。初始化描述符表对于每个RX描述符填入BUFFER_ADDRESSCONTROL设为缓冲长度NXTDESC指向下一个描述符最后一个描述符的NXTDESC指回第一个环形链。循环设置每个描述符的地址把第一个描述符地址写入S2MM_CURDESC然后用一个循环把后续描述符地址写入S2MM_TAILDESC。注意这里有个细节先写CURDESC再写TAILDESC顺序别反。反了之后DMA会从错误的描述符开始执行。启动S2MM通道置S2MM_DMACR的RS位为1。打开中断注册S2MM的IOC中断使能相应中断。TX描述符表初始化略有不同TX描述符不需要提前绑定缓冲区发送时才填但要先建立好描述符链CONTROL字段的SOF/EOF由驱动在发送时赋值。4.2 收发中断处理逻辑中断处理是整个驱动性能的分水岭。我见过很多人的中断处理写得像轮询一样每来一个帧就做一大堆操作性能当然上不去。我的建议是接收中断里只做三件事读取S2MM_DMASR确认IOC中断源。遍历描述符链找出所有STATUS被硬件标记为完成的描述符。把完成描述符对应的缓冲区上交给协议栈或业务队列然后重新初始化该描述符为“空闲可用”状态。清中断。发送中断处理逻辑类似但更简单找出所有DMA已搬完的TX描述符释放对应的数据缓冲区只要把缓冲区交还操作系统或内存池即可。这里有一个亲测有效的小技巧如果业务允许把TX完成中断和RX中断合并处理或者干脆关闭TX完成中断改用描述符轮询来回收能减少大量中断开销。尤其在每个帧都单独发送的小包场景TX完成中断数量往往比RX还多反而拖累吞吐。4.3 缓冲管理与零拷贝实现零拷贝是“数据直传”的一个高级形态。举个例子我的业务需要把网口A收到的UDP包直接转发到网口B传统做法是RX DMA写入缓冲A - CPU拷贝到缓冲B - TX DMA读取缓冲B。有了描述符链我只需要把缓冲A的物理地址直接写到TX描述符的BUFFER_ADDRESSCPU省掉拷贝这一步。这个操作的代价是必须保证缓冲A在TX DMA搬完之前不会被RX DMA再次写入。这对驱动的描述符管理提出了要求需要维持缓冲区“所有权”的概念。每个缓冲区同时只能属于RX或TX队列切换所有权时注意访问同步。硬件上还有Cache一致性问题。DMA写DDR时数据会被写进内存层级但如果CPU也缓存了同一块地址的数据比如CPU可能提前看过就会出现缓存不一致。解决方案是用dma_alloc_coherent分配一致性内存或者用dma_map_single配合DMA API做同步。因为这个细节在实际调试中极容易出问题能提前学明白就少走很多弯路。4.4 性能调优的三个层面把最基础的通路跑通之后性能调优就是重头戏了。一般来说需要关注三个层面中断合并Interrupt CoalescingAXI DMA原生支持简单的延时中断机制。你可以在计时器里设定延时等一段时间或积攒多个包后再统一触发中断。代价是增加了单包延迟但CPU占用率明显下降。如果业务能接受亚毫秒级延迟这个收益非常大。描述符深度与环形缓冲大小描述符越多DMA越不容易“空转”但描述符本身也占用内存且在描述符遍历时消耗CPU时间。我实际测试中64~256个描述符是性能和资源相对平衡的范围少于32个时高帧率下很容易丢包。Alignment对齐缓冲区对齐到Cache Line通常64字节非常关键。原因有两点一是避免一个描述符缓冲区与相邻缓冲区共用Cache Line导致伪共享二是对齐后DMA访问DDR效率更高。跑到千兆线速还要动态检测丢包。我在软硬件联调时通常会在发送端固定发定量的大包UDP然后接收端统计收到的包数量两个数字之差就是丢包数。这个方法简单粗暴但非常有效。5. 常见问题与排查经验5.1 典型故障现象快速定位这部分内容建议直接抄录到你的调试笔记里。我把自己踩过的坑按现象归好类做成了一张速查表每次遇到问题先对表定位往往能省下半天抓波时间。故障现象可能原因排查思路DMA不启动寄存器状态不变TAILDESC没写或写错确认TAILDESC写入顺序确认描述符地址是物理地址数据多出几个字节TKEEP处理错误无效字节被搬运检查MAC转AXI4-Stream逻辑里TKEEP生成一帧数据被切分成多帧TLAST信号未及时拉高或丢失抓AXI4-Stream接口波形确认TLAST时序接收缓冲区数据不断变化CPU缓存与DMA不一致检查是否用了dma_alloc_coherent必要时做cache clean/invalidate网口发不出去TX描述符一直Busy发送缓冲区地址是虚拟地址确认用的是物理地址并做了正确的DMA映射高帧率下大量丢包中断响应不及时或描述符跑空增加描述符数量开启中断合并寄存器写入不生效AXI-Lite接口地址不对或时钟未稳定先读回校验地址再确认整个互联时钟5.2 调试必备ILA抓包的几个关键信号遇到疑难杂症仅仅看寄存器信息往往不够必须回到波形上找证据。我常用的ILA观测点有几个AXI DMA的AXI4-Stream接口TVALID、TREADY、TLAST、TKEEP。如果数据通路上有自研逻辑观测帧起始/结束状态机的状态跳转。MM2S/S2MM侧AXI总线AW/AR通道的握手信号能反映DDR访问效率。波形和描述符日志相结合能快速锁定问题先在驱动里打印每个描述符回写的STATUS再对比ILA波形里的帧时序一般很快能找到是硬件问题还是软件问题。5.3 身边案例一次高帧率丢包背后的原因之前做测试时千兆线速吞吐怎么都跑不满接收端总是掉包。查驱动、查中断、查描述符各项指标看着都正常最后用ILA抓到一条信息RX侧的TREADY偶尔拉低且持续时间较长。顺着这条线索追下去定位到MAC桥接逻辑里的FIFO深度不够。突发流量来的时候FIFO写满只能把TREADY拉低等待DMA消费。DMA端本身是“来多少收多少”的状态问题反而出在中间缓存太平庸。把FIFO深度从512改成2048之后不再出现TREADY长时间拉低的现象丢包归零。这个案例的启发是数据直传并非“只要DMA够快就行”整条链路上任何一个部件的吞吐短板都会成为瓶颈。排查时不要只顾DMA本身也要盯住MAC、FIFO、互联总线和DDR的带宽余量。5.4 功耗与资源占用情况AXI DMA在资源占用上不算大但我手头项目的实际数据可以提供参考AXI DMA IP在Zynq UltraScale上约占用1500~2000个LUT以及相近数量的FFBRAM消耗主要集中在内部缓冲和描述符缓存大约6~10个BRAM36。相比MAC和DDR控制器AXI DMA的面积开销算是很小的。功耗方面AXI DMA本身的动态功耗相比DDR控制器微乎其微真正的功耗大头在DDR读写和MAC PHY上。如果想进一步省电可以在DMA空闲时将其置于低功耗或关闭时钟但这个优化对于大多数以太网应用来说收益微薄我建议把精力放在数据帧合并和批量传输上。6. 实测总结与扩展思路之前项目中我用这套AXI DMA直传方案实现了千兆以太网接收CPU在ARM Cortex-A53核上跑Linux网口持续收UDP包时CPU占用率约12%~15%而之前的纯中断方案实测要高达70%。数据直传到用户态之后业务应用能很轻松地处理数据而不会出现瓶颈。以太网帧的搬运只是一个小场景同样的架构可以延伸到很多领域。比如把AXI DMA的数据流接口替换成视频流或者ADC采样流就能实现高速数据采集到DDR/存储的直通如果再接上MIPI CSI-2接口就变成了一条图像采集通路。描述符链在这类系统里同样是核心组件缓冲管理逻辑几乎可以复用。在收尾处我再分享一个实测教训每次调DMA先把描述符状态打印全再开中断。很多人为了省事一开始就把中断打开出问题后排查范围太大。先保证“不开中断也能搬数据”再开中断这样调试节奏就稳多了。后续可以沿着这个方向继续扩展比如加入多队列支持、实现硬件时间戳、做UDP分片重组甚至把DMA描述符的管理下沉到硬件定时器驱动的状态机里进一步解放CPU。这套AXI DMA直传方案作为地基往上叠再多业务都塌不了值得好好吃透。