ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AXI DMA在Zynq7000上的Linux驱动调试:从设备树到DMA engine

AXI DMA在Zynq7000上的Linux驱动调试:从设备树到DMA engine 简介面向Zynq-7000嵌入式开发者的Xilinx AXI DMA驱动资源聚焦AXI DMA在PS/PL协同场景下的字符设备驱动实现与测试验证适合有Linux驱动基础、希望深入DMA高性能传输的工程师。压缩包共25个文件以C源码、头文件和Kbuild/Makefile构建脚本为主包含axidma_chrdev字符设备驱动、libaxidma用户态库以及benchmark、transfer、display_image等多个测试示例整体约81KB目录层级直观便于按模块阅读。目前已有1383人学习对于掌握设备注册、中断处理、IOCTL接口、DMA描述符管理等核心点是一份可运行、可实验的参考工程。通过代码可快速理解驱动与用户态程序的数据交互完成源目的数据完整性验证和吞吐性能评估并能扩展至图像显示、高速采集等实际场景。除字符设备驱动外资源还涉及设备树匹配和DMA引擎配置有助于打通完整数据链路。源码结构清晰既能直接编译运行也适合对照驱动框架逐行学习。1. AXI DMA在Zynq7000上为什么难调从axidma_chrdev到DMA engine在Vivado里拉一个AXI DMA IP、把MM2S和S2MM接好、Generate Output Products前后不到十分钟真正卡人的是Linux侧。dmesg里看不到dma节点、/dev/axidma不出现、回环传输数据永远对不上这三个问题能消耗掉一整个下午。这份xilinx_axidma-master源码的价值在于它把「AXI DMA控制器驱动」和「用户态测试工具」一次给全了。内核侧有axidma_of.c做设备树匹配、axidma_dma.c实现DMA engine回调、axidma_chrdev.c暴露字符设备用户侧libaxidma库封装ioctlexamples目录里带axidma_transfer和axidma_benchmark可以直接编译跑数据完整性校验。适合刚接手Zynq7000 PS-PL高速数据通路、又不想从零啃Xilinx官方文档的Linux驱动开发者对只看过XDMA例程、没碰过AXI DMA的人这份代码也是理清dmaengine框架的捷径。2. 设备树匹配与通道绑定axidma_of.c如何接住Zynq7000的PL侧DMA2.1 先弄清楚这份驱动里三个文件各管什么Xilinx的AXI DMA IP在Zynq7000里挂在AXI_HP口或AXI_GP口上PS侧能通过S_AXI_LITE寄存器接口配置它但Linux内核不会因为你在Block Design里拖了个IP就自动认识它。设备树里必须有一个节点描述DMA控制器的地址、中断、兼容属性然后由驱动里的of_match_table把它认领走。这份源码里驱动被拆成三个文件而不是像很多教程那样把所有逻辑塞进一个axidma.c文件职责对应Linux子系统axidma_of.c设备树匹配、probe入口、通道分配platform_driver / of_match_tableaxidma_dma.cdma_device回调、描述符管理、中断完成处理DMA engine frameworkaxidma_chrdev.c字符设备注册、open/ioctl、用户态请求中转miscdevice / file_operations拆开的直接好处是换FPGA工程时只需要改设备树和of_match里的compatibleDMA搬运和字符设备逻辑完全不用动。很多做Zynq7000开发的人习惯把probe、中断、ioctl写在一个文件里结果换一颗芯片就要重读一千行代码这个结构值得直接借鉴。2.2 设备树节点与of_match_table的匹配过程AXI DMA在设备树里通常长这样axi_dma_0: dma40400000 { compatible xlnx,axi-dma-7.1, xlnx,axi-dma; reg 0x40400000 0x10000; dma-channels 1; interrupts 0 29 4, 0 30 4; interrupt-parent intc; };reg里的0x40400000是Vivado里给AXI DMA的S_AXI_LITE基地址0x10000是地址空间大小interrupts两个条目分别对应MM2S和S2MM的中断第三个数字4表示高电平触发这是AXI DMA默认的中断类型。axidma_of.c里会定义一个of_device_id数组static const struct of_device_id axidma_of_match[] { { .compatible xlnx,axi-dma-7.1, .data axidma_of_data }, { .compatible xlnx,axi-dma, .data axidma_of_data }, { /* end of table */ } }; MODULE_DEVICE_TABLE(of, axidma_of_match);compatible字符串必须和设备树节点完全一致内核才会上报probe。实际排错时最常见的错误就是Vivado工程的IP版本是7.1设备树里只写了xlnx,axi-dma或者反过来导致dmesg里出现no matching platform driver但平台设备明明存在。2.3 probe里通道是怎么拿到的probe函数的核心动作不再是直接ioremap寄存器然后操作而是走dmaengine的框架驱动本身也是一个DMA控制器驱动它向内核注册一个dma_device同时把硬件通道抽象出来。axidma_of.c在probe里一般会做这几件事分配axidma_dev结构体、解析设备树里的通道数量、为每个通道初始化AXI DMA的SG描述符和环形buffer、调用dma_async_device_register注册到内核。static int axidma_of_probe(struct platform_device *pdev) { struct axidma_device *adev; struct resource *res; adev devm_kzalloc(pdev-dev, sizeof(*adev), GFP_KERNEL); adev-dev pdev-dev; platform_set_drvdata(pdev, adev); res platform_get_resource(pdev, IORESOURCE_MEM, 0); adev-base devm_ioremap_resource(pdev-dev, res); adev-tx_chan dma_request_chan(pdev-dev, tx); adev-rx_chan dma_request_chan(pdev-dev, rx); /* 初始化 dma_device 的 capability 与回调 */ dma_cap_set(DMA_SLAVE, adev-dma_dev.cap_mask); adev-dma_dev.device_prep_slave_single axidma_prep_slave_single; adev-dma_dev.device_issue_pending axidma_issue_pending; adev-dma_dev.device_terminate_all axidma_terminate_all; return dma_async_device_register(adev-dma_dev); }dma_request_chan的第二个参数是通道名来自设备树里的dma-names属性不是随便写的。如果设备树里没有对应名字probe会返回-ENODEV字符设备也不会创建这是另一个隐蔽的坑。3. 字符设备与中断回调axidma_chrdev.c里的ioctl命令链路3.1 从file_operations到DMA搬运的完整路径用户态操作AXI DMA不是直接往寄存器写而是打开/dev/axidma然后用ioctl把一段用户缓冲区交给内核让内核完成映射、描述符填充和中断等待。axidma_chrdev.c提供的file_operations一般包括open、release和unlocked_ioctl其中ioctl是核心入口static const struct file_operations axidma_fops { .owner THIS_MODULE, .open axidma_chrdev_open, .release axidma_chrdev_release, .unlocked_ioctl axidma_chrdev_ioctl, }; static long axidma_chrdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct axidma_transfer transfer; switch (cmd) { case AXIDMA_IOCTL_TRANSFER: if (copy_from_user(transfer, (void __user *)arg, sizeof(transfer))) return -EFAULT; return axidma_do_dma_transfer(filp, transfer); default: return -ENOTTY; } }copy_from_user这一步不能省直接访问用户指针会导致内核崩溃。AXIDMA_IOCTL_TRANSFER在axidma_ioctl.h里用_IOWR定义表示同时携带读写方向的参数。真正发起DMA的是axidma_do_dma_transfer它把axidma_transfer里的虚拟地址做dma_map_single得到物理地址再通过前面注册的device_prep_slave_single回调构造描述符。3.2 中断完成信号是怎么传回用户态的DMA数据搬完硬件产生中断但这份驱动里中断服务程序并不是标准的request_irq写法而是依赖dmaengine的callback机制。axidma_dma.c里描述符完成时会触发注册好的回调static void axidma_dma_callback(void *param) { struct axidma_dma_chan *achan param; struct axidma_pending *pending achan-pending; /* 记录传输状态并唤醒等待的 ioctl 进程 */ pending-status AXIDMA_STATUS_COMPLETE; complete(achan-completion); }这里的completion是struct completionioctl发起传输后调用wait_for_completion_interruptible_timeout等待超时则返回-ETIMEDOUT。中断号和设备树里interrupts属性的对应关系由platform_get_irq解析AXI DMA的MM2S和S2MM是两个独立中断probe时分别注册。排错时用cat /proc/interrupts看这两个中断有没有触发比读驱动日志更快。3.3 传输描述符里的关键参数axidma_transfer结构体是用户态与内核态之间的协议字段顺序和大小在libaxidma.h和axidma_ioctl.h里必须保持一致字段含义常见取值direction传输方向DMA_MEM_TO_DEV / DMA_DEV_TO_MEMvaddr用户态缓冲区虚拟地址malloc返回值len传输字节数4K对齐最佳如4096frame_size帧大小用于流式传输与len相等或按帧拆分vaddr不能是栈上地址因为dma_map_single要求缓冲区能够被内核映射栈变量跨系统调用后生命周期不确定。常见做法是malloc或者用posix_memalign做cacheline对齐缓冲区长度比实际数据多留几个字节避免越界访问。4. 用户态libaxidma把axidma_transfer组装成可跑的回环验证4.1 libaxidma.c封装了什么如果每个用户程序都自己写open、ioctl、close代码会非常啰嗦。libaxidma.c把这些操作收进一个库对外只暴露axidma_open、axidma_transfer和axidma_close。它的内部核心逻辑是维护一个axidma_dev句柄句柄里记录了设备文件描述符、通道方向、通道数量。axidma_open会先打开/dev/axidma然后通过一个内部ioctl读取可用DMA通道信息这样上层应用就不用关心设备节点的具体路径。struct axidma_dev *axidma_open(void) { struct axidma_dev *dev; dev malloc(sizeof(*dev)); dev-fd open(/dev/axidma, O_RDWR); if (dev-fd 0) { free(dev); return NULL; } /* 读取该设备支持的 DMA 通道数量与方向 */ if (ioctl(dev-fd, AXIDMA_IOCTL_GET_CHANNELS, dev-chan_info) 0) { close(dev-fd); free(dev); return NULL; } return dev; }AXIDMA_IOCTL_GET_CHANNELS这条命令在字符设备里返回的是一个通道描述数组每个元素标记该通道是发送还是接收。这样在examples里就可以遍历通道把第一个TX通道用于发送、第一个RX通道用于接收完全是数据驱动的写法。4.2 axidma_transfer的调用与参数选择传入用户缓冲区后axidma_transfer内部会构造struct axidma_transfer并做ioctl。封装后的调用方式非常直白struct axidma_dev *dev axidma_open(); struct axidma_channel *tx_ch axidma_get_channel(dev, AXIDMA_TX); char *buf malloc(4096); memset(buf, 0x5A, 4096); struct axidma_transfer transfer; memset(transfer, 0, sizeof(transfer)); transfer.direction DMA_MEM_TO_DEV; transfer.vaddr (unsigned long)buf; transfer.len 4096; transfer.frame_size 1024; if (axidma_do_transfer(dev, tx_ch, transfer) ! 0) { fprintf(stderr, transfer failed\n); }frame_size在这里等于1024表示把4096字节拆成4个帧下发给DMA帧大小必须能被len整除否则驱动会返回-EINVAL。这个参数在实际采集场景里对应AXI DMA的帧同步信号如果PL侧对接的是AD9361这类带帧标志的接口frame_size要配置成与采样周期匹配的值。4.3 examples里两个程序的定位差异axidma_transfer.c是单次传输验证工具适合确认通路是否通axidma_benchmark.c则循环发起大量传输统计吞吐量和耗时。跑benchmark时常见的命令组合是./axidma_benchmark -t -r -d /dev/axidma -s 16384 -n 1000-t指定使用发送通道-r指定使用接收通道-s是单次传输字节数-n是循环次数。如果PL侧没有真实的DMA外设可以把AXI DMA的MM2S和S2MM在Vivado里直接对接做成环回这样benchmark一发一收验证的是整条通路而不只是单个方向。-s选择16384而不是4096是因为DMA引擎在大块连续传输时描述符开销占比更小吞吐量数字更能反映硬件极限。5. 从axidma_benchmark确认DMA真的在跑中断计数、吞吐量与坑5.1 传输前先确认中断在涨跑benchmark之前先开一个终端持续观察/proc/interrupts。AXI DMA的中断在设备树里注册后会显示为axidma或芯片名加通道名数值每完成一次传输就增加。如果中断完全不涨问题基本不在用户态而是驱动probe阶段中断号解析错误或者设备树interrupt-parent指向了错误的GIC实例。这时候先用dmesg | grep axidma检查有没有probe failed再用cat /sys/bus/platform/devices/40400000.dma/irq确认实际分配的中断号两分钟就能定位。5.2 吞吐量数据怎么看在Zynq7000上AXI DMA走HP口访问DDR的理论带宽大约在600MB/s级别。跑完1000次16384字节传输如果算下来的吞吐量只有几十MB/s先看时钟AXI DMA的S_AXI_ACLK在Vivado里如果只给了50MHz带宽上限就被锁死了。其次看地址映射DMA描述符所在的内存如果被配置成非cacheable每次描述符读取都要穿透到DDR性能会掉一半以上。把这几个因素列成排查顺序现象优先检查项处理方式中断不触发设备树irq配置核对reg与Vivado地址吞吐量偏低S_AXI_ACLK频率提到100MHz以上数据校验失败缓冲区未对齐改用posix_memalignioctl返回ETIMEDOUT外设未准备好确认PL侧AXI Stream握手5.3 和XDMA调试思路的对比经常有人问“怎么知道XDMA有没有工作起来”在AXI DMA上同样适用这套验证逻辑中断计数必须增长完成回调必须被调用用户态ioctl必须返回成功。XDMA多了一步PCIe link up的判断而AXI DMA多了一步PL侧时钟和复位的确认。只要把/proc/interrupts、dmesg、benchmark三个输出对齐任何一个环节断了都能立刻看出是哪一层的问题。实际收尾时我习惯用这样一个命令组合做最终确认while true; do grep axidma /proc/interrupts; sleep 1; done在另一个终端跑第二轮benchmark如果中断计数以和传输次数一致的速率增长同时dmesg里没有任何超时或错误记录这套AXI DMA驱动就可以从调试状态转入业务代码开发了。本文还有配套的精品资源点击获取
返回列表