DMA基础回顾:STM32 DMA控制器架构、传输模式与配置流程

说实话,很多工程师用STM32做项目,DMA这块都是「能用就行」。复制个例程,改改外设地址,跑通了就不再深究。但真到了低功耗场景,你会发现——DMA用得对不对,直接决定你的系统能不能睡着,睡多深。

我个人习惯,每接手一个新项目,第一件事就是翻开参考手册的DMA章节,把架构图重新过一遍。别笑,这习惯救过我不少次。

DMA控制器架构:两条总线,一个核心

STM32的DMA控制器,说白了就是一个独立的数据搬运工。它不占用CPU的运算资源,自己就能在外设和存储器之间倒腾数据。

以F4系列为例,DMA控制器挂载在AHB总线上。它有两条主接口总线:一条是存储器端口,一条是外设端口。为什么分两条?为了并行处理。你想想看,如果只有一条总线,读源地址和写目标地址就得排队,效率直接砍半。

核心要点:DMA传输的本质是「源地址 → 目标地址」的数据拷贝,整个过程由硬件状态机控制,不需要CPU逐字节干预。

我在项目中遇到过一个问题:某次用DMA从ADC搬运数据到内存,发现数据偶尔会错位。查了半天,原来是DMA的优先级配置和另一个高优先级DMA通道冲突了。嗯,这里要注意——DMA通道之间是有优先级仲裁的,软件可以配置。

下面这张图是我自己整理的DMA控制器架构,你看一眼就明白了:

STM32 DMA控制器架构图 CPU (Cortex-M) AHB 系统总线 DMA 控制器 存储器端口 外设端口 CH1 CH2 CH3 CH4 外设 (USART/SPI/ADC...) 存储器 (SRAM/Flash) DMA通过存储器端口和外设端口分别访问存储器和外设,实现双通道并行传输

三种传输模式:你该怎么选?

STM32的DMA支持三种传输模式。很多初学者搞不清区别,我直接给你列个表:

传输模式 工作方式 典型应用场景 注意事项
普通模式 传输完设定的数据量后自动停止 单次ADC采集、单帧SPI收发 需要软件重新使能才能再次启动
循环模式 传输完成后自动重载,无限循环 连续ADC采样、音频流、实时数据采集 注意缓冲区不要被覆盖
存储器到存储器模式 无需外设触发,由软件启动 内存拷贝、数据搬移、图像处理 F1系列不支持,F4及以上才有

我的经验:低功耗场景下,优先用普通模式。循环模式虽然方便,但会让DMA一直处于活跃状态,系统很难进入深度睡眠。我曾经在一个电池供电的传感器节点上,就因为用了循环模式,导致待机电流多了200μA——查了两天才发现。

配置流程:七步走,一步别少

DMA的配置流程其实很固定。我把它总结成7个步骤,你照着做基本不会出错:

  1. 使能DMA时钟——RCC_AHB1PeriphClockCmd(),别漏了
  2. 初始化DMA通道——配置源地址、目标地址、传输方向、数据宽度、优先级
  3. 设置传输数据量——DMA_CNDTR寄存器,注意是传输次数,不是字节数
  4. 配置外设触发源——比如USART_TX请求、SPI_RX请求、TIM_UP请求
  5. 使能DMA中断(可选)——传输完成中断、半传输中断、错误中断
  6. 使能DMA通道——DMA_Cmd(),开始干活
  7. 启动外设传输——外设自己产生DMA请求,DMA自动响应

下面给一个标准配置代码,我用的是F4标准库,HAL库逻辑一样:

void DMA_Config(void)
{
    // 1. 使能DMA时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
    
    // 2. 初始化DMA通道
    DMA_InitTypeDef DMA_InitStruct;
    DMA_InitStruct.DMA_Channel = DMA_Channel_0;          // 选择通道
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;  // 外设地址
    DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)TxBuffer;        // 存储器地址
    DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral;            // 传输方向
    DMA_InitStruct.DMA_BufferSize = 256;                            // 传输数据量
    DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   // 外设地址固定
    DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;            // 存储器地址递增
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;                      // 普通模式
    DMA_InitStruct.DMA_Priority = DMA_Priority_High;                // 优先级
    DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable;
    DMA_InitStruct.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
    DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single;
    DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    DMA_Init(DMA2_Stream7, &DMA_InitStruct);
    
    // 3. 使能传输完成中断
    DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE);
    
    // 4. 使能DMA流
    DMA_Cmd(DMA2_Stream7, ENABLE);
}

曾经踩过的坑:配置DMA时,一定要先配置完所有参数,最后再使能通道。我曾经在初始化一半的时候使能了DMA,结果外设突然来了请求,DMA拿着半残的配置就开始传输——数据全乱了。顺序很重要,别偷懒。

关键寄存器:你只需要记住这三个

DMA的寄存器很多,但真正需要你手配的,就三个:

  • CCR(配置寄存器)——传输方向、模式、优先级、数据宽度,都在这里
  • CNDTR(数据量寄存器)——还剩多少数据没传完,读这个就知道进度
  • CPAR/CMAR(地址寄存器)——外设地址和存储器地址,各占一个

调试的时候,我最喜欢看CNDTR。如果它一直在减,说明DMA在正常工作。如果它不动了——嗯,要么传输完了,要么卡住了。

中断与事件:别让CPU空等

DMA可以产生三种中断:

中断类型 触发条件 低功耗场景建议
传输完成中断 CNDTR减到0 必须用,用于唤醒CPU处理数据
半传输中断 CNDTR减到一半 双缓冲场景用,普通场景可关
传输错误中断 总线错误、FIFO溢出 建议开启,调试阶段尤其重要

低功耗设计里,我通常只开传输完成中断。半传输中断在双缓冲音频播放时有用,但平时开着只会增加不必要的唤醒次数。

小结

DMA的基础其实不复杂。架构上记住「两条总线、一个控制器」;模式上记住「普通、循环、存储器到存储器」;配置上记住「七步流程」。把这些吃透了,后面讲低功耗协同的时候,你才能理解为什么DMA是低功耗系统的「神队友」。