第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/突发,一目了然。

DMA控制器工作原理全景图 DMA控制器工作原理全景图 DMA1 (AHB1) DMA2 (AHB2) 通道1~7 通道1~5 请求映射 USART1→CH4, SPI1→CH3... 请求映射 SDIO→CH3, DCMI→CH1... 通道配置(7个参数) 源/目标/方向/数量/模式/优先级/FIFO Normal模式 一次性传输 Circular模式 循环传输 FIFO + 突发 性能加速 核心:查映射 → 配通道 → 选模式 → 调FIFO/突发

4.7 避坑指南:我踩过的那些坑

做DMA调优这几年,我踩过不少坑。挑几个典型的说说:

  • 坑一:忘记使能DMA时钟。配置了半天,DMA根本不工作。检查时钟使能,这是第一步。
  • 坑二:外设地址写错了。USART的数据寄存器是DR,不是SR。我见过有人把状态寄存器的地址填进去,结果数据全乱了。
  • 坑三:传输数量没减1。DMA的传输数量寄存器是实际数量减1。比如要传256个字节,填255。这个坑在STM32F1系列上特别常见。
  • 坑四:中断标志没清除。传输完成中断触发后,一定要在中断服务函数里清除标志位。否则会一直进中断,系统卡死。
  • 坑五:Circular模式下缓冲区溢出。处理速度跟不上DMA传输速度,数据被覆盖。解决方案是使用双缓冲,或者提高中断处理效率。

我曾经踩过的一个大坑:在一个工业控制项目中,我用DMA配合SPI读取传感器数据。配置了FIFO和突发传输,结果数据总是错位。排查了两天,最后发现是SPI的时钟极性和相位配置错了,导致数据采样点不对。DMA本身没问题,但数据源就是错的。所以,调DMA之前,先确认外设本身工作正常。