STM32 DMA控制器架构:DMA1与DMA2的区别、数据流与通道、FIFO与直接模式

DMA这东西,说白了就是帮CPU干苦力的。CPU忙着跑主逻辑,你让它一边算数据一边搬数据,效率肯定打折扣。我刚开始用STM32的时候,总觉得DMA是个黑盒子,配置一堆寄存器,跑起来还容易出bug。后来摸透了它的架构,才发现其实没那么玄乎。

这一章,咱们就把DMA控制器的底裤扒开看看。DMA1和DMA2到底差在哪?数据流(Stream)和通道(Channel)是什么关系?FIFO模式和直接模式又该怎么选?搞懂这些,你写DMA驱动的时候心里就有谱了。

DMA1与DMA2:双核搬运工的分工

STM32的DMA控制器,在F4/H7系列里通常有两个:DMA1和DMA2。它们长得几乎一样,但服务的总线不同。

特性 DMA1 DMA2
挂载总线 AHB1(低速外设总线) AHB2(高速外设总线)
服务对象 ADC、SPI、I2C、USART、定时器 DCMI(摄像头)、SDIO、以太网、高速SPI
数据流数量 8条(Stream 0~7) 8条(Stream 0~7)
通道数量 每流8个通道 每流8个通道
优先级 软件可配4级 软件可配4级

你可能会问:为什么要有两个?其实很简单——避免总线冲突。DMA1走AHB1,DMA2走AHB2,两个总线矩阵是独立的。如果你的项目里同时有ADC采集和摄像头数据流,让ADC走DMA1、摄像头走DMA2,它们就不会抢总线带宽。

我的习惯:低速外设(USART、I2C、SPI)一律扔给DMA1。高速外设(DCMI、SDIO)必须用DMA2。这样分配,我在项目里几乎没遇到过DMA传输卡顿的问题。

数据流(Stream)与通道(Channel):一对多的映射关系

这是新手最容易搞混的地方。我刚开始学的时候,以为Stream和Channel是同一个东西,结果配置错了,数据死活传不过去。

数据流(Stream):你可以把它想象成一条物理传输管道。每个DMA控制器有8条这样的管道(Stream 0~7)。每条管道有自己的FIFO、地址寄存器、传输计数器,是独立的硬件资源。

通道(Channel):它是外设请求的编号。每个外设(比如USART1_TX、SPI2_RX)都有一个固定的通道号。通道号决定了这个外设的请求信号连接到哪条Stream。

关键来了:一条Stream可以响应多个通道的请求,但同一时间只能选一个通道。这就是所谓的“多对一”映射。

举个例子:

DMA2的Stream 0,可以响应通道0(SPI4_RX)、通道1(SPI1_RX)、通道2(SPI2_RX)……但你在初始化的时候,只能选其中一个。如果你想让SPI4_RX用Stream 0,那就把通道号设为0。

我整理了一张常用的映射表,你写代码的时候可以直接查:

DMA控制器 Stream编号 可用的通道(外设请求)
DMA1 Stream 0 CH0: SPI3_RX / CH1: I2C1_RX / CH2: TIM7_UP / ...
DMA1 Stream 1 CH0: SPI3_TX / CH1: I2C1_TX / CH2: TIM7_UP / ...
DMA2 Stream 0 CH0: SPI4_RX / CH1: SPI1_RX / CH2: SPI2_RX / ...
DMA2 Stream 1 CH0: SPI4_TX / CH1: SPI1_TX / CH2: SPI2_TX / ...
我曾经踩过的坑:两个外设用了同一条Stream的不同通道,结果它们同时请求DMA,导致数据错乱。后来我强制规定:每个Stream只分配给一个外设,哪怕通道不同也不行。虽然浪费了点资源,但稳定性高了很多。

FIFO与直接模式:两种传输策略

每条Stream内部都有一个4字(16字节)的FIFO。你可以选择用还是不用。

直接模式(Direct Mode)

说白了就是不经过FIFO,外设数据直接写到内存,或者内存数据直接送到外设。这种模式延迟最低,但要求外设和内存的数据宽度必须一致。比如外设是8位,内存也得是8位。

  • 优点:延迟小,适合单次传输或小数据量。
  • 缺点:不能做数据打包/解包,外设和内存宽度必须一致。
  • 适用场景:USART发送单个字节、SPI单次读写。

FIFO模式

数据先攒到FIFO里,攒够了阈值(1/4、1/2、3/4或满)再一次性搬走。这种模式允许外设和内存的数据宽度不同,比如外设是8位,内存是32位,FIFO会自动帮你打包。

  • 优点:减少总线访问次数,支持数据宽度转换,适合大数据块传输。
  • 缺点:增加了延迟(要等FIFO攒够数据)。
  • 适用场景:ADC多通道采集、DAC波形输出、SDIO读写。
我的建议:如果你不确定用哪种,优先选FIFO模式,阈值设成1/2。这样既有一定的缓冲,延迟也不会太大。我在做音频采集项目时,FIFO模式帮我解决了数据断流的问题。

DMA传输的完整流程

为了让你更直观地理解,我画了一张流程图:

DMA传输流程图 DMA传输完整流程 外设发出DMA请求 通道匹配(Channel) Stream仲裁(优先级) FIFO / 直接模式 数据传输完成 关键点 • 每个Stream有4级优先级 • 高优先级Stream先服务 • 同优先级按Stream编号 FIFO阈值 • 1/4: 4字节 • 1/2: 8字节 • 3/4: 12字节 / 满: 16字节

整个流程其实就五步:外设请求 → 通道匹配 → Stream仲裁 → 模式选择 → 数据传输。每一步都有对应的寄存器配置,HAL库帮你封装好了,但理解底层逻辑能让你在出问题时快速定位。

HAL库中的DMA配置要点

在HAL库里,DMA的配置主要靠HAL_DMA_Init()函数。你需要填一个DMA_InitTypeDef结构体。我挑几个关键字段说说:

DMA_HandleTypeDef hdma;
hdma.Instance = DMA2_Stream0;          // 选Stream
hdma.Init.Channel = DMA_CHANNEL_0;     // 选通道
hdma.Init.Direction = DMA_PERIPH_TO_MEMORY;  // 方向
hdma.Init.PeriphInc = DMA_PINC_DISABLE;      // 外设地址不增
hdma.Init.MemInc = DMA_MINC_ENABLE;          // 内存地址递增
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;  // 外设8位
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;     // 内存32位
hdma.Init.Mode = DMA_NORMAL;              // 普通模式(非循环)
hdma.Init.Priority = DMA_PRIORITY_HIGH;   // 高优先级
hdma.Init.FIFOMode = DMA_FIFOMODE_ENABLE; // 开启FIFO
hdma.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL; // 1/2阈值
HAL_DMA_Init(&hdma);

注意:当PeriphDataAlignment和MemDataAlignment不一致时,必须开启FIFO模式。否则HAL库会返回错误。我刚开始不知道这个限制,折腾了半天才发现是FIFO没开。

避坑指南:我踩过的三个DMA坑

  1. Stream冲突:两个外设用了同一条Stream,同时请求时数据错乱。解决方案:每个Stream只分配给一个外设。
  2. FIFO阈值设置不当:阈值设得太高(比如3/4),导致外设数据溢出。建议从1/2开始调。
  3. 中断优先级:DMA传输完成中断的优先级设得太低,导致CPU来不及处理,数据被覆盖。我一般把DMA中断优先级设到次高。