3. HAL库DMA初始化结构体详解:DMA_HandleTypeDef、DMA_InitTypeDef、各成员变量含义与配置

DMA配置,说白了就是告诉硬件三件事:数据从哪来、到哪去、怎么传。HAL库把这三件事封装成了两个结构体——DMA_HandleTypeDef和DMA_InitTypeDef。我刚开始用HAL库时,总觉得这两个结构体长得差不多,容易搞混。后来踩了几次坑才明白,它们的分工其实很清晰。

3.1 两个结构体的角色分工

先看一张图,理清它们的关系:

DMA初始化结构体关系图 DMA_HandleTypeDef 句柄结构体(运行时核心) Instance : DMA_Stream_TypeDef* Init : DMA_InitTypeDef Parent : DMA_HandleTypeDef* State : HAL_DMA_StateTypeDef Lock : HAL_LockTypeDef DMA_InitTypeDef 配置结构体(初始化参数) Channel : uint32_t Direction : uint32_t PeriphInc : uint32_t MemInc : uint32_t PeriphDataAlignment : uint32_t MemDataAlignment : uint32_t 嵌套

简单来说:DMA_HandleTypeDef是句柄,负责整个DMA传输的生命周期管理;DMA_InitTypeDef是配置参数,只负责告诉硬件怎么传。句柄里嵌套了配置结构体,这是HAL库的惯用手法。

3.2 DMA_HandleTypeDef:传输的"总管家"

这个结构体是DMA操作的"总管家"。我习惯把它想象成一个项目经理——它不直接干活,但所有资源调度、状态跟踪、错误处理都归它管。

成员变量 类型 作用
Instance DMA_Stream_TypeDef* 指向具体的DMA流寄存器基地址
Init DMA_InitTypeDef 嵌套的配置结构体,存放所有传输参数
Parent DMA_HandleTypeDef* 用于多DMA级联时的父句柄指针
State HAL_DMA_StateTypeDef DMA状态:就绪、忙、超时、错误等
Lock HAL_LockTypeDef 锁机制,防止重入

重点提醒:Instance成员必须指向正确的DMA流寄存器地址。STM32F4系列有8个DMA流(Stream0~7),每个流对应一组独立的寄存器。选错了流,数据传不到目标外设。

State和Lock这两个成员,平时写简单demo时很少关注。但做产品时它们很关键。我曾经在一个多任务系统中,两个任务同时操作同一个DMA句柄,结果State被覆盖,导致DMA卡死在半传输状态。后来加了互斥锁才解决。

3.3 DMA_InitTypeDef:传输参数的"配置单"

这个结构体才是真正干活的地方。每个成员都对应DMA_SxCR寄存器的一个或几个位域。我建议你配置时对照着参考手册的寄存器描述来看,这样理解更深。

成员变量 可选值(宏定义) 说明
Channel DMA_CHANNEL_0 ~ DMA_CHANNEL_7 选择DMA请求通道,与外设的映射关系固定
Direction DMA_PERIPH_TO_MEMORY
DMA_MEMORY_TO_PERIPH
DMA_MEMORY_TO_MEMORY
数据传输方向
PeriphInc DMA_PINC_ENABLE / DMA_PINC_DISABLE 外设地址是否自增
MemInc DMA_MINC_ENABLE / DMA_MINC_DISABLE 存储器地址是否自增
PeriphDataAlignment DMA_PDATAALIGN_BYTE
DMA_PDATAALIGN_HALFWORD
DMA_PDATAALIGN_WORD
外设数据宽度:字节/半字/字
MemDataAlignment DMA_MDATAALIGN_BYTE
DMA_MDATAALIGN_HALFWORD
DMA_MDATAALIGN_WORD
存储器数据宽度:字节/半字/字
Mode DMA_NORMAL / DMA_CIRCULAR 普通模式/循环模式
Priority DMA_PRIORITY_LOW ~ DMA_PRIORITY_VERY_HIGH DMA流优先级
FIFOMode DMA_FIFOMODE_ENABLE / DMA_FIFOMODE_DISABLE 是否使能FIFO
FIFOThreshold DMA_FIFO_THRESHOLD_1QUARTERFULL ~ DMA_FIFO_THRESHOLD_FULL FIFO触发阈值
MemBurst DMA_MBURST_SINGLE / DMA_MBURST_INC4 / DMA_MBURST_INC8 / DMA_MBURST_INC16 存储器突发传输大小
PeriphBurst DMA_PBURST_SINGLE / DMA_PBURST_INC4 / DMA_PBURST_INC8 / DMA_PBURST_INC16 外设突发传输大小

3.4 关键成员配置经验

3.4.1 Channel:选错了,数据就串门了

