第4章 DMA传输配置实战:使用STM32CubeMX配置DMA(以USART RX为例)
好,咱们直接进入正题。上一章讲了DMA的基本原理,这一章就来点实在的——用STM32CubeMX把USART RX的DMA传输配出来,再分析HAL库背后到底干了什么。
我个人习惯是:能自动生成绝不动手写。CubeMX生成的初始化代码虽然啰嗦,但胜在规范、不容易漏配置。你想想看,手动配DMA要翻寄存器手册、算地址对齐、搞优先级……稍不留神就踩坑。所以,咱们先走一遍CubeMX的图形化配置流程。
4.1 使用CubeMX配置USART RX DMA
打开CubeMX,选好芯片型号(我以STM32F407为例)。新建工程后,按以下步骤操作:
- 使能USART:在Pinout & Configuration标签页,找到USART1,Mode选Asynchronous。波特率我一般设115200,数据位8、无校验、1停止位。
- 添加DMA:在USART1的Configuration选项卡里,找到DMA Settings,点击Add。通道选择DMA2 Stream2(不同芯片不一样,F4上USART1_RX对应这个),方向选Peripheral To Memory。
- 配置DMA参数:Mode选Normal(循环模式后面讲环形缓冲区时再开),Increment Address勾上Memory,Peripheral不勾。Data Width两边都选Byte——串口数据就是字节流。
- 生成代码:Project Manager里填好工程名、路径,Toolchain选MDK-ARM或STM32CubeIDE。点Generate Code。
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通道了。
4.3 HAL库DMA传输完整流程
咱们以HAL_UART_Receive_DMA()为例,看看数据从外设到内存的完整路径。我画了一张流程图,帮你理清脉络。
流程看起来简单,但有几个细节值得注意:
- 传输长度:
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的价值所在。