DMA基础回顾:STM32 DMA控制器架构、传输模式与配置流程
说实话,很多工程师用STM32做项目,DMA这块都是「能用就行」。复制个例程,改改外设地址,跑通了就不再深究。但真到了低功耗场景,你会发现——DMA用得对不对,直接决定你的系统能不能睡着,睡多深。
我个人习惯,每接手一个新项目,第一件事就是翻开参考手册的DMA章节,把架构图重新过一遍。别笑,这习惯救过我不少次。
DMA控制器架构:两条总线,一个核心
STM32的DMA控制器,说白了就是一个独立的数据搬运工。它不占用CPU的运算资源,自己就能在外设和存储器之间倒腾数据。
以F4系列为例,DMA控制器挂载在AHB总线上。它有两条主接口总线:一条是存储器端口,一条是外设端口。为什么分两条?为了并行处理。你想想看,如果只有一条总线,读源地址和写目标地址就得排队,效率直接砍半。
核心要点:DMA传输的本质是「源地址 → 目标地址」的数据拷贝,整个过程由硬件状态机控制,不需要CPU逐字节干预。
我在项目中遇到过一个问题:某次用DMA从ADC搬运数据到内存,发现数据偶尔会错位。查了半天,原来是DMA的优先级配置和另一个高优先级DMA通道冲突了。嗯,这里要注意——DMA通道之间是有优先级仲裁的,软件可以配置。
下面这张图是我自己整理的DMA控制器架构,你看一眼就明白了:
三种传输模式:你该怎么选?
STM32的DMA支持三种传输模式。很多初学者搞不清区别,我直接给你列个表:
| 传输模式 | 工作方式 | 典型应用场景 | 注意事项 |
|---|---|---|---|
| 普通模式 | 传输完设定的数据量后自动停止 | 单次ADC采集、单帧SPI收发 | 需要软件重新使能才能再次启动 |
| 循环模式 | 传输完成后自动重载,无限循环 | 连续ADC采样、音频流、实时数据采集 | 注意缓冲区不要被覆盖 |
| 存储器到存储器模式 | 无需外设触发,由软件启动 | 内存拷贝、数据搬移、图像处理 | F1系列不支持,F4及以上才有 |
我的经验:低功耗场景下,优先用普通模式。循环模式虽然方便,但会让DMA一直处于活跃状态,系统很难进入深度睡眠。我曾经在一个电池供电的传感器节点上,就因为用了循环模式,导致待机电流多了200μA——查了两天才发现。
配置流程:七步走,一步别少
DMA的配置流程其实很固定。我把它总结成7个步骤,你照着做基本不会出错:
- 使能DMA时钟——RCC_AHB1PeriphClockCmd(),别漏了
- 初始化DMA通道——配置源地址、目标地址、传输方向、数据宽度、优先级
- 设置传输数据量——DMA_CNDTR寄存器,注意是传输次数,不是字节数
- 配置外设触发源——比如USART_TX请求、SPI_RX请求、TIM_UP请求
- 使能DMA中断(可选)——传输完成中断、半传输中断、错误中断
- 使能DMA通道——DMA_Cmd(),开始干活
- 启动外设传输——外设自己产生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是低功耗系统的「神队友」。