3. Linux DMA子系统概览

在Linux内核中,DMA(Direct Memory Access)并不是由各个外设驱动各自“直接操作寄存器”来完成,而是通过一套统一的dmaengine子系统进行抽象。这样做的目的,是让外设驱动只关心“我要搬什么数据、从哪搬到哪、搬完通知我”,而不必关心底层DMA控制器是哪个厂商、有多少通道、描述符如何排布。理解dmaengine框架,是后续调试DMA传输、中断协同与缓存一致性的基础。

一、dmaengine框架的分层结构

dmaengine框架自上而下大致分为四层:客户端驱动层、dmaengine核心层、DMA控制器驱动层和硬件DMA控制器。客户端驱动通常是SPI、I2C、UART、音频、存储等外设驱动,它们通过dma_request_chan()申请通道,通过dmaengine_prep_slave_sg()等接口提交传输请求。核心层负责通道管理、描述符队列、回调调度和生命周期管理。控制器驱动则实现struct dma_device和struct dma_chan,把核心层的请求翻译成具体寄存器操作。

Linux dmaengine 框架分层与传输路径 客户端驱动层(SPI / I2C / UART / 音频 / 存储) dma_request_chan() → dmaengine_prep_slave_sg() → dmaengine_submit() → dma_async_issue_pending() dmaengine 核心层 通道管理 · 描述符队列 · 回调调度 · cookie 跟踪 · 生命周期控制 DMA 控制器驱动层 struct dma_device · struct dma_chan · 描述符填充 · 寄存器编程 · 中断处理 硬件 DMA 控制器 源地址 · 目的地址 · 传输长度 · 通道寄存器 · 完成中断 关键对象 dma_chan:通道实例 dma_device:控制器能力 dma_desc:传输描述符 dma_cookie:传输标识 dma_slave_config:从机配置 callback:完成回调 数据流:内存 ↔ DMA 控制器 ↔ 外设 FIFO,CPU 仅参与配置与中断处理

二、DMA控制器驱动与通道申请释放

DMA控制器驱动在probe阶段会注册一个struct dma_device,并向核心层注册若干struct dma_chan。每个通道既可以用于内存到内存的memcpy,也可以用于内存与外设之间的slave传输。客户端驱动通过设备树中的dmas和dma-names属性,或者通过dma_request_chan()按名称申请通道。

struct dma_chan *chan;
chan = dma_request_chan(dev, "tx");
if (IS_ERR(chan)) {
    dev_err(dev, "request dma chan failed: %ld\n", PTR_ERR(chan));
    return PTR_ERR(chan);
}

/* 使用完成后必须释放 */
dma_release_channel(chan);

通道申请成功后,通常还需要调用dmaengine_slave_config()配置从机参数,例如外设FIFO地址、传输方向、地址宽度、突发长度等。这一步非常关键,因为很多DMA控制器只有在配置正确后才会真正启动传输。

提示:通道是有限资源。申请后忘记释放,会导致其他驱动无法申请通道,表现为-EBUSY或-ENODEV。在probe失败路径和remove路径中都要确保释放。

三、描述符与传输配置

dmaengine中的“描述符”是对一次或一组DMA传输的抽象。客户端驱动通过dmaengine_prep_slave_sg()、dmaengine_prep_dma_memcpy()、dmaengine_prep_dma_cyclic()等接口准备描述符,然后通过dmaengine_submit()提交到通道队列,最后调用dma_async_issue_pending()触发硬件执行。

接口 用途 典型场景
dmaengine_prep_slave_sg() 准备散列表从机传输 SPI/I2C 大块数据收发
dmaengine_prep_dma_memcpy() 内存到内存拷贝 缓冲区搬运、加速 memcpy
dmaengine_prep_dma_cyclic() 循环传输 音频播放/采集、ADC 连续采样
dmaengine_submit() 提交描述符到队列 所有传输类型
dma_async_issue_pending() 启动挂起的传输 提交后触发硬件
dmaengine_terminate_sync() 同步终止传输 错误恢复、模块卸载

描述符提交后会返回一个dma_cookie_t,客户端驱动可以用它来跟踪传输状态。完成回调callback通常在DMA中断上下文中被调用,因此回调函数中不能睡眠,只能做轻量级处理,例如唤醒等待队列或提交下一次传输。

四、典型场景与常见误区

以音频播放为例:音频驱动申请一个播放通道,配置从机地址为I2S TX FIFO,然后使用dmaengine_prep_dma_cyclic()准备循环描述符。DMA控制器按周期搬运PCM数据,每完成一个周期触发一次中断,驱动在回调中更新缓冲区指针。如果通道申请失败、描述符未正确提交或缓存未同步,就会出现“无声”“爆音”或“数据错位”等现象。

常见误区:
  • 只调用dmaengine_submit()却忘记dma_async_issue_pending(),导致传输永远不启动。
  • 在回调中执行msleep()或kmalloc(GFP_KERNEL),引发内核告警。
  • 释放通道前未终止正在进行的传输,导致DMA访问已释放内存。
  • 忽略dma_slave_config中的地址宽度和突发长度,造成数据错位。

复盘时,可以按“通道申请 → 从机配置 → 描述符准备 → 提交 → 触发 → 中断回调 → 释放”这条链路逐项检查。dmaengine框架的价值在于把硬件差异封装在控制器驱动中,但客户端驱动仍然必须正确管理通道生命周期、描述符状态和缓存一致性,否则DMA传输依然会失败。