STM32 UART + DMA + 空闲中断使用中的帧错误(FE)问题及解决方案
STM32 UART + DMA + IDLE中断使用中的帧错误(FE)问题及解决方案
在我调试STM32H7串口空闲中断DMA接受时遇到了一个bug,这个现象发生在系统刚上电时,有个串口由于帧错误FE挂起了中断,之后在HAL_UART_IRQHandler这个全局中断处理函数结束后,所有的中断使能标志位都被清除了,经过反复调试发现以下问题:
- 上电初始化后,串口的 帧错误标志(FE) 会被意外触发,导致系统进入中断。
- 在中断处理中,由于 EIE(错误中断使能位) 被清除,导致帧错误的处理没有进入预期的
HAL_UART_ErrorCallback
函数。 - 当 DMA 被停止时,空闲中断功能(
IDLEIE
)也被清除,后续数据接收无法正常触发空闲中断。
这是一个隐藏较深的问题,目前尚无直接的参考案例。
问题分析
通过对 HAL 库源码和中断流程的分析,发现问题的根源如下:
-
帧错误(FE)触发中断:
-
帧错误通常发生在上电阶段,可能是由于串口初始化前的干扰或接收了无效数据。
-
帧错误会触发中断,但由于 HAL 中的错误处理逻辑,
UART_EndRxTransfer
会清除EIE
和IDLEIE
,导致后续的错误无法触发中断。HAL_UART_IRQHandler中的UART_EndRxTransfer函数导致所有中断使能标志位被清除
进入这个函数的原因是因为我是用DMA来传输数据,如果这时候发生了任何错误,都会进入这个函数,看看这个函数原型
/** * @brief End ongoing Rx transfer on UART peripheral (following error detection or Reception completion). * @param huart UART handle. * @retval None */ static void UART_EndRxTransfer(UART_HandleTypeDef *huart) { /* Disable RXNE, PE and ERR (Frame error, noise error, overrun error) interrupts */ ATOMIC_CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE_RXFNEIE | USART_CR1_PEIE)); ATOMIC_CLEAR_BIT(huart->Instance->CR3, (USART_CR3_EIE | USART_CR3_RXFTIE)); /* In case of reception waiting for IDLE event, disable also the IDLE IE interrupt source */ if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE) { ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE); } /* At end of Rx process, restore huart->RxState to Ready */ huart->RxState = HAL_UART_STATE_READY; huart->ReceptionType = HAL_UART_RECEPTION_STANDARD; /* Reset RxIsr function pointer */ huart->RxISR = NULL; }
可以看到确实是这个函数清掉了中断使能标志位,由于这个是库函数,我们不好直接对其改动,这样整个代码的可移植性会降低,所以需要换一种方式
-
EIE 被清除:
- 在 HAL 的
UART_EndRxTransfer
函数中,错误中断使能位(EIE
)被清除,这是设计上的默认行为,但会导致后续错误无法触发HAL_UART_ErrorCallback
函数 -
DMA 与空闲中断的联动:
- DMA 停止时,HAL 会清除相关中断使能位(包括
IDLEIE
),而空闲中断的触发依赖于IDLEIE
,导致无法检测数据接收完成。
解决方案
为解决上述问题,采用以下方法:
1. 修改中断处理函数
在 UART 中断处理函数中(例如 UART4_IRQHandler
),针对帧错误(FE
)的特殊处理:
- 检测
FE
标志位。 - 停止当前的 DMA。
- 手动恢复错误中断使能位(
EIE
)。
代码如下:
void UART4_IRQHandler(void)
{
/* USER CODE BEGIN UART4_IRQn 0 */
if (__HAL_UART_GET_FLAG(&huart4, UART_FLAG_FE) != RESET)
{
// 停止 DMA 接收
HAL_UART_DMAStop(&huart4);
// 恢复错误中断使能
ATOMIC_SET_BIT(UART4->CR3, USART_CR3_EIE);
}
/* USER CODE END UART4_IRQn 0 */
HAL_UART_IRQHandler(&huart4);
/* USER CODE BEGIN UART4_IRQn 1 */
/* USER CODE END UART4_IRQn 1 */
}
2. 修改错误回调函数
在 HAL 库的错误回调函数 HAL_UART_ErrorCallback
中:
- 针对
FE
标志,清除帧错误标志位。 - 重置接收状态
- 重新启用空闲中断(
IDLEIE
),以确保后续接收正常工作。 - 使用
HAL_UARTEx_ReceiveToIdle_DMA
重新启动 DMA 接收。
代码如下:
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == UART4)
{
if (huart->ErrorCode & HAL_UART_ERROR_FE)
{
// 清除帧错误标志
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_FEF);
// 重置接收状态
huart->RxState = HAL_UART_STATE_READY;
// 重新启用空闲中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
}
// 重新启动 DMA 接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, rxdata3, sizeof(rxdata3));
}
}
3. 防止错误标志干扰
在系统初始化时,提前清除 UART 的所有错误标志,确保上电过程中不会因干扰触发错误中断:
__HAL_UART_CLEAR_FLAG(&huart4, UART_CLEAR_FEF | UART_CLEAR_OREF | UART_CLEAR_NEF);
完整解决流程
- 检测和恢复中断配置:
- 在中断处理函数中手动恢复
EIE
和其他相关标志位。 - 错误回调中清理状态:
- 在
HAL_UART_ErrorCallback
中清除错误标志,并重新启用 DMA 和空闲中断。 - 初始化时预处理:
- 上电后立即清除所有挂起的错误标志,避免误触发中断。
优化建议
为了提升代码复用性,可以将错误恢复处理提取成独立函数:
void HandleUARTFrameError(UART_HandleTypeDef *huart)
{
// 停止 DMA
HAL_UART_DMAStop(huart);
// 清除错误标志
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_FEF);
// 恢复错误中断使能
ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_EIE);
// 恢复空闲中断
}
然后在 UART4_IRQHandler
中调用:
if (__HAL_UART_GET_FLAG(&huart4, UART_FLAG_FE) != RESET)
{
HandleUARTFrameError(&huart4);
}
效果验证
通过上述修改,可以确保:
- 帧错误发生后,错误回调函数(
HAL_UART_ErrorCallback
)能够正常触发。 - 在错误恢复后,DMA 和空闲中断功能继续正常工作,后续数据传输不受影响。
作者:Jossen CIANG