DMA —— 让 CPU “偷懒”的数据搬运工

1. 为什么需要 DMA?

想象一下:你有一个串口每 100μs 收到一个字节,需要用 CPU 把这个字节从串口数据寄存器搬到内存的缓冲区里。

  • 没有 DMA 时:每次接收完成,串口中断触发 → CPU 读数据 → 存到数组 → 退出中断。如果数据速率很高(比如 2Mbps),CPU 大部分时间都在"搬砖",根本没空做其他任务。
  • 有 DMA 时 :配置 DMA 通道自动把串口数据搬运到内存,完全不需要 CPU 插手,只在搬完一整个数据块后才产生一次中断通知 CPU。

DMA(Direct Memory Access,直接存储器访问) 就是这样一个"硬件搬运工":它可以在 不占用 CPU 的情况下,在内存与外设、内存与内存之间快速传输数据。

2. DMA 的核心工作原理

DMA 控制器本身是一个精简的"小 CPU",它有:

  • 源地址:从哪读数据(可以是外设数据寄存器,也可以是内存数组)。
  • 目标地址:写到哪去。
  • 传输计数器:还要传多少字节。
  • 传输模式:单次、循环、增量/固定地址等。

基本流程

  1. CPU 配置 DMA 通道(源地址、目标地址、数据长度、触发源等)。
  2. 使能 DMA。
  3. 当触发事件发生(比如串口收到一个字节),DMA 自动执行一次搬运:读源地址 → 写目标地址 → 计数器减 1。
  4. 计数器减到 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 的注意事项

  1. 数据一致性 :如果 DMA 和 CPU 同时访问同一块内存,必须用 __DSB() 屏障或关中断保护,防止 CPU 读到缓存中的旧数据(尤其在 Cortex-M7 带数据缓存时)。
  2. 对齐要求:某些 DMA 传输要求源/目标地址按字(4 字节)对齐,否则可能出错或降低效率。
  3. 通道冲突:多个外设可能共享同一个 DMA 通道(如 USART1_TX 和 SPI1_RX),需要合理分配优先级和仲裁。
  4. 中断优先级:DMA 传输完成中断通常设较低优先级,避免影响实时任务。
  5. 调试困难: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,你的程序就能从"忙得团团转"变成"悠然自得"。

相关推荐
IAR Systems31 分钟前
在IAR Embedded Workbench for Arm中实现ROPI
arm开发·嵌入式·嵌入式开发·iar
Elihuss37 分钟前
关于RK3506 的MCU软复位后跑不起问题
linux·单片机·嵌入式硬件
fengfuyao9851 小时前
GRBL 1.1 移植到 STM32 (HAL库)
stm32·单片机·嵌入式硬件
biyezuopinvip1 小时前
基于STC89C51单片机的多波形信号发生器设计与Proteus仿真
单片机·proteus·课程设计·proteus仿真·基于stc89c51单片机的·多波形·信号发生器设计
无人装备硬件开发爱好者1 小时前
STM32G474 驱动 1.54 寸三色电子墨水屏实现贪吃蛇游戏完整指南
stm32·嵌入式硬件·游戏
山木嵌入式1 小时前
FreeRTOS任务创建全解析:动态/静态创建+实战案例+参数深度剖析
stm32·freertos
项目題供诗1 小时前
STM32-定时器定时中断&定时器外部时钟(十一)
stm32·单片机·嵌入式硬件
披着假发的程序唐2 小时前
STM32 H743 MPU的配置使用方法
linux·c语言·c++·驱动开发·stm32·单片机·mcu
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 9 天:单片机、MCU、开发板、固件
单片机·嵌入式硬件
张健11564096482 小时前
MSP主堆栈指针
单片机