ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA开发中AXI DMA实战:从选型配置到调试优化的完整指南

2026/10/6 1:32:03 拓冰建站 浏览量
FPGA开发中AXI DMA实战:从选型配置到调试优化的完整指南 在FPGA开发里PS和PL之间的数据搬运是个绕不开的坎。你可能会遇到这样的场景PL端采集完一批数据PS端要拿去处理或者存到DDR里如果全靠CPU去读写寄存器那速度慢得让人抓狂而且CPU基本被绑死在这件事上什么都干不了。AXI DMA就是为解决这个问题而生的它能在PS的DDR和PL的流接口之间搭一条高速通道让数据自己跑起来CPU只需要下个命令、等个中断就行。这篇内容适合已经了解FPGA基本开发流程、用过Vivado搭过Block Design但对AXI DMA的配置细节和实际调试还不太有把握的开发者。我会从选型思路讲到寄存器操作再到实际调试中那些让人抓狂的坑尽量把每个环节的“为什么”说清楚。1. 先搞清楚AXI DMA到底解决了什么问题1.1 从一次实际的数据搬运需求说起假设你在做一个图像采集项目PL端接了一个CMOS传感器每帧图像是1920x1080的灰度图大概2MB的数据量。传感器通过LVDS或者DVP接口把像素数据送进FPGA你在PL端做了些预处理之后需要把这2MB的数据送到PS端的DDR里让运行在ARM核上的Linux应用程序去读取和处理。如果不用DMA你能怎么做最直接的办法是在PL端写一个寄存器组PS端通过AXI-Lite总线轮询读取。但AXI-Lite的位宽通常是32位时钟频率可能也就100MHz左右理论带宽上限是400MB/s实际因为握手开销和CPU轮询延迟能跑到50MB/s就算不错了。2MB的数据要传40ms这还没算CPU被完全占住的时间。对于实时性要求高的场景这根本不可接受。AXI DMA的做法完全不同。它内部有专门的读写通道每个通道都是独立的AXI4 Master接口可以直接访问DDR。PL端的数据通过AXI-Stream接口送入DMADMA自动打包成AXI突发传输写到DDR指定地址。整个过程CPU只需要配置几个寄存器启动传输然后等中断通知完成就行。带宽可以轻松跑到几百MB/s甚至上GB/s取决于DDR的实际性能和DMA的位宽配置。1.2 AXI DMA在Xilinx生态里的定位Xilinx现在是AMD提供了好几个DMA相关的IP容易让人搞混。AXI DMA、AXI VDMA、AXI CDMA、AXI MCDMA名字看着差不多用起来差别不小。AXI DMA是最基础的一款主要面向memory-to-stream和stream-to-memory的场景。它有一个读通道MM2SMemory-Mapped to Stream和一个写通道S2MMStream to Memory-Mapped分别对应从DDR读数据发到PL、从PL收数据写到DDR。每个通道支持独立的使能和配置适合点对点的数据搬运。AXI VDMA是专门为视频流设计的支持多帧缓冲和帧同步信号适合做视频帧的缓存管理。如果你只是搬原始数据不需要帧同步用AXI DMA就够了VDMA反而增加复杂度。AXI CDMA是纯memory-to-memory的DMA没有Stream接口适合在DDR不同地址之间搬数据。AXI MCDMA是多通道版本适合需要同时处理多路数据流的场景。选型的原则很简单如果你的数据源或目的地是AXI-Stream接口用AXI DMA如果是视频帧带同步信号考虑VDMA如果只是内存间搬运用CDMA。对于大多数PL和PS之间的数据交互场景AXI DMA是最合适的选择。1.3 简单模式和Scatter-Gather模式的区别AXI DMA有两种工作模式Simple Mode和Scatter-Gather Mode。这个选择直接影响你的软件复杂度和传输效率。Simple Mode下每次传输你只能指定一个连续的DDR缓冲区DMA把整块数据搬完就结束。软件上就是写几个寄存器源地址、目标地址、传输长度然后启动。中断来了就表示传完了。这种方式简单直接适合数据量不大、传输次数不频繁的场景。Scatter-Gather模式就复杂多了。它需要一个描述符链表每个描述符包含一块数据的地址和长度。DMA会自动遍历链表把多块分散的数据搬完。这种方式适合数据包大小不固定、需要零拷贝或者多缓冲区的场景。但代价是你需要在DDR里维护描述符环软件初始化更复杂调试也更麻烦。我的建议是如果你的数据是固定大小的连续块Simple Mode完全够用别给自己找麻烦。只有当你有明确的多缓冲区管理需求或者数据包长度变化很大时才考虑Scatter-Gather。很多项目一开始就上SG模式结果调试描述符环花了好几天其实Simple Mode就能满足需求。2. Vivado里的Block Design该怎么搭2.1 IP核的关键参数配置在Vivado里添加AXI DMA IP之后双击打开配置界面有几个参数需要仔细考虑。首先是Width of Buffer Length Register。这个参数决定了单次传输的最大长度。默认是14位也就是最大16383字节。如果你要传2MB的一帧图像14位肯定不够。每增加1位最大传输长度翻倍。26位对应64MB对于大多数应用足够了。但要注意这个位宽也影响描述符的大小如果用SG模式增加位宽会消耗更多逻辑资源。然后是Memory Map Data Width和Stream Data Width。前者是DMA访问DDR的位宽通常设成64位或128位以匹配DDR的位宽这样能获得更高的带宽。后者是AXI-Stream接口的位宽取决于你PL端数据源的位宽。如果Stream位宽是32位而Memory Map是64位DMA内部会做位宽转换但效率会受一些影响。理想情况下两者保持一致或者成整数倍关系。Max Burst Size决定了DMA每次突发传输的最大节拍数。设成256是常见的选择能充分利用AXI总线的突发传输能力。设得太小会导致总线效率下降设得太大在某些DDR控制器上可能反而效果不好。一般保持默认值就行除非你有明确的性能调优需求。还有一个容易忽略的参数是Enable Scatter Gather Engine。如果你用Simple Mode记得把这个勾掉否则Vivado会生成SG相关的逻辑浪费资源不说软件上还要多处理一堆东西。2.2 时钟域和复位连接AXI DMA有两个时钟域一个是AXI-Lite控制接口的时钟s_axi_lite_aclk一个是数据通道的时钟m_axi_mm2s_aclk和m_axi_s2mm_aclk。通常这两个时钟可以接同一个时钟源比如PS端输出的FCLK_CLK0频率100MHz或150MHz。但如果你PL端的数据源用的是不同的时钟比如视频像素时钟是74.25MHz那就需要把Stream接口的时钟和DMA的数据时钟处理好。AXI DMA的Stream接口时钟是m_axi_s2mm_aclk对于S2MM通道如果PL逻辑的时钟和这个不一致需要在Stream接口上加一个FIFO做跨时钟域处理。这个FIFO可以自己写也可以用AXI-Stream Data FIFO IP。复位方面AXI DMA的复位信号是低电平有效。通常接PS端的FCLK_RESET0_N或者自己产生的复位逻辑。注意复位必须同步到对应的时钟域异步复位同步释放是基本要求。我见过有人在复位上偷懒结果DMA偶尔启动不了查了半天才发现是复位释放时机不对。2.3 地址分配与DDR访问范围在Address Editor里你需要给AXI DMA的M_AXI_MM2S和M_AXI_S2MM接口分配DDR地址范围。这个范围必须覆盖你实际要访问的DDR区域。比如PS端DDR是1GB从0x00000000到0x3FFFFFFF那DMA的地址范围至少要包含你计划使用的缓冲区地址。这里有个常见的坑如果你在Linux下跑DDR的一部分可能被内核占用用户空间能用的物理地址是有限的。你需要确保DMA访问的物理地址在保留范围内否则会踩到内核数据导致系统崩溃。后面讲软件部分时会详细说这个问题。另外如果你用的是Zynq MPSoC或者VersalDDR可能有多個区域地址映射更复杂。一定要对照芯片手册确认物理地址范围别想当然。3. 软件端的寄存器操作与中断处理3.1 Simple Mode下的寄存器配置流程AXI DMA的寄存器手册有好几十页但Simple Mode下常用的就那么几个。以S2MM通道PL到DDR为例基本流程是这样的第一步复位DMA通道。往S2MM控制寄存器偏移0x30写0x00000004这是Soft Reset位。然后轮询这个寄存器直到Reset位自动清零表示复位完成。第二步配置中断。如果你用中断方式需要使能IOC_Irq传输完成中断和Err_Irq错误中断。往S2MM控制寄存器写0x00001000IOC_IrqEn和0x00004000Err_IrqEn。如果不用中断就轮询S2MM状态寄存器偏移0x34的Idle位。第三步设置目标地址。往S2MM目标地址寄存器偏移0x48写入DDR缓冲区的物理地址。注意这个地址必须是物理地址不是虚拟地址。第四步启动传输。往S2MM长度寄存器偏移0x58写入要传输的字节数这个写操作同时会启动DMA传输。第五步等待完成。如果用了中断就在中断服务函数里处理如果轮询就不断读状态寄存器直到Idle位为1。MM2S通道的流程类似只是寄存器偏移不同。控制寄存器在0x00状态在0x04源地址在0x18长度在0x28。3.2 中断控制器与Linux下的处理在裸机环境下中断处理相对直接注册中断服务函数就行。但在Linux下事情复杂一些。你需要一个内核驱动来管理DMA和中断用户空间通过设备节点或者ioctl来交互。Xilinx提供了DMA Proxy驱动和AXI DMA驱动但实际项目中我建议自己写一个简单的字符设备驱动。原因在于通用驱动往往为了兼容各种场景做了很多抽象反而不好控制。自己写的话核心就是几件事申请DMA缓冲区、映射物理地址、注册中断处理函数、提供read/write接口给用户空间。DMA缓冲区的分配是个关键点。不能用普通的kmalloc因为那只能保证虚拟地址连续物理地址不一定连续。要用dma_alloc_coherent它能保证物理地址连续并且是cache一致的。如果你需要非一致性映射比如自己管理cache用dma_alloc_attrs配合DMA_ATTR_NON_CONSISTENT标志。中断处理函数里先读DMA的状态寄存器确认中断原因清除中断标志然后唤醒等待队列或者完成completion。注意在中断上下文里不能做耗时操作数据处理的活留给下半部或者用户空间线程。3.3 地址转换与cache一致性这是最容易出问题的地方。在Linux下用户空间看到的是虚拟地址DMA需要的是物理地址。你需要用virt_to_phys或者dma_map_single来做转换。但要注意如果缓冲区是cacheable的DMA写入DDR后CPU的cache里可能还是旧数据。这时候需要invalidate cache或者使用非一致性内存。对于S2MM方向PL到DDRDMA写完之后CPU读之前必须invalidate对应的cache行。对于MM2S方向DDR到PLCPU写完之后DMA读之前必须flush cache。这些操作在驱动里用dma_sync_single_for_cpu和dma_sync_single_for_device来完成。如果你用的是dma_alloc_coherent分配的内存驱动会自动处理cache一致性你不需要手动操作。但代价是这块内存是uncached的CPU访问速度会慢一些。对于大数据量的场景uncached访问可能成为瓶颈。这时候可以考虑用非一致性内存加手动cache管理但复杂度会上升。4. 调试中那些让人抓狂的坑4.1 DMA启动后没反应状态寄存器一直是Idle这种情况我遇到过好几次原因各不相同。最常见的是Stream接口没有数据或者握手信号不对。AXI DMA的S2MM通道需要Stream源持续提供有效数据如果PL端的Stream源没有正确产生TValid和TDataDMA会一直等下去。排查方法在Vivado里加一个ILA核抓Stream接口的TValid、TReady、TLast信号。正常情况下应该能看到TValid和TReady同时为高的数据传输。如果TValid一直为低说明PL端逻辑没输出数据如果TValid为高但TReady一直为低说明DMA没准备好接收可能是DMA没启动或者配置有问题。还有一个可能的原因是TLast信号缺失。AXI DMA在S2MM模式下依赖TLast来判断传输包边界。如果你的Stream源没有正确产生TLastDMA会认为传输还没结束状态寄存器不会回到Idle。检查你的Stream源逻辑确保在最后一个数据节拍上TLast为高。4.2 传输完成但数据不对地址偏移或数据错位数据错位通常和位宽转换有关。如果你的Stream位宽是32位Memory Map位宽是64位DMA内部会把两个32位数据拼成一个64位写入DDR。如果你在软件端按32位读取字节序可能不对。这时候需要检查DMA的位宽转换设置或者在软件端做相应的字节序调整。另一个常见原因是地址没有对齐。AXI DMA要求传输地址按数据位宽对齐。如果Memory Map位宽是64位目标地址必须是8字节对齐。地址不对齐会导致传输错误或者数据错位。在分配DMA缓冲区时用dma_alloc_coherent会自动对齐但如果你自己指定地址一定要检查对齐。还有一种情况是传输长度设置错误。比如你实际要传1920x10802073600字节但长度寄存器写成了2073600/4518400误以为按32位字计数。AXI DMA的长度寄存器是以字节为单位的别搞错了。4.3 中断来了但系统卡死或崩溃在Linux下中断处理不当很容易导致系统崩溃。最常见的问题是中断没有正确清除导致中断风暴。AXI DMA的中断状态寄存器是写1清除的你在中断处理函数里必须读状态寄存器然后写回相同的值来清除中断。如果忘了这一步中断会一直触发系统就卡死了。另一个问题是中断处理函数里访问了非法地址。比如DMA传输完成后你在中断里直接访问用户空间的虚拟地址但那个地址在当前上下文可能无效。正确的做法是在中断里只做最小必要的工作比如设置一个标志或者完成一个completion实际的数据处理留给用户空间线程。还有cache一致性问题导致的数据损坏。如果DMA缓冲区是cacheable的DMA写入后CPU没有invalidate cacheCPU读到的可能是旧数据。更危险的是如果CPU的cache里有过期的脏数据cache回写时可能覆盖DMA刚写入的数据。这种问题往往表现为偶发的数据错误很难定位。用dma_alloc_coherent或者严格管理cache同步可以避免。4.4 带宽远低于预期如果你发现DMA的实际带宽只有理论值的十分之一先检查这几个地方。第一DDR控制器的配置。在Zynq里DDR控制器的性能受很多参数影响比如突发长度、调度策略等。默认配置不一定是最优的。你可以用Xilinx提供的DDR性能测试工具或者自己写一个简单的读写测试来评估DDR的实际带宽。第二AXI总线的位宽和频率。如果AXI互联的位宽是32位而DMA是64位数据会被拆成两次传输带宽减半。检查Block Design里AXI SmartConnect或者AXI Interconnect的位宽配置确保和DMA匹配。第三DMA的Max Burst Size。如果设得太小比如只有16每次突发传输的数据量很少总线利用率低。设成256通常能获得较好的性能。第四Stream接口的握手效率。如果PL端的Stream源不能持续提供数据DMA会频繁等待带宽自然上不去。用ILA抓一下TValid的占空比如果经常出现空闲周期说明数据源供不上。5. 性能调优与进阶技巧5.1 双缓冲与乒乓操作单缓冲区的模式下DMA传输期间CPU不能碰这块内存传输完成后CPU处理期间DMA又不能启动下一次传输。这种串行方式浪费了很多时间。双缓冲乒乓缓冲可以解决这个问题准备两块缓冲区DMA往缓冲区A写的时候CPU处理缓冲区BDMA写完A切换到BCPU处理A。这样传输和处理可以并行。实现上你可以在中断处理函数里切换缓冲区指针然后立即启动下一次DMA传输。用户空间通过两个设备节点或者一个带缓冲索引的ioctl来获取数据。关键是要保证CPU在处理一块缓冲区时DMA不会去写它。用两个独立的completion或者信号量来同步。如果数据量更大可以用多缓冲区环形队列。DMA按顺序填充每个缓冲区CPU按顺序消费。这种模式在视频流处理里很常见能有效平滑传输抖动。5.2 中断合并与轮询的取舍每次传输完成都产生一个中断如果传输很频繁比如每帧只传几KB中断开销会很大。Linux下一次中断处理大概几微秒如果每100微秒就中断一次CPU有5%的时间花在中断上还不算上下文切换的开销。AXI DMA支持中断合并Interrupt Coalescing可以设置每传输N个描述符或者延迟M微秒后才产生中断。在SG模式下这个功能很有用。Simple Mode下没有这个功能但你可以通过增大单次传输长度来减少中断频率。另一种思路是混合模式短传输用轮询长传输用中断。在驱动里根据传输长度动态选择。轮询虽然浪费CPU但对于短传输来说省下的中断开销可能更划算。5.3 用AXI Performance Monitor做量化分析Xilinx提供了AXI Performance Monitor IP可以挂在AXI总线上统计读写吞吐量、延迟、事务数等指标。在调优阶段把它加到Block Design里通过AXI-Lite读取统计寄存器能帮你快速定位瓶颈。比如你发现DMA的写入带宽只有200MB/s但DDR的理论带宽是1GB/s。用Performance Monitor一看发现AXI总线上有大量的小事务每次只传32字节。这就说明DMA的突发长度没设对或者数据源提供的Stream数据不连续。根据这个线索去调整参数往往能事半功倍。5.4 在Zynq MPSoC上的注意事项Zynq MPSoC的架构比Zynq-7000复杂得多有多个DDR区域和更复杂的AXI互联。AXI DMA在MPSoC上使用时有几个额外要注意的点。首先是DDR地址映射。MPSoC的DDR可能分为多个区域低地址区和高地址区的性能特性可能不同。尽量把DMA缓冲区分配在性能较好的区域。具体哪个区域性能好需要查芯片手册或者实际测试。其次是CCICache Coherent Interconnect的影响。MPSoC的CPU通过CCI访问DDR如果DMA也走CCIcache一致性由硬件保证软件上简单很多。但CCI可能成为带宽瓶颈。如果DMA绕过CCI直接访问DDR带宽更高但需要软件手动管理cache。这个取舍需要根据实际需求决定。还有一点是MPSoC的PL时钟和PS时钟的关系。MPSoC的PL端时钟资源更丰富但和PS端时钟的相位关系可能不确定。跨时钟域处理要更加小心必要时用异步FIFO。6. 从裸机到Linux的移植经验6.1 裸机代码的寄存器操作直接搬过来行不行很多开发者先在裸机环境下调通了DMA然后想直接把代码搬到Linux驱动里。寄存器操作部分确实可以照搬但有几个关键差异必须处理。裸机下你可以直接访问物理地址Linux下必须用ioremap把物理地址映射到虚拟地址。AXI DMA的寄存器基地址在设备树里定义驱动里用platform_get_resource获取然后devm_ioremap_resource映射。之后所有寄存器读写都通过映射后的虚拟地址进行。中断处理差异更大。裸机下你注册的是中断向量表里的处理函数Linux下要用request_irq注册并且中断号从设备树里获取。中断处理函数的签名也不同返回值和处理流程都要按Linux的规范来。内存分配是另一个大差异。裸机下你可以直接指定一个物理地址作为DMA缓冲区Linux下必须用DMA API分配确保物理地址连续且cache一致。6.2 设备树节点的编写要点在设备树里描述AXI DMA节点关键属性包括compatible字符串、reg寄存器基地址和范围、interrupts中断号和类型、dmas和dma-names如果用DMA Engine框架。compatible字符串通常用xlnx,axi-dma-1.00.a或者类似的版本号。reg属性要和Vivado Address Editor里的地址一致。interrupts属性要对应正确的GIC中断号这个在Vivado的Address Editor里也能看到。如果你用DMA Engine框架还需要在设备树里引用DMA控制器并在客户端设备节点里用dmas属性指定通道。这种方式更符合Linux的DMA使用规范但学习曲线陡一些。对于简单的点对点传输自己写字符设备驱动可能更直接。6.3 用户空间接口的设计驱动对用户空间暴露什么接口直接影响应用程序的开发难度。最简单的做法是提供一个字符设备支持open、read、write、ioctl。read用于从DMA缓冲区获取数据write用于向DMA缓冲区发送数据ioctl用于配置传输参数和启动传输。更高效的做法是用mmap把DMA缓冲区映射到用户空间应用程序直接读写省去内核和用户空间之间的数据拷贝。但mmap的缓冲区管理更复杂需要确保DMA传输期间用户空间不会访问正在被DMA写入的区域。还有一种做法是用UIOUserspace I/O框架把DMA寄存器映射到用户空间应用程序直接操作寄存器。这种方式最灵活但失去了内核的保护一个错误的寄存器操作可能导致系统崩溃。适合对性能要求极高且开发者对硬件非常熟悉的场景。我个人倾向于字符设备加mmap的方案。驱动负责DMA传输和中断处理用户空间通过mmap直接访问数据缓冲区通过ioctl触发传输和等待完成。这样既保证了安全性又避免了数据拷贝的开销。7. 几个实际项目中的典型场景7.1 高速数据采集系统在一个雷达信号采集项目里PL端ADC以200MSPS的速率采样16位精度数据率是400MB/s。数据经过PL端的数字下变频和抽取后降到50MB/s然后通过AXI DMA送到PS端DDR。这个场景下DMA的配置要点是Stream位宽设成64位一次传4个16位采样Memory Map位宽128位Max Burst Size 256。用S2MM通道Simple Mode每次传输1MB数据。中断频率是每秒50次完全在可接受范围内。关键坑点是ADC数据的TLast信号。因为数据是连续流没有自然的包边界。我们在PL端加了一个计数器每采集够1MB数据就产生一个TLast脉冲。这样DMA每次传输正好1MB软件处理起来也方便。7.2 视频帧缓存管理另一个项目是做视频拼接两路1080p视频输入PL端做缩放和拼接后输出一路4K视频。中间需要把拼接后的帧缓存到DDR再由PS端做H.264编码。这个场景数据量大4K帧约12MB实时性要求高30fps。我们用AXI VDMA而不是AXI DMA因为VDMA支持帧同步和多帧缓冲。但VDMA的配置比DMA复杂得多尤其是帧缓冲环的管理。踩过的坑是帧缓冲数量不够导致丢帧。一开始只分配了3个帧缓冲结果编码器偶尔处理慢了VDMA写满所有缓冲后只能覆盖未处理的帧。后来增加到5个缓冲问题解决。帧缓冲数量的经验值是编码器最坏情况下的处理时间乘以帧率再加2个余量。7.3 与自定义IP的Stream对接很多时候PL端的数据源是自定义IPStream接口的时序可能不完全符合AXI-Stream规范。最常见的问题是TValid和TReady的握手逻辑写错导致数据丢失或重复。AXI-Stream的握手规则是TValid由发送方控制TReady由接收方控制只有当两者同时为高的时钟上升沿数据传输才发生。发送方不能在TValid为高后撤销它直到TReady为高完成握手。接收方不能在TValid为低时拉高TReady又拉低除非TValid一直为低。我见过有人在自定义IP里把TValid和TReady直接连在一起以为这样就能传输。实际上这会导致组合逻辑环路时序根本过不了。正确的做法是用状态机或者FIFO来管理握手。调试时用ILA抓TValid、TReady、TData、TLast四个信号设置触发条件为TValid TReady观察数据传输是否符合预期。如果发现TValid和TReady同时为高的周期数少于预期说明握手有问题。8. 写在最后的一些个人体会AXI DMA这个IP说复杂也复杂说简单也简单。核心就是配置好几个寄存器把Stream接口的时序搞对剩下的就是调试和优化。我见过很多新手在这个IP上卡很久往往不是因为原理多难而是被一些细节坑住了。我的建议是第一次用的时候先用最简单的配置跑通一个小的数据传输比如从PL发256字节到DDR确认整个链路是通的。然后再逐步增加数据量、调整位宽、优化性能。不要一上来就搞大而全的配置出了问题很难定位。另外ILA核是你最好的朋友。在Stream接口、AXI总线和DMA寄存器上都挂上ILA出问题时抓波形看比盯着代码猜效率高得多。Vivado的硬件管理器里也能实时查看DMA的状态寄存器调试阶段很有用。最后说一个容易被忽略的点DMA缓冲区的对齐。不管是Simple Mode还是SG模式缓冲区地址一定要按数据位宽对齐。64位位宽就按8字节对齐128位就按16字节对齐。不对齐的地址在某些情况下能工作但性能会下降而且可能在某些DDR控制器上直接报错。用dma_alloc_coherent分配的内存天然是对齐的自己管理内存时一定要注意这一点。