第 3 章 DMA 传输类型:四种核心模式

3.1 从一根数据线说起

DMA 的传输类型,说白了就是数据从哪里来、到哪里去。我刚开始做驱动时,总觉得这玩意儿简单——不就是搬数据嘛。但后来调试一次 SPI 外设时才发现,选错了传输类型,性能直接腰斩。

常见的 DMA 传输有四种:内存到内存、外设到内存、内存到外设、链式(Scatter-Gather)。它们各有各的脾气,咱们一个一个过。

3.2 内存到内存(Memory-to-Memory)

这种模式下,源和目标都是系统内存。DMA 控制器的搬运路径:内存 A → 内存 B。
典型场景:图像帧拷贝、协议栈零拷贝、加密算法的输入输出缓冲区。我在项目中用它加速过摄像头帧的预处理,比 CPU memcpy 快 3 倍。

配置要点:

  • 需要设置 M2M 模式位(通常在 DMA_CCR 寄存器)。
  • 源地址和目标地址都必须是可寻址的内存区。
  • 注意对齐:32 位总线建议 4 字节对齐,否则会触发总线错误。
⚠ 我曾经踩过的坑: 有些 DMA 控制器硬件不支持 M2M 模式!比如 STM32F1 的 DMA1 就只支持外设相关传输,你用 M2M 会直接挂掉。一定要先查芯片手册的 DMA 特性表。

3.3 外设到内存(Peripheral-to-Memory)

外设产生数据,DMA 把它搬到内存。典型路径:UART/USART RX → 内存 buffer / ADC 采样值 → 内存数组。

这是个单向传输,外设通常是固定地址(如 USART_DR),内存地址递增。我调试一个音频输入项目时,就用 P2M 把 I2S 的左右声道数据直接灌到环形缓冲区。

寄存器关键配置:

字段说明常用值
源地址模式外设地址固定不变PINC = 0
目标地址模式内存地址自动递增MINC = 1
传输方向外设 → 内存DIR = 0 (对外设读取)
💡 个人习惯: 我会优先为 P2M 传输开中断或使用双缓冲,避免数据溢出。尤其在高波特率 UART 场景,一个字节漏掉就可能导致协议帧错乱。

3.4 内存到外设(Memory-to-Peripheral)

方向反过来:内存 → 外设。比如播放音频时,DMA 从内存中的 PCM 数据送到 I2S 控制器 TX 寄存器。

我印象最深的是驱动一个 LED 矩阵屏。数据量不大但需要精确时序,用 M2P 模式把显存内容搬到 SPI 移位寄存器,CPU 几乎零负担。

配置注意:

  • 源地址递增,目标地址固定(外设寄存器)。
  • 外设的“请求信号”要正确连接。有些 SoC 的外设需要软件触发。
  • 如果外设 FIFO 深度很小(比如 4 字节),传输宽度和外设宽度必须匹配,否则会丢数据。
实战案例: 我在调 SDIO 写卡时,曾经因为 DMA 传输宽度设为 32 位,而 SDIO 控制器寄存器只支持 16 位访问,结果数据写错位置。后来改成 16 位 + 半字对齐,问题解决。

3.5 链式传输(Scatter-Gather)

这是最强大的模式——用描述符链表管理多个不连续的内存块。你想想看,当你的数据分散在 8 个不同的 buffer 中,但外设要求连续发送,怎么办?用链式 DMA,一个描述符链搞定。

链式传输本质上是一张“传输任务表”:每个描述符包含源地址、目标地址、传输长度、控制标志、下一个描述符指针。DMA 控制器自动遍历链表,完成一次完整的数据搬运。

典型场景:

  • 网络协议层:多个 TCP segment 分散在不同 skb 中。
  • 多媒体系统:视频帧的 YUV 分量存在三个独立 buffer。
  • 文件系统:用户态 buffer 不连续,用 SG 一次提交。

配置要点:

  1. 描述符数据结构必须 8 字节或 16 字节对齐(取决于 MCU)。
  2. 最后一个描述符的“链接”位必须清 0,否则会非法循环。
  3. 如果用缓存的描述符内存,记得传输前做 cache flush,完成后做 cache invalidate。
⚠ 我曾经遇到的大坑: 某 ARM 平台的 DMA 描述符指针要求 32 字节对齐,我按 16 字节做了,结果 DMA 控制器读描述符时随机出错。这类 bug 很难复现,后来用 trace 抓总线才查到。切记:对齐要求以芯片手册为准,别信经验值。
DMA 传输类型概览 内存到内存 M2M 帧拷贝 / 零拷贝 外设到内存 P2M UART RX / ADC 采样 内存到外设 M2P I2S TX / SPI 显示 链式传输 (Scatter-Gather) 描述符链表管理多段不连续 buffer 网络 / 多媒体 / 文件系统 说明: • 前三种是单次传输(单描述符),链式传输是批量传输(描述符链表)。 • 大多数 DMA 控制器支持同时配置不同通道执行不同传输类型。 • 链式传输可组合前三种类型:例如外设→内存A + 内存A→内存B 通过链表一条龙完成。

3.6 四种模式对比一览

模式源地址行为目标地址行为典型触发方式适用场景
M2M递增递增软件触发(或自动连续)memcpy、加密缓冲
P2M固定递增外设请求(如 RXNE)串口接收、ADC 采集
M2P递增固定外设请求(如 TXE)音频播放、SPI 发送
SG按描述符不定按描述符不定触发一次,遍历链表网络、音视频、文件 I/O

3.7 避坑总结(老司机版)

  • 对齐错乱: 描述符或 buffer 首地址未按 DMA 控制器要求对齐,数据会跑偏。
  • 缓存一致性问题: 链式传输的描述符在 cacheable 区域,做完修改一定要 flush,否则 DMA 看到的是 stale 数据。我会在每次更新描述符后加 dma_map_single(..., DMA_TO_DEVICE)。
  • 中断风暴: P2M 传输频繁触发中断,CPU 被拖死。用阈值触发或双缓冲转移。
  • 死链表: 链式描述符的 next 指针变成野指针,或者环路。我在调试时用打印逐个打印链表确认。

好了,这四种类型你已经心里有数了。实际项目中不要盲选——先问自己三个问题:数据从哪来?到哪去?需不需要拼接?答案自然出来。我记得有一次帮同事排查 SD 卡驱动性能低的问题,他发现用的 M2M 而不是链式,导致每帧都要 CPU 整理碎片 buffer。改成 SG 后吞吐量翻倍。选对类型,事半功倍。