Linux DMA Engine 框架 slave 模式编程实战:从 dma_slave_config 到回调的异步传输全流程

Linux DMA Engine 框架 slave 模式编程实战:从 dma_slave_config 到回调的异步传输全流程 Linux DMA Engine 框架 slave 模式编程实战从 dma_slave_config 到回调的异步传输全流程一、当CPU被memcpy拖垮DMA异步传输的工程价值在嵌入式Linux系统中外设数据搬运是最常见的性能瓶颈之一。以SPI NOR Flash的页编程为例一页256字节的数据从内核缓冲区传输到SPI控制器FIFO的过程若使用CPU逐字节搬移Cortex-A7 800MHz上约消耗40μs——看似微不足道但考虑页编程时间约300μs总时长CPU利用率达到13%。当系统同时驱动多个SPI外设、I2S音频流和UART调试输出时CPU的memcpy开销会迅速累积挤压应用层和实时任务的执行时间。DMADirect Memory Access引擎的价值在于将数据搬运从CPU卸载到专用硬件。Linux内核的DMA Engine框架提供了统一的API层支持DMA Controller的slave模式编程将外设的DMA请求线与内存缓冲区关联实现全硬件自动传输。本文以STM32MP1平台的DMA2控制器为参考剖析slave模式从配置到回调的完整异步传输流程。二、DMA Engine 框架的slave模式驱动模型Linux内核的DMA Engine框架将DMA控制器抽象为dma_device将每次传输抽象为dma_async_tx_descriptor。在slave模式下外设驱动不直接操作DMA控制器寄存器而是通过框架API描述传输需求。sequenceDiagram participant DRV as 外设驱动 participant ENG as DMA Engine核心 participant CTL as DMA控制器驱动 participant HW as DMA硬件 DRV-ENG: dma_request_slave_channel() ENG-CTL: device_alloc_chan_resources() CTL--ENG: 返回可用通道 ENG--DRV: dma_chan 句柄 DRV-ENG: dmaengine_slave_config()br配置 src/dst 地址/宽度/突发大小 ENG-CTL: device_config() CTL-HW: 写入通道配置寄存器 DRV-ENG: dmaengine_prep_slave_single()br准备单次传输描述符 ENG-CTL: device_prep_slave_sg() CTL--ENG: dma_async_tx_descriptor DRV-ENG: 设置回调 dma_desc-callback DRV-ENG: dmaengine_submit() dma_async_issue_pending() ENG-CTL: issue_pending() CTL-HW: 使能通道启动硬件传输 HW--CTL: 传输完成中断 CTL-ENG: 调用 tasklet 调度 ENG-DRV: 执行 dma_desc-callback()关键阶段拆解通道申请dma_request_slave_channel()根据设备树中的dmas和dma-names属性查找匹配的DMA控制器和请求线。匹配规则依赖dma_request_slave_channel_compat()对struct dma_slave_map的遍历。配置阶段dma_slave_config结构体定义了外设端的地址、地址增量行为、数据宽度和最大突发大小。这些参数由外设的数据手册决定——SPI控制器通常需要固定FIFO地址、8位宽度、单次传输音频I2S则需要16/32位宽度和DMAFIFO的半满/全满水位触发。传输准备与提交device_prep_slave_sg()根据scatter-gather列表准备描述符链表。DMA硬件按链表顺序自动处理每个描述符块无需CPU干预。回调与完成通知传输完成时DMA控制器驱动通过tasklet机制调用描述符的callback函数。回调在中断下半部上下文执行tasklet软中断可能睡眠但不可做重度计算。三、生产级slave模式编程实现以下代码是一个完整的外设DMA传输封装包含错误恢复和超时保护。适用于SPI/I2S/UART等外设的DMA传输控制#include linux/dmaengine.h #include linux/dma-mapping.h #include linux/completion.h #include linux/err.h #include linux/jiffies.h /* DMA传输控制块管理一次完整的异步传输生命周期 */ struct dma_xfer_ctrl { struct dma_chan *chan; /* DMA通道句柄 */ struct dma_slave_config cfg; /* slave配置缓存 */ dma_addr_t dma_handle; /* DMA总线地址 */ struct completion done; /* 传输完成信号量 */ dma_cookie_t cookie; /* 传输cookie用于状态查询 */ int error; /* 错误码0成功0失败 */ }; /* * DMA传输完成回调 * 在tasklet软中断上下文执行——不可睡眠、不可执行重度计算 */ static void dma_xfer_callback(void *param) { struct dma_xfer_ctrl *ctrl (struct dma_xfer_ctrl *)param; /* 检查传输状态区分正常完成与传输错误 */ enum dma_status status dma_async_is_tx_complete( ctrl-chan, ctrl-cookie, NULL, NULL); if (status DMA_COMPLETE) { ctrl-error 0; /* 正常完成 */ } else if (status DMA_ERROR) { ctrl-error -EIO; /* 硬件传输错误 */ } else { ctrl-error -ETIMEDOUT; /* 状态未知视为异常 */ } complete(ctrl-done); /* 唤醒等待线程 */ } /* * 配置DMA slave通道 * 必须在外设时钟和电源域就绪后调用 * 返回0成功负值表示配置失败 */ static int dma_config_slave_channel(struct dma_xfer_ctrl *ctrl, dma_addr_t periph_addr, enum dma_transfer_direction dir, u32 max_burst) { if (!ctrl || !ctrl-chan) { pr_err(DMA配置失败: 通道句柄为空\n); return -EINVAL; } int ret; /* 释放旧资源如果通道曾被其他外设使用 */ if (ctrl-chan-device-device_free_chan_resources) { ctrl-chan-device-device_free_chan_resources(ctrl-chan); } /* 重新分配通道资源 */ ret ctrl-chan-device-device_alloc_chan_resources(ctrl-chan); if (ret 0) { pr_err(DMA通道资源分配失败: %d\n, ret); return ret; } /* 填充slave配置 —— 参数来源于外设数据手册 */ memset(ctrl-cfg, 0, sizeof(ctrl-cfg)); if (dir DMA_MEM_TO_DEV) { /* 内存 → 外设src地址递增dst地址固定 */ ctrl-cfg.direction DMA_MEM_TO_DEV; ctrl-cfg.dst_addr periph_addr; /* 外设FIFO地址固定 */ ctrl-cfg.dst_addr_width DMA_SLAVE_BUSWIDTH_1_BYTE; ctrl-cfg.dst_maxburst max_burst; ctrl-cfg.src_addr_width DMA_SLAVE_BUSWIDTH_4_BYTES; } else if (dir DMA_DEV_TO_MEM) { /* 外设 → 内存src地址固定dst地址递增 */ ctrl-cfg.direction DMA_DEV_TO_MEM; ctrl-cfg.src_addr periph_addr; /* 外设FIFO地址固定 */ ctrl-cfg.src_addr_width DMA_SLAVE_BUSWIDTH_1_BYTE; ctrl-cfg.src_maxburst max_burst; ctrl-cfg.dst_addr_width DMA_SLAVE_BUSWIDTH_4_BYTES; } else { pr_err(不支持的传输方向: %d\n, dir); return -EINVAL; } /* * 应用配置到DMA控制器硬件寄存器 * 注意dmaengine_slave_config 可能返回 -EBUSY 如果通道正忙 */ ret dmaengine_slave_config(ctrl-chan, ctrl-cfg); if (ret 0) { pr_err(DMA slave配置写入失败: %d\n, ret); return ret; } return 0; } /* * 执行单次DMA异步传输 * * param ctrl: 传输控制块 * param buf: 内核虚拟地址缓冲区必须是DMA一致性的 * param len: 传输字节数 * param dir: 传输方向 * param timeout_ms: 超时时间毫秒0表示不超时 * return: 实际传输的字节数负值表示失败 */ static ssize_t dma_async_transfer(struct dma_xfer_ctrl *ctrl, void *buf, size_t len, enum dma_transfer_direction dir, unsigned int timeout_ms) { struct dma_async_tx_descriptor *desc; int ret; if (!ctrl || !ctrl-chan || !buf) { return -EINVAL; } if (len 0) { return 0; } /* * 将内核虚拟地址映射为DMA总线地址 * 对于DMA一致性内存dma_map_single 返回物理地址 */ ctrl-dma_handle dma_map_single(ctrl-chan-device-dev, buf, len, dir); ret dma_mapping_error(ctrl-chan-device-dev, ctrl-dma_handle); if (ret) { pr_err(DMA映射失败: 缓冲区地址 %p, 长度 %zu\n, buf, len); return -ENOMEM; } /* * 准备传输描述符 * 使用 prep_slave_sg 或 prep_dma_memcpy 取决于方向 */ if (dir DMA_MEM_TO_DEV || dir DMA_DEV_TO_MEM) { desc dmaengine_prep_slave_single(ctrl-chan, ctrl-dma_handle, len, dir, DMA_PREP_INTERRUPT | DMA_CTRL_ACK); } else { pr_err(不支持的DMA方向: %d\n, dir); dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return -EINVAL; } if (!desc) { pr_err(DMA描述符准备失败: 通道可能正忙或配置不兼容\n); dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return -EIO; } /* 绑定完成回调 */ reinit_completion(ctrl-done); ctrl-error -EINPROGRESS; /* 标记传输进行中 */ desc-callback dma_xfer_callback; desc-callback_param ctrl; /* 提交到DMA引擎队列并启动 */ ctrl-cookie dmaengine_submit(desc); ret dma_submit_error(ctrl-cookie); if (ret) { pr_err(DMA提交失败: cookie错误 %d\n, ret); dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return ret; } dma_async_issue_pending(ctrl-chan); /* * 等待传输完成或超时 * 使用 completion 机制 —— 不消耗CPU周期 */ unsigned long timeout timeout_ms ? msecs_to_jiffies(timeout_ms) : MAX_SCHEDULE_TIMEOUT; ret wait_for_completion_timeout(ctrl-done, timeout); /* DMA取消如果超时必须终止通道传输 */ if (ret 0) { pr_err(DMA传输超时: cookie %d, 超时阈值 %ums\n, ctrl-cookie, timeout_ms); dmaengine_terminate_sync(ctrl-chan); /* 同步终止等待硬件确认 */ dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return -ETIMEDOUT; } /* 检查回调中设置的错误码 */ if (ctrl-error 0) { pr_err(DMA传输错误: 错误码 %d\n, ctrl-error); dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return ctrl-error; } /* 解除DMA映射 */ dma_unmap_single(ctrl-chan-device-dev, ctrl-dma_handle, len, dir); return len; } /* * 释放DMA通道资源 * 在驱动remove或suspend时调用 */ static void dma_release_channel(struct dma_xfer_ctrl *ctrl) { if (!ctrl || !ctrl-chan) return; /* 确保通道上没有未完成的传输 */ if (ctrl-chan-device-device_terminate_all) { ctrl-chan-device-device_terminate_all(ctrl-chan); } dma_release_channel(ctrl-chan); ctrl-chan NULL; }设备树配置示例/* STM32MP1 SPI DMA绑定 */ spi4 { dmas dmamux1 61 0x400 0x01, /* TX请求线 */ dmamux1 62 0x400 0x01; /* RX请求线 */ dma-names tx, rx; };四、DMA引擎的工程陷阱与设计约束DMA一致性内存的必要性内核缓冲区必须通过dma_alloc_coherent()或kmalloc() dma_map_single()分配。在启用MMU和Cache的系统上直接使用栈变量或未映射缓冲区的物理地址会导致Cache一致性问题——DMA写入的数据被CPU的Cache行覆盖或反之。表现为偶尔丢数据的间歇性bug。Scatter-Gather链的物理连续性DMA描述符链表在物理内存中必须是连续的或通过物理地址指针链接。在内存碎片化严重的长时间运行系统中动态分配SG列表可能失败。应对策略是在驱动probe阶段预分配固定数量的描述符池。通道共享与优先级多个外设共享同一DMA控制器时通道优先级配置直接影响传输的实时性。以STM32MP1的DMA2为例通道0-7共享一个AHB从端口当SPI和SDMMC同时请求时低优先级通道的传输延迟可能从预期的5μs增加到50μs以上。解决方法是根据外设的实时性要求显式配置DMA控制器的优先级寄存器。与Cache预取和写合并的交互Cortex-A系列的Write-Back Cache和Store Buffer可能导致DMA看到的数据与CPU写入顺序不一致。在启动DMA传输前必须对源缓冲区执行dma_wmb()写内存屏障确保CPU写入的数据已刷新到DDR。接收方向完成后执行dma_rmb()读取屏障以确保读到的是DMA写入的最新数据。中断合并与延迟高性能DMA控制器如STM32的MDMA支持中断合并——连续多个描述符完成后才触发一次中断。这能显著降低中断开销但增加了完成通知的延迟抖动。音频场景需要低延迟每帧128 samples 48kHz 约2.7ms必须关闭中断合并而大块Flash编程场景则可以安全地合并8-16个描述符来降低CPU负载。五、总结Linux DMA Engine框架的slave模式通过dma_slave_config→device_prep_slave_sg→dmaengine_submit→回调的四步流程实现了外设数据搬运的全硬件自动化。关键是正确配置外设端地址、数据宽度和突发大小并在DMA一致性内存上操作。落地路径首先在设备树中正确描述DMA请求线的绑定关系其次在驱动中按本文封装模式实现传输控制块然后针对具体外设场景测试Cache一致性DMA写入后对比CPU读取的正确性和超时恢复强制拉低DMA请求线测试terminate_sync的响应最后考虑是否启用中断合并来优化CPU负载权衡延迟与吞吐。