
简介这是一份面向Zynq SoC开发者的AXIDMA驱动源码包适用于在Petalinux环境中移植、编译和调试Xilinx AXI DMA控制器帮助解决处理器系统PS与可编程逻辑PL之间高效批量数据迁移的问题。整个压缩包仅84KB包含25个文件以C源文件9个和头文件5个为核心辅以Makefile、Kbuild、配置模板和驱动模块构建脚本这些构建文件可与源码配合直接挂接到Petalinux内核驱动目录中完成交叉编译。已有1635人学习下载。压缩包内不仅提供libaxidma用户库和字符设备驱动源码还附带DMA传输、性能测试、图像显示等示例程序以及README与LICENSE文档方便对照学习DMA传输流程和API调用方式。对于希望深入理解AXI总线协议、DMA工作原理及Petalinux驱动移植步骤的开发者这是一份精简而完整的参考实现覆盖从设备树配置到中断处理、从用户态API到硬件控制的关键环节。1. AXI DMA驱动不是玄学先弄懂数据是怎么绕过CPU的做 FPGA 加速卡或者高速数据采集的人迟早会撞上一个问题ADC 或者高速接口往内存里灌数据的速度能把 CPU 的中断打到怀疑人生。去年我做某图像采集项目用了一个带 AXI Stream 接口的采集 IP刚开始直接用中断方式搬运数据每秒几十万次中断系统直接卡成幻灯片。后来把方案换成 DMADirect Memory Access数据从采集端到 DDR 内存完全不经过 CPU 搬运CPU 只在传输完成时收到一个中断这个问题才彻底解决。这套xilinx_axidma-master.zip就是解决这个场景的东西——一份基于 AXI DMA 的 Linux 驱动源码包包含了内核驱动模块、设备树配置和测试用例。它适合两类人一类是第一次在 FPGA Linux 平台上做 DMA 驱动、想找一份能直接编译跑通的参考实现另一类是已经在用官方驱动但想理解描述符、中断、mmap 这些底层机制好做二次开发的。文章会从模块构成、核心代码、编译移植到踩坑记录把这套驱动完整过一遍。2. 模块构成与技术选型从 IP 核到 Linux 驱动的三层链路2.1 AXI DMA 外设侧MM2S / S2MM 双通道与描述符环这套驱动对应的外设最常见的做法是挂在 AXI 总线上的 DMA 控制器 IP 核它与 CPU 侧的交互就是两组寄存器加一块描述符内存。DMA 核内部通常有两条独立通道MM2SMemory-Mapped to Stream负责把内存里的数据搬给外设比如发数据给 DAC、以太网 MACS2MMStream to Memory-Mapped负责把外设来的数据流搬进内存比如 ADC 采样数据。通道的工作核心是描述符环Descriptor Ring。驱动把待传输的数据地址、长度、下一个描述符指针写进内存中的描述符表然后更新 DMA 核的尾指针寄存器DMA 核就会自己按描述符链一条一条搬数据搬完一条通过中断通知驱动。描述符环是理解这套驱动的关键——它本质上就是驱动和 DMA 控制器之间的“任务队列协议”。源码包里对这两条通道的处理方式通常不会拆成两个独立驱动文件而是放在同一个平台驱动里。注册两个 DMA 通道对应的字符设备节点或者统一暴露为一个设备节点用户态通过 ioctl 指定操作 MM2S 还是 S2MM。2.2 驱动侧分层platform_driver dmaengine 字符设备在 Linux 内核里驱动通常不会直接去操作 DMA 控制器的寄存器而是走dmaengine框架。你写的是dmaengine设备的用户consumer框架负责管理虚拟通道、描述符池和硬件通道之间的映射。这套驱动的结构我最常见到的布局是三层第一层是platform_driver负责从设备树里拿到 DMA 控制器的寄存器地址、中断号、通道编号做 ioremap 和 request_irq。第二层是dmaengine相关实现包括填充dma_device结构体、实现device_prep_slave_sg或device_prep_dma_cyclic这类回调。第三层是字符设备接口用misc_register或者cdev_add暴露给用户态提供 open、ioctl、mmap 三个核心操作。这种分层的好处是如果只是做数据搬运用户态写ioctl就可以了如果想接 ALSA、V4L2 这类内核子系统可以直接复用第二层的dmaengine接口不用动字符设备逻辑。2.3 源码包目录结构驱动、设备树、测试程序的分工拿到源码包之后建议先花五分钟扫一眼目录结构。多数这类驱动工程的布局比较接近下面这套我按最常见的安排列一下路径片段内容典型文件内核驱动字符设备和 dmaengine 实现axidma.c、axidma.h设备树覆盖描述 DMA IP 的节点和中断dma.dtsi、system.dts用户态测试验证收发正确性的工具dma_test.c、loopback_test.c构建脚本交叉编译辅助Makefile、build.sh测试程序通常不是随便写来占位置的——它验证两条重要路径一是ioctl下发传输描述符的路径是否能正确触发 DMA二是 DMA 完成后用户态数据是否完整。如果你拿到源码后先编译跑dma_test能通就说明你的设备树中断号和寄存器地址基本没错。3. 核心代码拆解一次 DMA 传输从 ioctl 到中断的完整路径3.1 通道结构体描述符队列和状态机的最小单元驱动里最核心的数据结构是描述每个 DMA 通道的实例结构体。常见的写法类似这样struct axidma_channel { struct device *dev; void __iomem *regs; /* DMA 控制器寄存器基地址 */ int irq; /* 该通道中断号 */ spinlock_t lock; /* 保护描述符队列的自旋锁 */ struct list_head pending_list; /* 待提交的描述符链表 */ struct list_head done_list; /* 已完成但未回收的描述符链表 */ struct dma_chan *chan; /* dmaengine 虚拟通道 */ dma_addr_t desc_dma_addr; /* 描述符表的 DMA 地址 */ struct axidma_ring_desc *desc_ring; /* 硬件描述符环的内存映射 */ size_t ring_size; /* 描述符环深度 */ u32 chan_id; /* 0 MM2S, 1 S2MM */ bool running; /* 通道是否正在搬运数据 */ };这个结构体把一个 DMA 通道在软件侧需要的东西全包进去了。irq和regs来自设备树解析是通道能工作的硬件基础pending_list和done_list是软件队列用户在ioctl里提交一个传输请求驱动把它包装成描述符挂在pending_list上DMA 中断触发后再移到done_list等待用户态取走结果。参数上最需要关注的是ring_size。描述符环是固定深度的我一般会配成 16 或者 32。如果 DDR 带宽高、传输频繁但环深度太小驱动可能来不及补描述符导致 DMA 停顿环太深又会浪费内存每个描述符通常占 64 字节对齐的一块空间这个属于调参的活后面性能部分会细说。3.2 中断处理描述符回收与下一次传输的启动中断处理是 DMA 驱动里最容易翻车的地方。硬件搬完一条链上的数据后会写一个状态寄存器的中断位驱动需要判断是 MM2S 完成还是 S2MM 完成然后做两件事把完成的描述符移到done_list再从pending_list里取下一个待传输描述符写进硬件。static irqreturn_t axidma_irq_handler(int irq, void *dev_id) { struct axidma_channel *chan dev_id; u32 status; unsigned long flags; status ioread32(chan-regs AXI_DMA_STATUS_REG); /* 清除中断位不写的话 DMA 核不会触发下一次中断 */ iowrite32(status AXI_DMA_IRQ_ALL_MASK, chan-regs AXI_DMA_STATUS_REG); spin_lock_irqsave(chan-lock, flags); /* 把当前完成的描述符从硬件环摘下来挪入 done_list */ while (axidma_ring_desc_completed(chan)) { struct axidma_ring_desc *recv axidma_ring_get_completed(chan); list_move_tail(recv-node, chan-done_list); --chan-pending_cnt; } /* 还有排队中的传输就继续编程 DMA 硬件 */ if (!list_empty(chan-pending_list) chan-pending_cnt chan-ring_size) { struct axidma_desc *next list_first_entry(chan-pending_list, struct axidma_desc, node); axidma_hw_start_transfer(chan, next); } spin_unlock_irqrestore(chan-lock, flags); /* 唤醒等待在 poll/waitqueue 上的用户态进程 */ wake_up_interruptible(chan-wait_queue); return IRQ_HANDLED; }这段代码体现了 DMA 驱动的两个血泪要点。第一中断处理里操作描述符链表必须持有自旋锁因为ioctl提交传输的上下文也在操作同一个链表不锁就是竞争条件。第二ioread32和iowrite32是重传寄存器状态的关键动作我在一个板子上碰到过中断只触发一次的现象最后发现就是中断处理里忘了清除 IRQ 位后续中断全被硬件屏蔽了。另一个细节是pending_cnt chan-ring_size的判断。这意味着每次中断只补充一个描述符而不是一次性把整个pending_list灌进硬件环。这种做法的好处是简单可靠坏处是中断频率高时描述符补给跟不上吞吐量。我做高速数据采集时会改成批量补充一次中断塞满整个环吞吐率能涨接近一倍。3.3 用户态访问ioctl 描述符与 mmap 共享缓冲区用户态要走自定义 ioctl。把 DMA 的目的地址、源地址、传输长度这些参数从用户态拷贝到内核构造axidma_desc挂到pending_list再启动硬件传输就是 ioctl 的完整工作。static long axidma_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct axidma_device *adev file-private_data; struct axidma_transfer_req req; struct axidma_desc *desc; int ret; switch (cmd) { case AXIDMA_IOCTL_TRANSFER: if (copy_from_user(req, (void __user *)arg, sizeof(req))) return -EFAULT; /* req.dir 决定走 MM2S 还是 S2MM 通道 */ desc axidma_desc_alloc(req.dir, req.buf_size, req.user_addr); if (IS_ERR(desc)) return PTR_ERR(desc); ret axidma_desc_submit(adev, desc); if (ret) return ret; /* 环形缓冲模式下用户可以选择等待完成 */ if (req.wait_done) { wait_event_interruptible_timeout(adev-wait_queue, axidma_desc_is_done(desc), msecs_to_jiffies(1000)); if (!axidma_desc_is_done(desc)) return -ETIMEDOUT; } break; default: return -ENOIOCTLCMD; } return 0; }copy_from_user是用户态数据进入内核的入口驱动的复杂度都压在axidma_desc_alloc和axidma_desc_submit这两个函数上。axidma_desc_alloc需要把用户态的虚拟地址转换成物理地址常见做法是pin_user_pages然后sg_table映射这套操作在只做copy_from_user时是不需要的。wait_event_interruptible_timeout提供一个超时保护避免用户态傻等一个永远不来的中断。我在首次调试时吃过这个亏设备树中断号配错驱动一直卡等加上这个超时机制后至少能从日志里看到“传输超时”而不是整进程挂死。mmap 的实现相对独立通常是把预先分配的dma_alloc_coherent缓冲区映射给用户态省掉一次用户-内核拷贝测试程序里会用它来做大数据块连续传输。4. 编译移植与设备树让驱动在自己的板子上跑起来4.1 设备树节点告诉内核 DMA 控制器在哪里设备树是驱动能否跑起来的第一道关卡。DMA 控制器节点通常长这样axi_dma_0: dma40400000 { compatible xlnx,axi-dma-1.00.a; reg 0x40400000 0x10000; interrupts 0 29 4, 0 30 4; interrupt-names mm2s_introut, s2mm_introut; xlnx,addrwidth 32; xlnx,include-sg 1; xlnx,num-mm2s-channels 0x1; xlnx,num-s2mm-channels 0x1; } ;reg是 DMA 控制器的寄存器基地址和地址空间大小这个必须和 FPGA 里给 IP 分配的地址一致。interrupts里两个中断分别对应 MM2S 和 S2MM 通道的设备树中断号注意这里的中断号层次和中断类型的数值编法要按你的平台来。我在某 ARM 核的 FPGA SoC 上调试时两个中断写反了表现出来就是 MM2S 传输永远在等超时。设备树里xlnx,include-sg是否设为 1决定了 DMA 核运行在 Scatter-Gather 模式还是简单模式。这套驱动一般按 SG 模式写因为简单模式一个描述符只能搬一段连续内存SG 模式才能支撑链式描述符和分散-聚集传输。如果你在 FPGA 端配置的 IP 核没有开 SG那么设备树这个属性也别写 1否则驱动初始化时校验寄存器能力部会失败。4.2 内核配置与模块编译常见的问题是按错了工具链驱动本身的编译方法不复杂就是标准的 out-of-tree 模块export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export KDIR/path/to/kernel/build/dir make -C $KDIR M$PWD modules这里KDIR必须指向目标板上同一个内核版本的编译目录且内核要开启了CONFIG_DMA_ENGINE和CONFIG_OF。如果KDIR指向的是纯净源码树而不是构建目录模块也能编但加载时可能因为 vermagic 不匹配报错。另外ARCH和CROSS_COMPILE缺一不可不然模块会编到本机架构上去。还要注意include/linux/dmaengine.h的接口版本。内核从 4.x 到 6.xdma_device的结构体字段和一些回调名是有改动的。这套驱动源码在较新的内核上编译报错常见原因就是回调指针的名字不一样解决方式是查内核源码里dma_device定义把驱动里对应的字段逐个对齐。4.3 加载与测试先跑一次回环传输模块编译通过之后加载顺序也有讲究。首先确认 DMA 控制器对应的amba_pl或platform总线驱动已经准备好然后insmod axidma.ko。查看dmesg里是否出现类似axidma dma40400000: registered MM2S channel和axidma dma40400000: registered S2MM channel的日志。如果只有一条通道注册成功检查中断号或者interrupt-names的命名是否和设备树一致。接着跑一次回环测试——把 MM2S 的输出端在 FPGA 内部连接到 S2MM 输入端然后用户态程序先通过 ioctl 发起一个 MM2S 传输再发起一个 S2MM 传输最后对比数据内容/* 伪代码回环测试核心路径 */ int buf_fd open(/dev/axidma_buf, O_RDWR); void *buf mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, buf_fd, 0); /* 写 0xAA 模式数据到 buf发 MM2S 传输 */ memset(buf, 0xAA, BUF_SIZE); ioctl(buf_fd, AXIDMA_IOCTL_TRANSFER, (struct transfer_req){ .dir MM2S, .buf_size BUF_SIZE, .wait_done 1 }); /* 发 S2MM 传输收回到同一块缓存 */ memset(buf, 0x00, BUF_SIZE); ioctl(buf_fd, AXIDMA_IOCTL_TRANSFER, (struct transfer_req){ .dir S2MM, .buf_size BUF_SIZE, .wait_done 1 }); /* 校验数据失败则说明传输路径上有问题 */ for (int i 0; i BUF_SIZE; i) assert(buf[i] 0xAA);回环测试能通过基本可以确定寄存器地址、中断、描述符环这三项都是对的。如果数据校验不过先检查dma_alloc_coherent分配的缓冲区地址在 FPGA 侧是否可见——一些带 IOMMU 的平台上DMA 地址和物理地址不是同一个驱动里要做iommu-dma映射处理否则 FPGA 搬的数据落在错误的物理页上。5. 踩坑记录与调试手段DMA 驱动的血泪经验5.1 现象传输完成中断来了但数据全零这是我做过的一个带 S2MM 的图像采集案例里遇到的。DMA 中断定时触发读回的数据却始终是 0像是外设根本没把数据放进内存。原因设备树的 DMA 节点地址和 FPGA 里的实际地址偏了一位——FPGA 侧分配的地址是0x40401000设备树里写的却是0x40400000驱动ioremap到了对的地址读取寄存器也像模像样但描述符尾指针更新的寄存器偏移不对DMA 核收到的描述符环基地址是错的实际上硬件写到了另一块内存。解决在设备树里重新核对reg做一次对齐同时在驱动初始化时打印desc_ring的物理地址和chan-regs的物理地址手动对照 FPGA 工程里的地址表。从那以后我改任何设备树地址都会先在启动日志里把解析出来的资源打印出来再继续调试协议。5.2 现象中断只触发一次后续所有传输全部挂住第二个高频问题驱动加载后第一次传输成功第二次开始用户态永久阻塞在wait_done。原因中断处理函数没有清中断状态寄存器中的传输完成位。DMA 核通常设计成“写 1 清 0”的寄存器语义驱动只读状态没回写硬件中断线上一直保持有效进了第一次中断后内核不会再派人进来触发后续中断。解决在中断处理入口处把整个状态寄存器的IRQ_ALL_MASK位写回去模拟里直接iowrite32(status mask, regs STATUS). 这里有个常见误用想只清一个通道的中断位结果把另一个通道的状态也清了可能导致另一条通道传输中断丢失。我一般会定义宏区分MM2S_IRQ_MASK和S2MM_IRQ_MASK按通道分别写。5.3 现象mmap 的用户态缓冲区数据不一致缓存一致性是 DMA 驱动绕不开的坑。现象是用户态通过 mmap 拿到缓冲区DMA 写完后主 CPU 读到的数据不是最新的必须加__arm_dma_cache或重新打开文件才看到对的数据。原因dma_alloc_coherent是内核里常见的无缓存分配方式但有些驱动实现 mmap 时采用的是remap_pfn_range映射的普通内存CPU 缓存没有失效DMA 外设写内存的数据还留在外设侧。解决mmap 的内核侧实现优先用dma_mmap_coherent——它专门处理了 coherent 缓冲区的映射。如果驱动里用的是remap_pfn_range就得手动在每次 DMA 完成后执行dma_sync_single_for_cpu或者干脆放弃在普通内核内存上做 DMA。我建议直接统一切换到dma_alloc_coherentdma_mmap_coherent省去后续一堆不可复现的问题。5.4 现象insmod 报参数错误或资源被占用驱动加载失败常见的有两类报错一个是IRQ_NONE相关的中断请求失败另一个是ioremap失败。前者大概率是设备树里中断号超出该系统的中断控制器范围后者通常是寄存器地址与其他外设重叠或者地址空间配置得不合理。有些平台的devm_ioremap_resource会检查资源预留标记同一个地址在另一份设备树覆盖里被用了就直接报 busy。解决用cat /proc/iomem查看系统已占用的内存映射区域确认 DMA 控制器的物理地址没有被其它节点占用。中断号的排查则需要看中断控制器的级联关系和interrupt-cells的属性取值。5.5 现象大数据量连续传输时吞吐突然掉到零超过某个阈值后DMA 彻底停摆但小数据块正常。这是描述符环深度和中断频率之间没匹配好的表现。原因描述符环只有固定数量驱动在中断处理里逐个补充描述符如果每毫秒中断上百次软件侧来不及将pending_list里的节点编程进硬件环环耗尽后 DMA 核即返回 idle。解决在中断处理里改成批量回填描述符每次中断从pending_list中掏出尽可能多的描述符一次性填入环同时将环深度提升到 32 或 64。断层式的吞吐骤降是一个信号优先查环深度而不是带宽。6. 二次开发方向多通道扩展与性能收尾这套驱动的常规形态是暴露一个字符设备内部固定两条通道。做二次开发时最常见的需求是如何扩展成多 DMA 通道或接入内核子系统。多通道扩展的工作量主要在前面维护的那个struct axidma_channel和中断服务上。可以让一套驱动实例解析设备树里的dmas属性为每个通道创建独立的字符设备次设备号。常见做法是用miscdevice链表每个通道一个minor然后用container_of从struct file里反查设备实例。注意每个通道必须有独立的自旋锁和独立的中断回调如果共用一把锁一个通道的中断可能阻塞另一个通道的提交路径。接入内核子系统方面如果想把数据直接引给 ALSA 或 V4L2字符设备这一层就不太适合了需要把 DMA 通道注册到系统dmaengine框架里让音频或视频驱动的dma_request_channel能申请到你的通道。架构上字符设备适合做带宽测试和裸数据吞吐验证而走dmaengine则是长期的融合方向。性能调优有几个直接可用的参数。第一是前面说的描述符环深度半双工连续传输时 32 的环深比 16 普遍能提升约两成吞吐。第二是中断合并部分 AXI DMA 核带IRQ_DELAY或IRQ_THRESHOLD寄存器适当延迟中断上报时间可以降低 CPU 中断频率但也会增加单包延迟。如果做实时性要求高的采集这个参数不要调太大。第三是 CPU 绑核把中断处理函数用irq_set_affinity_hint绑定到专用 CPU 核上避免中断在多个核间反复迁移性能会更稳定。我踩过最深的二次开发坑是把原先测试程序里固定的 4K 传输长度改成 64KB 后发现sg_table的映射粒度不够size超过一个 page 后要去处理multiple_pages的拆分。从那以后我每次改自定义长度路径时都会强制走一遍大数据量压力测试——连续跑一万次回环同时用perf top盯中断占比。这套做法帮我挡住了好几个隐蔽的内存边界问题希望也能帮到你。本文还有配套的精品资源点击获取