第4章 DMA传输配置实战:使用STM32CubeMX配置DMA(以USART RX为例)

好,咱们直接进入正题。上一章讲了DMA的基本原理,这一章就来点实在的——用STM32CubeMX把USART RX的DMA传输配出来,再分析HAL库背后到底干了什么。

我个人习惯是:能自动生成绝不动手写。CubeMX生成的初始化代码虽然啰嗦,但胜在规范、不容易漏配置。你想想看,手动配DMA要翻寄存器手册、算地址对齐、搞优先级……稍不留神就踩坑。所以,咱们先走一遍CubeMX的图形化配置流程。

4.1 使用CubeMX配置USART RX DMA

打开CubeMX,选好芯片型号(我以STM32F407为例)。新建工程后,按以下步骤操作:

  1. 使能USART:在Pinout & Configuration标签页,找到USART1,Mode选Asynchronous。波特率我一般设115200,数据位8、无校验、1停止位。
  2. 添加DMA:在USART1的Configuration选项卡里,找到DMA Settings,点击Add。通道选择DMA2 Stream2(不同芯片不一样,F4上USART1_RX对应这个),方向选Peripheral To Memory。
  3. 配置DMA参数:Mode选Normal(循环模式后面讲环形缓冲区时再开),Increment Address勾上Memory,Peripheral不勾。Data Width两边都选Byte——串口数据就是字节流。
  4. 生成代码:Project Manager里填好工程名、路径,Toolchain选MDK-ARM或STM32CubeIDE。点Generate Code。
小技巧:生成代码前,记得在NVIC Settings里把USART和DMA的中断优先级配好。我一般把DMA优先级设得比USART高一点,避免数据丢失。

4.2 生成的初始化代码分析

代码生成后,打开main.c,你会看到MX_USART1_UART_Init()和MX_DMA_Init()两个函数。咱们拆开看。

4.2.1 DMA初始化函数

void MX_DMA_Init(void)
{
  __HAL_RCC_DMA2_CLK_ENABLE();          // 使能DMA2时钟
  HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0); // 设置中断优先级
  HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);         // 使能中断
}

嗯,这里要注意:CubeMX生成的DMA初始化只开了时钟和中断,并没有配置传输参数。为什么?因为真正的DMA配置是在HAL_UART_Init()里完成的。HAL库把DMA和UART绑定在了一起。

4.2.2 USART初始化中的DMA关联

打开HAL_UART_Init(),你会发现它调用了HAL_UART_MspInit()。这个MspInit函数里,CubeMX帮我们生成了DMA句柄的初始化:

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
  // ... GPIO配置省略 ...
  if(huart->Instance == USART1)
  {
    hdma_usart1_rx.Instance = DMA2_Stream2;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
    HAL_DMA_Init(&hdma_usart1_rx);
    __HAL_LINKDMA(huart, hdmarx, hdma_usart1_rx);
  }
}

看到__HAL_LINKDMA这个宏了吗?它把UART句柄里的hdmarx指针指向了DMA句柄。这样,当你调用HAL_UART_Receive_DMA()时,HAL库就知道该用哪个DMA通道了。

核心理解:HAL库的DMA传输流程是“三层联动”——应用层调用HAL函数,HAL层操作DMA寄存器,DMA硬件自动搬运数据。你只需要配好参数,剩下的交给硬件。

4.3 HAL库DMA传输完整流程

咱们以HAL_UART_Receive_DMA()为例,看看数据从外设到内存的完整路径。我画了一张流程图,帮你理清脉络。

HAL_UART_Receive_DMA() 传输流程 应用层:HAL_UART_Receive_DMA() HAL层:配置DMA传输参数 使能DMA流和中断 DMA硬件:USART DR → 内存缓冲区 传输完成 → DMA中断触发 HAL_UART_RxCpltCallback() 回调 关键点 • 源地址:USART_DR • 目标地址:内存缓冲区 • 传输长度:用户指定

流程看起来简单,但有几个细节值得注意:

  • 传输长度:HAL_UART_Receive_DMA()的第三个参数是接收长度。DMA计数器会从该值递减到0,触发完成中断。如果你不知道数据长度,就不能用Normal模式——这就是为什么后面要讲环形缓冲区。
  • 中断优先级:DMA中断和UART中断是独立的。DMA传输完成中断由DMA控制器产生,而UART的IDLE中断(空闲中断)由UART外设产生。两者可以配合使用。
  • 回调函数:HAL库在HAL_UART_IRQHandler()里判断中断来源,如果是DMA传输完成,就调用HAL_UART_RxCpltCallback()。你只需要重写这个回调函数即可。
我曾经踩过的坑:有一次我忘了在回调函数里重新调用HAL_UART_Receive_DMA(),结果只收到一次数据就停了。Normal模式下,DMA传输完成后通道会自动关闭,必须重新使能才能接收下一包数据。后来我改用循环模式(Circular Mode)才解决了这个问题——当然,那是配合环形缓冲区用的。

4.4 关键配置参数对比

为了让你快速上手,我把几个容易混淆的参数整理成了表格:

参数 选项 说明 我的建议
Mode Normal / Circular Normal传输一次即停;Circular自动循环 不确定数据长度时用Circular
PeriphInc Enable / Disable 外设地址是否递增 USART数据寄存器固定,必须Disable
MemInc Enable / Disable 内存地址是否递增 接收数据必须Enable
Data Alignment Byte / HalfWord / Word 数据传输宽度 串口数据用Byte
Priority Low / Medium / High / Very High DMA流优先级 根据系统负载选择,一般Medium够用

4.5 实战:启动一次DMA接收

配置完成后,在main()函数里启动接收:

uint8_t rx_buffer[128];

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  // 启动DMA接收,接收128字节
  HAL_UART_Receive_DMA(&huart1, rx_buffer, 128);

  while (1)
  {
    // 主循环可以干别的事
  }
}

// 传输完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == USART1)
  {
    // 处理接收到的128字节数据
    // 注意:处理完后需要重新调用HAL_UART_Receive_DMA()
    HAL_UART_Receive_DMA(&huart1, rx_buffer, 128);
  }
}

看到没?就这么几行代码,DMA就开始自动搬运了。CPU可以腾出手来处理其他任务——这才是DMA的价值所在。

个人经验:在实际项目中,我通常不会在回调函数里直接处理数据,而是用消息队列或标志位通知任务层。回调函数里只做最轻量级的操作,避免阻塞中断。