每个DMA流可以连接多个外设请求,但同一时间只能选一个通道。比如USART1_TX在DMA2_Stream7的Channel4上。我见过有人配了DMA2_Stream7但忘了配Channel,结果数据跑到SPI那边去了。嗯,查了半天才发现。

3.4.2 Direction:方向错了,传了个寂寞

三个方向:外设→内存、内存→外设、内存→内存。注意内存到内存模式不需要外设触发,配置后立即启动。我刚开始做ADC采集时,把方向配反了,结果DMA一直往ADC的数据寄存器写数据,ADC读数全被覆盖了。

3.4.3 PeriphInc与MemInc:地址自增的学问

外设地址自增一般关闭——比如USART_DR寄存器就一个地址,自增了反而读到别处。存储器地址自增通常开启,除非你只操作一个固定地址的变量。这个逻辑其实很简单,但新手容易搞反。

我的习惯:配完PeriphInc和MemInc后,我会在脑子里模拟一遍地址变化。比如从USART收数据到数组buf[100],外设地址不变,内存地址每次+1。这样跑一遍,基本不会错。

3.4.4 数据对齐:宽度不匹配的坑

外设和存储器的数据宽度可以不同。比如外设是8位(USART数据寄存器),存储器是32位(int数组)。DMA会自动做数据拼接或拆分。但要注意:如果外设是16位,存储器是8位,高8位数据会丢失。我踩过这个坑——用DMA读16位ADC,结果存到8位数组里,数据全被截断了。

3.4.5 Mode:普通还是循环?

普通模式传完就停,适合一次性传输。循环模式传完自动重头开始,适合ADC连续采样、音频播放等场景。但循环模式有个坑:传输完成中断只触发一次,之后不会再进中断。如果你需要在每次循环完成时做处理,得用半传输中断或双缓冲模式。

3.4.6 Priority:优先级不是越高越好

DMA有4级优先级:低、中、高、非常高。同一DMA控制器下的多个流,优先级高的先获得总线访问权。但优先级设太高,会抢占CPU的总线带宽,影响系统实时性。我一般用默认的中等优先级,只在音频或高速数据采集时才提一级。

3.4.7 FIFO与突发传输:性能与风险的平衡

FIFO可以缓冲数据,减少总线访问次数。突发传输一次搬多个数据,效率更高。但这两个功能要配合使用:使能FIFO后才能用突发传输。而且突发传输的大小不能超过FIFO阈值的一半,否则会丢数据。

曾经踩过的坑:我在一个项目中使能了FIFO,设了4次突发,但FIFO阈值设成了1/4。结果FIFO还没填满就触发突发传输,数据错位。后来把阈值改成1/2才正常。记住:突发大小 ≤ FIFO阈值/数据宽度。

3.5 一个完整的配置示例

下面是一个典型的USART接收DMA配置。我习惯把配置写在单独的函数里,方便复用:

void MX_DMA_Init(void)
{
    // 1. 声明句柄
    static DMA_HandleTypeDef hdma_usart1_rx;

    // 2. 使能DMA时钟
    __HAL_RCC_DMA2_CLK_ENABLE();

    // 3. 配置参数
    hdma_usart1_rx.Instance = DMA2_Stream2;          // USART1_RX 使用 DMA2_Stream2
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;     // USART1 映射到 Channel4
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;  // 外设→内存
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;      // USART_DR 地址不变
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;          // 内存地址递增
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;   // USART 8位
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;      // 内存 8位
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;                // 普通模式
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;   // 中等优先级
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;  // 不使用FIFO

    // 4. 调用HAL库初始化函数
    HAL_DMA_Init(&hdma_usart1_rx);

    // 5. 将DMA句柄关联到USART句柄
    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}

注意最后一步__HAL_LINKDMA宏,它把DMA句柄的地址赋值给USART句柄的hdmarx成员。这样USART操作DMA时,就知道该用哪个句柄了。这个宏展开后其实就是一句赋值:huart1.hdmarx = &hdma_usart1_rx。

3.6 初始化流程总结

配置DMA的完整步骤,我总结成四步走:

  1. 声明句柄:定义一个DMA_HandleTypeDef变量,建议用static修饰,避免栈溢出。
  2. 填充配置:给Init结构体的每个成员赋值。拿不准的用默认值。
  3. 调用初始化:HAL_DMA_Init()会把配置写入寄存器。
  4. 关联外设:用__HAL_LINKDMA把DMA句柄挂到外设句柄上。

这四步走完,DMA就准备好干活了。但别急——DMA还没启动呢。启动传输需要调用HAL_DMA_Start_IT()或HAL_DMA_Start(),这个我们后面章节再细讲。

最后说一句:配置结构体里的成员,大部分都有默认值。但Channel和Direction必须显式配置,没有默认值。如果你发现DMA没反应,先检查这两个有没有配。