第3章 DMA初始化配置:HAL库流程与传输方向详解

DMA初始化,说白了就是告诉DMA控制器三件事:谁传、传给谁、怎么传。HAL库把这套流程封装得很规整,但规整不等于简单。我见过不少工程师,代码能跑就以为配对了,结果一上低功耗模式就翻车。今天咱们把这层窗户纸捅破。

3.1 HAL库DMA初始化标准流程

HAL库的DMA初始化,核心就一个函数:HAL_DMA_Init()。但调用它之前,你得先填好一个结构体——DMA_HandleTypeDef。我个人习惯把初始化拆成五步,每一步都有坑。

DMA初始化五步法

  1. 使能DMA时钟:__HAL_RCC_DMAx_CLK_ENABLE()
  2. 配置DMA_HandleTypeDef结构体:方向、地址、数据宽度、优先级等
  3. 调用HAL_DMA_Init():将配置写入寄存器
  4. 配置中断(可选):HAL_NVIC_SetPriority() + HAL_NVIC_EnableIRQ()
  5. 关联外设句柄:比如__HAL_LINKDMA()宏

嗯,这里要注意:很多人忘了第5步。DMA本身是个独立模块,你得告诉SPI或UART“你的数据由DMA1通道3来搬”。这个关联动作,HAL库用宏来实现。

// 标准初始化示例 - SPI1使用DMA2 Stream3
DMA_HandleTypeDef hdma_spi1_tx;

void MX_DMA_Init(void)
{
    __HAL_RCC_DMA2_CLK_ENABLE();  // 第一步:开时钟
    
    hdma_spi1_tx.Instance = DMA2_Stream3;  // 指定数据流
    hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;  // 指定通道
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;  // 内存到外设
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;  // 外设地址不递增
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;  // 内存地址递增
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;  // 普通模式
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_LOW;
    
    HAL_DMA_Init(&hdma_spi1_tx);  // 写入寄存器
    
    // 关联SPI1句柄
    __HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
}

我的经验:初始化顺序不能乱。先开时钟再配结构体,这是铁律。我曾经在项目里把这两步写反了,结果DMA寄存器写不进去,查了整整半天。

3.2 传输方向配置:三种模式深度解析

DMA传输方向,HAL库用Direction成员控制。一共三种,咱们一个一个说。

方向宏定义 含义 典型场景 地址递增规则
DMA_PERIPH_TO_MEMORY 外设→内存 ADC采集、UART接收 外设不变,内存递增
DMA_MEMORY_TO_PERIPH 内存→外设 SPI发送、DAC输出 内存递增,外设不变
DMA_MEMORY_TO_MEMORY 内存→内存 数据搬移、校验计算 源递增,目标递增

3.2.1 外设到内存(Periph to Memory)

这是最常用的方向。外设数据寄存器地址固定,内存缓冲区地址递增。比如ADC连续采样,每次转换完成,DMA自动把结果搬到数组里。

配置要点:PeriphInc = DMA_PINC_DISABLE,MemInc = DMA_MINC_ENABLE。外设地址不动,内存地址每次传输后加1(或加2/加4,取决于数据宽度)。

避坑指南:我曾经在UART接收时忘了关外设地址递增,结果DMA每次读的地址都往后跳了一个字节,收到的数据全是乱的。UART的DR寄存器只有一个,千万别让它递增。

3.2.2 内存到外设(Memory to Periph)

和上面正好相反。内存地址递增,外设地址固定。典型场景是SPI发送波形数据,或者DAC输出音频。

配置要点:PeriphInc = DMA_PINC_DISABLE,MemInc = DMA_MINC_ENABLE。嗯,你没看错,和外设到内存的配置一模一样。区别在于Direction不同,DMA控制器会自动判断源和目标。

3.2.3 内存到内存(Memory to Memory)

这个模式比较特殊。它不需要外设触发,配置完成后DMA自己就开始搬。常用于数据拷贝、校验计算。

