3. DMA传输类型:内存到内存、内存到外设、外设到内存、外设到外设
DMA 传输,说白了就是让数据自己跑,不占 CPU。但数据从哪跑到哪,这里头门道不少。我刚开始接触 DMA 时,以为就是把数据从 A 搬到 B,后来发现传输类型不同,硬件配置、缓存策略、甚至调试手段都完全不一样。
咱们把 DMA 传输分成四类,每一类都有它的脾气。你想想看,内存到内存和外设到外设,虽然都叫 DMA,但底层走的总线路径完全不同。
3.1 四种传输类型概览
| 传输类型 | 源端 | 目的端 | 典型场景 | 缓存一致性风险 |
|---|---|---|---|---|
| 内存到内存 (M2M) | DDR/SRAM | DDR/SRAM | 数据拷贝、帧缓冲复制、CRC 计算 | 高(CPU 缓存与 DMA 数据不同步) |
| 内存到外设 (M2P) | DDR/SRAM | UART/SPI/I2S/DAC | 发送数据、音频播放、波形输出 | 中(外设可能读到脏缓存) |
| 外设到内存 (P2M) | ADC/GPIO/Camera | DDR/SRAM | 数据采集、图像捕获、按键扫描 | 高(CPU 可能读到过时数据) |
| 外设到外设 (P2P) | 外设 A | 外设 B | SPI 转 UART、I2S 转 DAC | 低(通常不经过内存) |
核心观点: 传输类型决定了 DMA 控制器的通道配置、地址增长模式、以及是否需要手动维护缓存一致性。选错了类型,轻则数据错乱,重则系统死锁。
3.2 内存到内存(M2M)
这是最基础也最容易出坑的类型。DMA 直接把数据从一块内存搬到另一块内存,CPU 全程不参与。听起来简单,但我在项目中遇到过一个问题:用 DMA 拷贝一个视频帧缓冲,结果画面出现撕裂——因为 CPU 缓存里的数据和 DMA 搬的数据不一致。
为什么会这样?CPU 写数据时先写缓存,DMA 读内存时直接读物理地址。如果缓存没刷出去,DMA 拿到的就是旧数据。
避坑指南: 我曾经在 STM32 上做 M2M 传输,忘了在启动 DMA 前调用 SCB_CleanInvalidateDCache(),结果搬了 10 次数据,有 3 次是错的。调试了两天才找到原因。
配置要点:
- 源地址和目的地址都必须是物理地址(MMU 开启时尤其注意)
- 地址增长模式:源地址递增,目的地址递增
- 传输完成中断:必须开启,用于通知 CPU 数据已就绪
- 缓存操作:传输前刷 Cache,传输后无效化 Cache
// 伪代码:M2M DMA 配置示例
dma_config_t cfg;
cfg.src_addr = (uint32_t)src_buf_phys; // 源物理地址
cfg.dst_addr = (uint32_t)dst_buf_phys; // 目的物理地址
cfg.transfer_size = 1024; // 1024 字节
cfg.src_inc = DMA_ADDR_INC_ENABLE; // 源地址递增
cfg.dst_inc = DMA_ADDR_INC_ENABLE; // 目的地址递增
cfg.transfer_type = DMA_MEM_TO_MEM;
// 刷 Cache:确保源数据从缓存写回内存
SCB_CleanDCache_by_Addr(src_buf, 1024);
// 启动 DMA
dma_start(&cfg);
// 等待传输完成
while(!dma_is_done());
// 无效化 Cache:让 CPU 重新从内存读取目的数据
SCB_InvalidateDCache_by_Addr(dst_buf, 1024);
3.3 内存到外设(M2P)
这种类型很常见,比如用 DMA 把数据从内存搬到 UART 发送寄存器。CPU 只需要把数据准备好,DMA 自动按节奏往外送。
我个人习惯在 M2P 传输中特别注意外设的 FIFO 深度。如果外设 FIFO 只有 16 字节,而 DMA 一次突发传输 64 字节,就会导致数据溢出。
经验之谈: 我调试过一个音频播放项目,用 I2S 接口输出音频。DMA 配置成 M2P,但声音总是断断续续。后来发现是 DMA 的突发长度设成了 16,而 I2S 的 FIFO 深度只有 8。改成 4 之后,问题解决。
配置要点:
- 源地址:内存缓冲区(物理地址)
- 目的地址:外设数据寄存器地址(固定不变)
- 地址增长模式:源递增,目的固定
- 外设流控:通常由外设的请求信号触发 DMA 传输
- 缓存操作:传输前刷 Cache,确保外设读到最新数据
3.4 外设到内存(P2M)
这是数据采集场景的主力。ADC 采样、摄像头数据、GPIO 电平变化,都可以通过 DMA 直接存入内存。
嗯,这里要注意一个关键点:外设产生数据的速度和 DMA 搬数据的速度必须匹配。如果外设太快,DMA 来不及搬,就会丢数据。
避坑指南: 我曾经用 DMA 采集高速 ADC 数据,配置了双缓冲(ping-pong buffer)。结果发现有一半的数据是错的——因为我在 DMA 传输完成中断里处理数据时,忘了先无效化 Cache。CPU 读到的还是缓存里的旧数据。
配置要点:
- 源地址:外设数据寄存器地址(固定)
- 目的地址:内存缓冲区(物理地址,建议使用双缓冲)
- 地址增长模式:源固定,目的递增
- 传输完成中断:用于切换缓冲区和处理数据
- 缓存操作:传输完成后立即无效化目的内存的 Cache
3.5 外设到外设(P2P)
这种类型用得最少,但某些场景下非常高效。比如把 SPI 接收的数据直接转发到 UART 发送,不经过内存,延迟极低。
我印象最深的一个项目是用 FPGA 做协议转换,DMA 把 SPI 的数据直接搬到并行总线。当时硬件工程师说这样能省掉一个 FIFO 芯片,确实做到了。
配置要点:
- 源地址:外设 A 的数据寄存器
- 目的地址:外设 B 的数据寄存器
- 地址增长模式:源固定,目的固定
- 数据宽度:源和目的的数据宽度必须一致(比如都是 8 位)
- 流控:通常由源外设的请求信号触发
注意事项: P2P 传输不经过内存,所以不存在缓存一致性问题。但外设之间的时序匹配需要格外小心。我曾经遇到过 SPI 时钟太快,UART 来不及发送,导致数据丢失的情况。
3.6 四种传输类型的核心差异对比
| 对比维度 | M2M | M2P | P2M | P2P |
|---|---|---|---|---|
| 源地址增长 | 递增 | 递增 | 固定 | 固定 |
| 目的地址增长 | 递增 | 固定 | 递增 | 固定 |
| 缓存一致性风险 | 高 | 中 | 高 | 无 |
| 典型触发方式 | 软件触发 | 外设请求 | 外设请求 | 外设请求 |
| 调试难度 | 中 | 低 | 高 | 高 |
3.7 知识结构图:DMA 传输类型与数据流向
3.8 实际调试中的选择建议
说了这么多,到底怎么选?我个人的经验是:
- 数据拷贝、内存整理:用 M2M,但一定要处理好 Cache
- 发送数据到串口、音频输出:用 M2P,注意突发长度不要超过 FIFO 深度
- ADC 采样、摄像头数据:用 P2M,配合双缓冲和 Cache 无效化
- 协议转换、数据转发:用 P2P,但先确认外设之间能握手
小技巧: 如果你不确定该用哪种类型,先看数据是否需要经过内存。需要经过内存的,就是 M2M、M2P、P2M 之一;不需要的,就是 P2P。然后根据源和目的是谁,就能快速定位。