第 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 字节对齐,否则会触发总线错误。
3.3 外设到内存(Peripheral-to-Memory)
外设产生数据,DMA 把它搬到内存。典型路径:UART/USART RX → 内存 buffer / ADC 采样值 → 内存数组。
这是个单向传输,外设通常是固定地址(如 USART_DR),内存地址递增。我调试一个音频输入项目时,就用 P2M 把 I2S 的左右声道数据直接灌到环形缓冲区。
寄存器关键配置:
| 字段 | 说明 | 常用值 |
|---|---|---|
| 源地址模式 | 外设地址固定不变 | PINC = 0 |
| 目标地址模式 | 内存地址自动递增 | MINC = 1 |
| 传输方向 | 外设 → 内存 | DIR = 0 (对外设读取) |
3.4 内存到外设(Memory-to-Peripheral)
方向反过来:内存 → 外设。比如播放音频时,DMA 从内存中的 PCM 数据送到 I2S 控制器 TX 寄存器。
我印象最深的是驱动一个 LED 矩阵屏。数据量不大但需要精确时序,用 M2P 模式把显存内容搬到 SPI 移位寄存器,CPU 几乎零负担。
配置注意:
- 源地址递增,目标地址固定(外设寄存器)。
- 外设的“请求信号”要正确连接。有些 SoC 的外设需要软件触发。
- 如果外设 FIFO 深度很小(比如 4 字节),传输宽度和外设宽度必须匹配,否则会丢数据。
3.5 链式传输(Scatter-Gather)
这是最强大的模式——用描述符链表管理多个不连续的内存块。你想想看,当你的数据分散在 8 个不同的 buffer 中,但外设要求连续发送,怎么办?用链式 DMA,一个描述符链搞定。
链式传输本质上是一张“传输任务表”:每个描述符包含源地址、目标地址、传输长度、控制标志、下一个描述符指针。DMA 控制器自动遍历链表,完成一次完整的数据搬运。
典型场景:
- 网络协议层:多个 TCP segment 分散在不同 skb 中。
- 多媒体系统:视频帧的 YUV 分量存在三个独立 buffer。
- 文件系统:用户态 buffer 不连续,用 SG 一次提交。
配置要点:
- 描述符数据结构必须 8 字节或 16 字节对齐(取决于 MCU)。
- 最后一个描述符的“链接”位必须清 0,否则会非法循环。
- 如果用缓存的描述符内存,记得传输前做 cache flush,完成后做 cache invalidate。
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 后吞吐量翻倍。选对类型,事半功倍。