1. 为什么需要 DMA?
想象一下:你有一个串口每 100μs 收到一个字节,需要用 CPU 把这个字节从串口数据寄存器搬到内存的缓冲区里。
- 没有 DMA 时:每次接收完成,串口中断触发 → CPU 读数据 → 存到数组 → 退出中断。如果数据速率很高(比如 2Mbps),CPU 大部分时间都在"搬砖",根本没空做其他任务。
- 有 DMA 时 :配置 DMA 通道自动把串口数据搬运到内存,完全不需要 CPU 插手,只在搬完一整个数据块后才产生一次中断通知 CPU。
DMA(Direct Memory Access,直接存储器访问) 就是这样一个"硬件搬运工":它可以在 不占用 CPU 的情况下,在内存与外设、内存与内存之间快速传输数据。
2. DMA 的核心工作原理
DMA 控制器本身是一个精简的"小 CPU",它有:
- 源地址:从哪读数据(可以是外设数据寄存器,也可以是内存数组)。
- 目标地址:写到哪去。
- 传输计数器:还要传多少字节。
- 传输模式:单次、循环、增量/固定地址等。
基本流程:
- CPU 配置 DMA 通道(源地址、目标地址、数据长度、触发源等)。
- 使能 DMA。
- 当触发事件发生(比如串口收到一个字节),DMA 自动执行一次搬运:读源地址 → 写目标地址 → 计数器减 1。
- 计数器减到 0 时,DMA 产生传输完成中断(可选),通知 CPU 处理。
3. 在 STM32 中的应用场景
| 场景 | 作用 | 效果 |
|---|---|---|
| 串口(UART)接收 | DMA 将串口数据自动存入环形缓冲区 | CPU 只在收到一帧完整数据时才处理 |
| ADC 多通道扫描 | DMA 将 ADC 结果连续存入数组 | 可实现高频采样(几十 kHz)而 CPU 几乎零负担 |
| I2C/SPI 通信 | DMA 自动发送/接收数据块 | 配合 RTOS,通信任务可以阻塞直到 DMA 完成 |
| 内存拷贝 | 两个内存区域之间高速拷贝 | 比 memcpy 快,且不占用 CPU |
| 定时器 PWM 更新 | 用 DMA 自动修改多个 CCR 值 | 实现复杂的 LED 呼吸灯模式或波形输出 |
4. 关键概念:循环模式 vs 普通模式
- 普通模式:传输计数器减到 0 后停止,需要 CPU 重新配置才能再次启动。适合固定长度的数据块(比如发送一个 512 字节的传感器数据包)。
- 循环模式 :计数器减到 0 后自动重装初始值,继续从头搬运。适合环形缓冲区(比如连续 ADC 采样)。STM32 的 DMA 循环模式需要配合 FIFO 或 双缓冲区 来防止数据覆盖。
5. 双缓冲区(Double Buffer)技巧
某些 STM32 的 DMA 支持双缓冲区(如 DMA2)。两个缓冲区交替使用:
- 当 DMA 正在填充缓冲区 0 时,CPU 可以处理缓冲区 1。
- 缓冲区满后,DMA 自动切换目标到另一个缓冲区,并触发中断通知 CPU 处理已满的那个。
这是实现 无锁、零拷贝 高吞吐数据流的标准方案。
6. 使用 DMA 的注意事项
- 数据一致性 :如果 DMA 和 CPU 同时访问同一块内存,必须用
__DSB()屏障或关中断保护,防止 CPU 读到缓存中的旧数据(尤其在 Cortex-M7 带数据缓存时)。 - 对齐要求:某些 DMA 传输要求源/目标地址按字(4 字节)对齐,否则可能出错或降低效率。
- 通道冲突:多个外设可能共享同一个 DMA 通道(如 USART1_TX 和 SPI1_RX),需要合理分配优先级和仲裁。
- 中断优先级:DMA 传输完成中断通常设较低优先级,避免影响实时任务。
- 调试困难:DMA 在后台"偷偷"搬运,一旦配置错(如地址写反),现象很诡异(数据全零或错乱),需要仔细检查。
7. 代码示例(STM32 HAL)
c
// 配置 ADC1 用 DMA 连续采样 100 个值到数组
#define ADC_BUFFER_SIZE 100
uint32_t adc_buffer[ADC_BUFFER_SIZE];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void ADC_DMA_Init(void) {
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
// 配置 DMA 通道
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址固定(ADC_DR)
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
// 启动 ADC 并开始 DMA 传输
HAL_ADC_Start_DMA(&hadc1, adc_buffer, ADC_BUFFER_SIZE);
}
之后,adc_buffer 中的值会不断被更新,CPU 可以在主循环中读取最新数据,完全无感知。
总结
DMA 是嵌入式系统实现 高吞吐、低 CPU 负载 的关键技术。学会 DMA,你的程序就能从"忙得团团转"变成"悠然自得"。