第4章 DMA控制器工作原理:请求映射、通道配置与传输模式
DMA,全称是Direct Memory Access,直接存储器访问。说白了,就是让数据在内存和外设之间自己搬自己,不用CPU一趟一趟地跑腿。我刚开始接触STM32时,总觉得DMA是个黑盒子,配置起来心里没底。后来做项目踩了几次坑,才慢慢摸清楚它的脾气。
这一章,咱们就把DMA的底裤扒干净。从请求映射到通道配置,从Normal模式到Circular模式,再到FIFO和突发传输,一个一个说清楚。
4.1 DMA1与DMA2:谁管谁?
STM32里通常有两个DMA控制器:DMA1和DMA2。它们不是平起平坐的,分工很明确。
| 特性 | DMA1 | DMA2 |
|---|---|---|
| 挂载总线 | AHB1(低速总线) | AHB2(高速总线) |
| 通道数 | 7个通道 | 5个通道 |
| 主要服务外设 | USART1/2/3、SPI1/2、I2C1/2、ADC1、TIM2/3/4等 | USART4/5/6、SPI3/4、I2C3、SDIO、DCMI、TIM1/8等 |
| 支持突发传输 | 部分通道支持 | 全部通道支持 |
你看,DMA2挂载在高速总线上,服务的是高速外设。我做过一个项目,用DMA2配合SDIO读写SD卡,速度明显比DMA1快一截。所以,如果你的项目里有高速外设,优先考虑DMA2。
4.2 请求映射:谁找谁?
每个DMA通道可以响应多个外设的请求,但同一时间只能服务一个。这个映射关系是固定的,不是你想怎么配就怎么配。
举个例子,USART1的发送请求,只能映射到DMA1的通道4。USART1的接收请求,只能映射到DMA1的通道5。这个在参考手册的“DMA request mapping”表格里写得清清楚楚。
关键点:配置DMA时,第一件事就是查表,确认你的外设挂在哪个DMA的哪个通道上。配错了,数据根本动不了。
我见过有人把USART1的发送配到了DMA1通道3,折腾了半天数据就是发不出去。后来一查手册,通道3是TIM2的,根本不是USART1的。嗯,这种低级错误,犯一次就记住了。
4.3 通道配置:七个参数,一个都不能少
配置一个DMA通道,需要设置七个关键参数。我习惯把它们分成三组:
- 源和目标:外设地址、存储器地址、数据宽度(字节/半字/字)
- 传输控制:传输数量、传输模式(Normal/Circular)
- 优先级:低/中/高/非常高
代码示例,以USART1发送为例:
// 配置DMA1通道4用于USART1发送
DMA_InitTypeDef DMA_InitStruct;
// 1. 使能DMA1时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
// 2. 配置通道参数
DMA_InitStruct.DMA_Channel = DMA_Channel_4; // USART1_TX固定通道4
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; // 外设地址
DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)tx_buffer; // 内存地址
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; // 8位数据
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 8位数据
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; // FIFO阈值
DMA_InitStruct.DMA_MemoryBurst = DMA_MemoryBurst_Single; // 单次传输
DMA_InitStruct.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream4, &DMA_InitStruct);
我的习惯:配置DMA时,先把所有参数清零,再一个一个填。这样不会漏掉任何一项。尤其是FIFO和突发传输的配置,很多人会忘记设置,导致性能上不去。
4.4 Normal模式 vs Circular模式
这两个模式,是DMA传输的核心区别。选错了,你的系统可能直接崩掉。
| 特性 | Normal模式 | Circular模式 |
|---|---|---|
| 传输完成后 | 停止,需要重新使能 | 自动从头开始 |
| 适用场景 | 一次性数据传输(如发送一帧数据) | 连续数据流(如ADC连续采样、音频播放) |
| 中断触发 | 传输完成中断只触发一次 | 每次循环完成都触发中断 |
| 内存地址 | 递增到末尾后停止 | 递增到末尾后回到起始地址 |
Normal模式很好理解,就是一次性的。比如你要通过串口发送一包数据,发完就完事了。这时候用Normal模式,配合传输完成中断,在中断里做后续处理。
Circular模式就有点意思了。它像一个环形缓冲区,数据源源不断地往里填。我做过一个音频采集项目,ADC以48kHz采样率连续采集数据,用Circular模式配合双缓冲,数据永远不会丢。
注意:Circular模式下,如果你在中断里处理数据,一定要快。否则下一次循环完成时,你的数据还没处理完,就会覆盖掉。我曾经因为这个原因,音频里出现了爆音,排查了半天才发现是中断处理时间太长。
4.5 FIFO与突发传输:性能加速器
FIFO和突发传输,是DMA性能调优的两把利器。但用不好,反而会拖慢系统。
FIFO(先进先出缓冲器):每个DMA流都有一个4级深度的FIFO。它的作用是缓存数据,让DMA可以批量处理。FIFO有四个阈值可选:1/4、1/2、3/4、满。
突发传输(Burst Transfer):一次请求传输多个数据,而不是一个。突发长度可以是4、8、16个节拍。
这两个东西是配合使用的。FIFO满了之后,DMA发起一次突发传输,把FIFO里的数据一口气搬走。这样能减少总线仲裁的次数,提高传输效率。
我举个例子,你想想看:
- 没有FIFO和突发:每次传输1个字节,需要1次总线请求。传输256字节,需要256次请求。
- 有FIFO和突发:FIFO阈值设为1/2(2级),突发长度设为4。每次突发传输4个字节,只需要64次请求。
效率提升了4倍。这就是性能调优的价值。
配置建议:
- 低速外设(如USART 9600波特率):关闭FIFO,使用直通模式。因为数据量小,FIFO反而增加延迟。
- 高速外设(如SPI 10MHz以上):开启FIFO,阈值设为1/2或3/4,突发长度设为4或8。
- 内存到内存传输:开启FIFO,阈值设为满,突发长度设为16。这是最高效的配置。
4.6 知识结构图:DMA控制器全景
下面这张图,把DMA控制器的核心逻辑串起来了。从请求映射到通道配置,再到传输模式和FIFO/突发,一目了然。
4.7 避坑指南:我踩过的那些坑
做DMA调优这几年,我踩过不少坑。挑几个典型的说说:
- 坑一:忘记使能DMA时钟。配置了半天,DMA根本不工作。检查时钟使能,这是第一步。
- 坑二:外设地址写错了。USART的数据寄存器是DR,不是SR。我见过有人把状态寄存器的地址填进去,结果数据全乱了。
- 坑三:传输数量没减1。DMA的传输数量寄存器是实际数量减1。比如要传256个字节,填255。这个坑在STM32F1系列上特别常见。
- 坑四:中断标志没清除。传输完成中断触发后,一定要在中断服务函数里清除标志位。否则会一直进中断,系统卡死。
- 坑五:Circular模式下缓冲区溢出。处理速度跟不上DMA传输速度,数据被覆盖。解决方案是使用双缓冲,或者提高中断处理效率。
我曾经踩过的一个大坑:在一个工业控制项目中,我用DMA配合SPI读取传感器数据。配置了FIFO和突发传输,结果数据总是错位。排查了两天,最后发现是SPI的时钟极性和相位配置错了,导致数据采样点不对。DMA本身没问题,但数据源就是错的。所以,调DMA之前,先确认外设本身工作正常。