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 传输类型与数据流向

DMA 四种传输类型数据流向图 DMA 四种传输类型数据流向 DMA 控制器 内存 (DDR/SRAM) 外设 A (UART/SPI) 外设 B (I2S/DAC) M2M M2P P2M P2P 说明: M2M 和 P2M 需要重点关注缓存一致性;M2P 注意外设 FIFO 深度;P2P 注意外设时序匹配。 箭头方向表示数据流向,双向箭头表示 M2M 可双向传输。

3.8 实际调试中的选择建议

说了这么多,到底怎么选?我个人的经验是:

  • 数据拷贝、内存整理:用 M2M,但一定要处理好 Cache
  • 发送数据到串口、音频输出:用 M2P,注意突发长度不要超过 FIFO 深度
  • ADC 采样、摄像头数据:用 P2M,配合双缓冲和 Cache 无效化
  • 协议转换、数据转发:用 P2P,但先确认外设之间能握手

小技巧: 如果你不确定该用哪种类型,先看数据是否需要经过内存。需要经过内存的,就是 M2M、M2P、P2M 之一;不需要的,就是 P2P。然后根据源和目的是谁,就能快速定位。