STM32 HAL库串口DMA发送卡死?手把手教你排查HAL_UART_Transmit_DMA只能发一次的坑
STM32 HAL库串口DMA发送卡死问题深度排查指南
最近在调试STM32的串口DMA发送功能时,发现一个奇怪的现象:HAL_UART_Transmit_DMA()函数第一次调用能正常工作,但第二次调用就失效了,甚至会导致程序卡死。这个问题困扰了我整整两天,经过反复调试和查阅资料,终于找到了根本原因。本文将分享我的排查思路和解决方案,希望能帮助遇到类似问题的开发者少走弯路。
1. 问题现象与初步分析
当使用STM32CubeMX配置串口DMA发送功能后,调用HAL_UART_Transmit_DMA()发送数据时,第一次发送正常,但第二次调用时会出现以下现象之一:
- 函数直接返回错误状态
HAL_BUSY - 程序进入HardFault异常
- DMA传输没有启动,数据未被发送
关键观察点:
- 使用调试器查看
huart->gState和hdma->State的状态变化 - 检查DMA和串口中断是否正常触发
- 分析HAL库的状态机转换逻辑
提示:在开始调试前,建议先熟悉HAL库中UART和DMA的状态定义,这些状态决定了函数能否被成功执行。
2. HAL库状态机机制解析
HAL库通过状态机来管理外设的工作状态,这是导致HAL_UART_Transmit_DMA()只能调用一次的根本原因。我们需要深入理解两个关键状态变量:
2.1 UART全局状态(gState)
在UART_HandleTypeDef结构体中,gState表示UART的全局状态:
typedef enum { HAL_UART_STATE_RESET = 0x00U, /*!< Peripheral not initialized */ HAL_UART_STATE_READY = 0x20U, /*!< Peripheral Initialized and ready for use */ HAL_UART_STATE_BUSY = 0x24U, /*!< an internal process is ongoing */ HAL_UART_STATE_BUSY_TX = 0x21U, /*!< Data Transmission process is ongoing */ HAL_UART_STATE_BUSY_RX = 0x22U, /*!< Data Reception process is ongoing */ HAL_UART_STATE_BUSY_TX_RX = 0x23U, /*!< Data Transmission and Reception process is ongoing */ HAL_UART_STATE_TIMEOUT = 0xA0U, /*!< Timeout state */ HAL_UART_STATE_ERROR = 0xE0U /*!< Error */ } HAL_UART_StateTypeDef;2.2 DMA状态(State)
DMA_HandleTypeDef中的State表示DMA通道的状态:
typedef enum { HAL_DMA_STATE_RESET = 0x00U, /*!< DMA not yet initialized or disabled */ HAL_DMA_STATE_READY = 0x01U, /*!< DMA initialized and ready for use */ HAL_DMA_STATE_BUSY = 0x02U, /*!< DMA process is ongoing */ HAL_DMA_STATE_TIMEOUT = 0x03U, /*!< DMA timeout state */ HAL_DMA_STATE_ERROR = 0x04U, /*!< DMA error state */ HAL_DMA_STATE_ABORT = 0x05U /*!< DMA process is aborted */ } HAL_DMA_StateTypeDef;状态转换关键点:
- 调用
HAL_UART_Transmit_DMA()时,会检查huart->gState是否为HAL_UART_STATE_READY - 函数执行过程中会将状态改为
HAL_UART_STATE_BUSY_TX - DMA传输完成后,需要在回调函数中将状态恢复为
HAL_UART_STATE_READY
3. 常见问题排查步骤
3.1 CubeMX配置检查
使用STM32CubeMX生成代码时,有几个关键配置项需要特别注意:
DMA流中断使能:
- 在DMA配置界面,确保"NVIC Settings"中勾选了相应的中断
- 特别是传输完成中断(TC)和错误中断(TE)
串口中断使能:
- 在USART配置的NVIC Settings中启用USART全局中断
DMA模式选择:
- 对于发送操作,通常选择"Normal"模式而非"Circular"
- 确保Memory和Peripheral的地址增量设置正确
配置对比表:
| 配置项 | 正确设置 | 错误设置 | 可能导致的问题 |
|---|---|---|---|
| DMA模式 | Normal | Circular | 数据重复发送 |
| 内存地址增量 | Enable | Disable | 只发送第一个字节 |
| 外设地址增量 | Disable | Enable | 数据发送到错误地址 |
| DMA流中断 | Enable | Disable | 状态无法自动恢复 |
3.2 代码实现检查
在用户代码中,需要确保以下几点:
正确实现回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 发送完成处理 } }DMA中断处理:
- 检查
HAL_DMA_IRQHandler()是否被正确调用 - 确保DMA中断优先级设置合理
- 检查
状态恢复机制:
- 在传输完成回调中,HAL库会自动恢复UART和DMA状态
- 如果手动修改了这些状态,可能导致后续传输失败
3.3 调试技巧
当遇到问题时,可以按照以下步骤进行调试:
检查状态变量:
- 在调试器中观察
huart->gState和hdma->State的值 - 确认第二次调用前状态已恢复为READY
- 在调试器中观察
中断断点设置:
- 在DMA传输完成中断和串口发送完成中断处设置断点
- 确认这些中断是否被触发
寄存器检查:
- 查看USART_SR寄存器状态
- 检查DMA相关寄存器配置是否正常
4. 解决方案与最佳实践
根据上述分析,解决HAL_UART_Transmit_DMA()只能调用一次的问题,主要有以下几种方法:
4.1 方法一:确保中断正确配置
这是最推荐的解决方案,完全遵循HAL库的设计理念:
- 在CubeMX中启用DMA流中断和串口中断
- 确保中断服务函数被正确实现和调用
- 让HAL库自动管理状态转换
关键代码片段:
// 在main.c中确保中断优先级配置正确 HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn); HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn);4.2 方法二:手动重置状态
如果由于某些原因无法使用中断方案,可以手动重置状态:
// 在每次发送前强制重置状态 huart->gState = HAL_UART_STATE_READY; hdma->State = HAL_DMA_STATE_READY; HAL_UART_Transmit_DMA(&huart1, data, size);注意:这种方法破坏了HAL库的状态机机制,可能导致竞态条件,只建议作为临时解决方案。
4.3 方法三:使用自定义DMA管理
对于需要更高灵活性的应用,可以绕过HAL库直接操作DMA:
- 自定义DMA配置函数
- 手动控制DMA启动和停止
- 自行管理状态标志
示例代码:
void My_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { // 禁用DMA __HAL_DMA_DISABLE(huart->hdmatx); // 配置DMA参数 huart->hdmatx->Instance->PAR = (uint32_t)&huart->Instance->DR; huart->hdmatx->Instance->M0AR = (uint32_t)pData; huart->hdmatx->Instance->NDTR = Size; // 清除所有DMA标志 __HAL_DMA_CLEAR_FLAG(huart->hdmatx, DMA_FLAG_TCIF7_4 | DMA_FLAG_HTIF7_4 | DMA_FLAG_TEIF7_4); // 启用DMA __HAL_DMA_ENABLE(huart->hdmatx); // 启用UART DMA请求 SET_BIT(huart->Instance->CR3, USART_CR3_DMAT); }5. 深入原理:HAL_UART_Transmit_DMA()工作流程
要彻底理解这个问题,我们需要分析HAL_UART_Transmit_DMA()的内部实现:
状态检查:
- 检查
huart->gState是否为HAL_UART_STATE_READY - 检查
hdma->State是否为HAL_DMA_STATE_READY
- 检查
状态更新:
- 将UART状态设置为
HAL_UART_STATE_BUSY_TX - 将DMA状态设置为
HAL_DMA_STATE_BUSY
- 将UART状态设置为
DMA配置:
- 调用
HAL_DMA_Start_IT()启动DMA传输 - 设置源地址、目标地址和数据长度
- 调用
使能DMA请求:
- 通过设置USART_CR3的DMAT位使能DMA发送
关键点:只有在DMA传输完成中断中,HAL库才会将状态恢复为READY,这就是为什么中断配置如此重要。
在实际项目中,我发现最稳妥的解决方案是严格按照HAL库的设计理念,确保所有相关中断都被正确配置和处理。手动重置状态虽然能临时解决问题,但可能引入其他难以发现的隐患。
