1. 课程导入

在嵌入式与 Linux 驱动开发中,CPU 亲自搬运数据往往是最昂贵的操作。假设一次 ADC 采样需要把 4 KB 数据从外设 FIFO 搬到内存,如果由 CPU 逐字节读取,不仅占用大量指令周期,还会打断实时任务的节奏。DMA(Direct Memory Access,直接内存访问)正是为解决这一矛盾而生:它让一个独立的硬件控制器在外设与内存之间直接搬运数据,CPU 只负责“下达任务”和“处理完成通知”。

但 DMA 并不是“配置一个寄存器就完事”的简单外设。真实系统里往往同时存在多个 DMA 控制器、多条通道、多种传输方向,还要处理链式传输、循环缓冲、完成回调、错误恢复。于是 Linux 内核抽象出了 dmaengine 框架,把不同厂商、不同架构的 DMA 控制器统一成一套可移植的编程接口。本课程的核心,就是围绕 dmaengine 的接口使用与描述符链管理展开,帮助你从“会调用 API”进阶到“能设计可靠的数据流”。

一、DMA 传输的基本概念

DMA 传输的本质是“三方协作”:源地址、目的地址、传输长度,再加上触发方式与完成通知。按数据流向,常见类型包括:

  • 内存到外设(M2P):如把音频缓冲区数据送到 I2S FIFO。
  • 外设到内存(P2M):如 ADC、UART 接收数据写入内存。
  • 内存到内存(M2M):如大块数据拷贝、图像缩放前的搬运。

与 CPU 拷贝相比,DMA 的关键差异在于“并行性”:CPU 发起传输后即可去做别的事,DMA 控制器在后台完成搬运,并在结束时通过中断或轮询通知 CPU。这也带来了新的问题:数据一致性、缓存同步、传输粒度、通道竞争。理解这些约束,是后续使用 dmaengine 的前提。

核心认知:DMA 不是“更快的 memcpy”,而是一条由硬件维护的数据通路。你设计的是“通路如何被描述、被排队、被回收”,而不是简单的函数调用。

二、dmaengine 框架的定位

在没有 dmaengine 的时代,驱动开发者需要直接操作具体 DMA 控制器的寄存器,代码与硬件强耦合,换一颗芯片就要重写。dmaengine 框架位于具体 DMA 控制器驱动与使用 DMA 的外设驱动之间,提供统一的抽象层:

  • 对下:DMA 控制器驱动实现 struct dma_device 与通道操作,注册到框架。
  • 对上:外设驱动通过 dma_request_chan() 申请通道,用 dmaengine_prep_*() 准备传输,用 dmaengine_submit() 提交,用 dma_async_issue_pending() 启动。
  • 中间:框架负责描述符管理、回调分发、通道状态维护与资源回收。

这种分层让外设驱动只关心“我要传什么”,而不关心“底层是哪个控制器、用哪种描述符格式”。这也是本课程反复强调的“接口与实现分离”思想。

三、描述符链管理的核心价值

一次 DMA 传输可能只是一段连续内存,也可能是分散在多个不连续页上的数据块。描述符(descriptor)就是描述“一段传输”的数据结构,而描述符链把多个传输片段串成一条任务链,由 DMA 控制器依次执行。它的价值体现在三个方面:

  1. 支持分散/聚集(scatter-gather):把多个不连续缓冲区组织成一次逻辑传输,避免中间拷贝。
  2. 支持链式与循环模式:音频、视频等流式场景可预先挂好描述符环,实现持续传输。
  3. 支持异步完成与回调:每个描述符可绑定回调,实现分段处理与精细的错误定位。

下面这张图展示了 dmaengine 的整体数据流与描述符链的关系,建议先建立这个全局视图,再进入后续章节的接口细节。

dmaengine 框架:接口层、描述符链与数据通路 外设驱动(使用者) dma_request_chan() dmaengine_prep_slave_sg() dmaengine_submit() dmaengine 核心 描述符管理 / 通道调度 回调分发 / 状态维护 dma_cookie 跟踪 DMA 控制器驱动 struct dma_device device_prep_dma_*() issue_pending() 描述符链(Descriptor Chain) 描述符 1 src / dst / len callback 描述符 2 src / dst / len callback 描述符 3 src / dst / len callback NULL 链尾 源:外设 FIFO / 内存缓冲 ADC、UART、I2S、DDR 目的:内存 / 外设寄存器 DDR、SRAM、发送 FIFO 搬运 按链执行

四、典型场景:音频播放中的描述符链

以 I2S 音频播放为例,应用层把 PCM 数据分成多个 period,每个 period 对应一个描述符。驱动预先准备一条描述符链,DMA 控制器按顺序把每个 period 送入 I2S FIFO,每完成一个 period 触发一次回调,驱动再补充新的数据。这样 CPU 只需在回调中做少量工作,音频就能持续输出,不会因为一次大拷贝而卡顿。

/* 伪代码:音频播放中的描述符链准备 */
struct dma_async_tx_descriptor *desc;
struct scatterlist sg[PERIOD_NUM];

for (i = 0; i < PERIOD_NUM; i++) {
    sg_init_one(&sg[i], period_buf[i], period_size);
}

desc = dmaengine_prep_slave_sg(chan, sg, PERIOD_NUM,
                               DMA_MEM_TO_DEV,
                               DMA_PREP_INTERRUPT);
desc->callback = audio_dma_complete;
desc->callback_param = audio_dev;

dmaengine_submit(desc);
dma_async_issue_pending(chan);

这个例子中,dmaengine_prep_slave_sg() 把分散的 period 组织成一条链,DMA_PREP_INTERRUPT 表示需要回调,dma_async_issue_pending() 才真正启动硬件。理解“准备—提交—启动”三段式,是掌握 dmaengine 的关键。

五、常见误区与学习路径

误区一:把 dmaengine_submit() 当成“立即开始传输”。实际上它只是把描述符放入待处理队列,必须调用 dma_async_issue_pending() 才会触发硬件。

误区二:忽略缓存一致性。DMA 直接访问物理内存,若 CPU 缓存未同步,可能读到旧数据或覆盖新数据,需要 dma_map_sg() 等 API 配合。

误区三:在回调中执行耗时操作。回调运行在中断上下文,应尽快返回,把复杂处理交给工作队列。

本课程的学习路径建议如下表所示,先建立全局认知,再逐层深入接口与描述符管理,最后通过实战与排障巩固。

阶段 核心内容 关键接口 / 概念 目标
入门 DMA 基本概念、dmaengine 定位 通道、传输方向、cookie 看懂数据流与框架分层
接口 申请通道、准备传输、提交与启动 dma_request_chan、prep_slave_sg、submit 能写出可运行的 DMA 传输代码
描述符链 链式描述符、循环模式、回调管理 scatterlist、callback、cyclic 能设计流式与分段传输
实战 音频、串口、ADC 等场景 缓存同步、错误恢复、性能调优 能定位并解决真实问题