3. DMA 传输模式详解:内存到内存、内存到外设、外设到内存、外设到外设
DMA 的传输模式,说白了就是数据从哪搬到哪。我刚开始接触 DMA 时,总觉得模式很多容易搞混。后来发现,你只要搞清楚两个问题:源地址在哪?目标地址在哪?剩下的就是配置寄存器的事了。
DMA 控制器支持四种基本传输模式。每种模式对应不同的硬件场景,配置方式也有差异。咱们一个一个来看。
3.1 内存到内存(Memory-to-Memory)
这种模式最简单。数据从一块内存搬到另一块内存。CPU 不参与数据搬运,只负责启动传输。
典型应用场景:
- 数据缓冲区拷贝(比如从接收缓冲区搬到应用缓冲区)
- 图像帧缓冲区的快速复制
- 内存测试时的数据填充
配置时有个关键点:源地址和目标地址都必须支持增量模式。我遇到过有人把源地址设成固定模式,结果搬来搬去都是同一个数据,调试了半天才发现。
我的习惯:做内存到内存传输时,我会把数据宽度设成和总线宽度一致。比如 32 位总线就用字传输,这样效率最高。如果数据不对齐,DMA 控制器会自动处理,但性能会下降。
3.2 内存到外设(Memory-to-Peripheral)
这种模式用于把数据从内存发送到外设。比如通过 SPI 发送数据、通过 UART 发送字符串、通过 I2S 播放音频。
配置时要注意:源地址(内存)通常设为增量模式,目标地址(外设数据寄存器)设为固定模式。为什么?因为外设的数据寄存器地址是固定的,你每次写同一个地址就行。
举个例子,用 DMA 发送 UART 数据:
// 伪代码:UART DMA 发送配置
DMA_Channel->CMAR = (uint32_t)tx_buffer; // 内存源地址,增量模式
DMA_Channel->CPAR = (uint32_t)&USART1->DR; // 外设目标地址,固定模式
DMA_Channel->CNDTR = buffer_size; // 传输数据量
DMA_Channel->CCR |= DMA_CCR_MINC; // 内存地址增量
// 注意:不设置 PINC,外设地址固定
我曾经踩过的坑:有一次做 SPI DMA 发送,数据总是错位。查了半天发现是 SPI 的时钟极性和相位没配好。DMA 只管搬数据,外设本身的时序配置还得你自己搞定。别指望 DMA 能帮你解决外设的配置问题。
3.3 外设到内存(Peripheral-to-Memory)
这是最常用的模式。外设产生数据,DMA 自动搬到内存。比如 ADC 采样结果存储、UART 接收数据、SPI 接收数据。
配置要点:源地址(外设数据寄存器)固定,目标地址(内存)增量。和内存到外设正好反过来。
| 配置项 | 外设到内存 | 内存到外设 |
|---|---|---|
| 源地址模式 | 固定(外设寄存器) | 增量(内存缓冲区) |
| 目标地址模式 | 增量(内存缓冲区) | 固定(外设寄存器) |
| 触发源 | 外设请求 | 外设请求 |
| 典型应用 | ADC、UART 接收 | DAC、UART 发送 |
这里有个细节:传输完成中断。做外设到内存传输时,我习惯在传输完成中断里处理数据。比如 ADC 采集完 100 个点,中断里做 FFT 计算。这样 CPU 利用率极高。
3.4 外设到外设(Peripheral-to-Peripheral)
这种模式用得最少,但某些场景下非常有用。数据从一个外设直接搬到另一个外设,完全不经过内存。
举个例子:从 ADC 直接到 DAC,实现模拟信号的直通。或者从 SPI 接收的数据直接通过另一个 SPI 发送出去。
配置要点:
- 源地址和目标地址都设为固定模式
- 两个外设的数据宽度必须一致
- 触发源通常来自源外设
- 注意两个外设的时序匹配
说实话,外设到外设模式我实际项目中只用过一次。那次是做音频处理,需要把 I2S 接收的数据直接送到另一个 I2S 发送。用 DMA 外设到外设模式,延迟极低,效果很好。
注意:不是所有 DMA 控制器都支持外设到外设模式。有些 MCU 的 DMA 只支持内存参与传输。用之前一定要查数据手册,别想当然。
3.5 四种模式对比总结
为了让你看得更清楚,我把四种模式的核心差异整理成一张表:
| 传输模式 | 源地址 | 目标地址 | 触发方式 | 使用频率 |
|---|---|---|---|---|
| 内存→内存 | 增量 | 增量 | 软件触发 | 中等 |
| 内存→外设 | 增量 | 固定 | 外设请求 | 高 |
| 外设→内存 | 固定 | 增量 | 外设请求 | 最高 |
| 外设→外设 | 固定 | 固定 | 外设请求 | 低 |
3.6 传输模式选择流程图
下面这张图帮你快速判断该用哪种模式:
3.7 实际项目中的选择建议
说了这么多,我总结几条实战经验:
- 外设到内存用得最多——ADC 采集、UART 接收、SPI 接收,基本天天用
- 内存到外设次之——DAC 输出、UART 发送、PWM 波形数据更新
- 内存到内存偶尔用——大数据拷贝、图像处理、协议栈缓冲区管理
- 外设到外设很少用——特殊场景才用,比如音频直通、信号转发
我的建议:刚开始学 DMA,先把外设到内存和内存到外设这两种模式练熟。这两种覆盖了 90% 以上的应用场景。等用顺手了,再去研究内存到内存和外设到外设。