配置要点:PeriphInc = DMA_PINC_ENABLE,MemInc = DMA_MINC_ENABLE。两个地址都要递增。注意,这个模式下Periph其实也是内存地址,只是DMA把它当“外设”看待。

// 内存到内存配置示例
hdma_mem2mem.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma_mem2mem.Init.PeriphInc = DMA_PINC_ENABLE;   // 源地址递增
hdma_mem2mem.Init.MemInc = DMA_MINC_ENABLE;       // 目标地址递增
hdma_mem2mem.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_mem2mem.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
hdma_mem2mem.Init.Mode = DMA_NORMAL;

我的建议:内存到内存模式慎用。它占用总线带宽,会影响CPU执行指令。我一般只在系统空闲时做数据搬移,或者用DMA2(挂在AHB总线上)来减少冲突。

3.3 数据宽度与对齐:细节决定成败

数据宽度,就是DMA每次传输多少字节。HAL库提供三个选项:BYTE(1字节)、HALFWORD(2字节)、WORD(4字节)。

宏定义 宽度 适用场景
DMA_PDATAALIGN_BYTE 8位 UART、I2C、单字节传感器
DMA_PDATAALIGN_HALFWORD 16位 定时器CCR、16位ADC
DMA_PDATAALIGN_WORD 32位 SPI 32位模式、DAC

这里有个关键点:外设数据宽度和内存数据宽度可以不同。比如外设是8位寄存器,内存是32位数组。DMA会自动做数据打包或拆分。

但要注意对齐问题。STM32的DMA要求:源地址和目标地址必须按各自数据宽度对齐。什么意思?如果内存数据宽度是WORD(4字节),那内存地址必须是4的倍数。否则DMA会进入错误状态。

我曾经踩过的坑:定义一个uint8_t数组,然后强制转换成uint32_t指针传给DMA。结果地址不是4字节对齐,DMA传输到一半卡死了。后来我改用__attribute__((aligned(4)))声明数组,问题解决。

// 正确做法:声明对齐的内存缓冲区
__attribute__((aligned(4))) uint8_t buffer[1024];

// 或者用联合体强制对齐
typedef union {
    uint8_t  bytes[1024];
    uint32_t words[256];
} AlignedBuffer;

3.4 知识体系总览

下面这张图,把DMA初始化的核心逻辑串起来了。你对照着看,思路会更清晰。

DMA初始化配置知识体系 HAL_DMA_Init() 标准流程 传输方向配置 数据宽度配置 地址递增配置 三种方向 • 外设→内存 (PERIPH_TO_MEM) • 内存→外设 (MEMORY_TO_PERIPH) • 内存→内存 (MEMORY_TO_MEMORY) 数据宽度与对齐 • BYTE(8位) / HALFWORD(16位) / WORD(32位) • 外设与内存宽度可不同 • 地址必须按宽度对齐 递增规则 • PINC: 外设地址递增 • MINC: 内存地址递增 • 根据方向组合配置 Mode(模式) + Priority(优先级) DMA_HandleTypeDef 配置完成 → HAL_DMA_Init() 别忘了最后一步: __HAL_LINKDMA() 关联外设句柄

3.5 实战配置清单

最后,我整理了一份配置自查清单。每次写DMA初始化代码,我都会对着过一遍。

DMA初始化配置自查清单

  • ☐ DMA时钟是否已使能?
  • ☐ 传输方向是否正确?(外设→内存 / 内存→外设 / 内存→内存)
  • ☐ 外设地址递增是否关闭?(固定地址的外设必须关)
  • ☐ 内存地址递增是否开启?(连续缓冲区必须开)
  • ☐ 外设数据宽度是否匹配外设寄存器宽度?
  • ☐ 内存数据宽度是否匹配缓冲区元素宽度?
  • ☐ 内存缓冲区地址是否按数据宽度对齐?
  • ☐ 传输模式选择是否正确?(NORMAL / CIRCULAR / PFCTRL)
  • ☐ 优先级是否合理?(避免低优先级任务饿死)
  • ☐ 是否通过__HAL_LINKDMA()关联了外设句柄?