3. HAL库DMA初始化结构体详解:DMA_HandleTypeDef、DMA_InitTypeDef、各成员变量含义与配置
DMA配置,说白了就是告诉硬件三件事:数据从哪来、到哪去、怎么传。HAL库把这三件事封装成了两个结构体——DMA_HandleTypeDef和DMA_InitTypeDef。我刚开始用HAL库时,总觉得这两个结构体长得差不多,容易搞混。后来踩了几次坑才明白,它们的分工其实很清晰。
3.1 两个结构体的角色分工
先看一张图,理清它们的关系:
简单来说: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_MEMORYDMA_MEMORY_TO_PERIPHDMA_MEMORY_TO_MEMORY |
数据传输方向 |
PeriphInc |
DMA_PINC_ENABLE / DMA_PINC_DISABLE |
外设地址是否自增 |
MemInc |
DMA_MINC_ENABLE / DMA_MINC_DISABLE |
存储器地址是否自增 |
PeriphDataAlignment |
DMA_PDATAALIGN_BYTEDMA_PDATAALIGN_HALFWORDDMA_PDATAALIGN_WORD |
外设数据宽度:字节/半字/字 |
MemDataAlignment |
DMA_MDATAALIGN_BYTEDMA_MDATAALIGN_HALFWORDDMA_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的完整步骤,我总结成四步走:
- 声明句柄:定义一个
DMA_HandleTypeDef变量,建议用static修饰,避免栈溢出。 - 填充配置:给
Init结构体的每个成员赋值。拿不准的用默认值。 - 调用初始化:
HAL_DMA_Init()会把配置写入寄存器。 - 关联外设:用
__HAL_LINKDMA把DMA句柄挂到外设句柄上。
这四步走完,DMA就准备好干活了。但别急——DMA还没启动呢。启动传输需要调用HAL_DMA_Start_IT()或HAL_DMA_Start(),这个我们后面章节再细讲。
最后说一句:配置结构体里的成员,大部分都有默认值。但Channel和Direction必须显式配置,没有默认值。如果你发现DMA没反应,先检查这两个有没有配。