DMA+空闲中断实现STM32串口不定长收包:帧尾超时判定的三种策略对比

一、为什么需要“帧尾超时判定”

串口收包通常有两种方式:

  • 定长收包:长度固定,简单但灵活性差。
  • 不定长收包:长度未知,需要靠“帧间隔”判断一帧结束。

STM32的USART提供IDLE(空闲)中断:当接收线上连续出现一个字节时间(约10个bit)的高电平后触发。配合DMA,可在不占用CPU的情况下完成整帧接收。

但IDLE中断存在两个现实问题:

  1. 触发时刻受波特率影响:115200bps下一个字节约87μs,9600bps下约1.04ms,低波特率时IDLE响应明显滞后。
  2. 总线噪声/多帧粘连:若两帧间隔小于IDLE判定时间,会被合并成一帧;若噪声导致误触发,会截断数据。

因此,仅靠IDLE中断往往不够,需要引入帧尾超时判定策略来补强。

二、DMA+IDLE基础配置

以STM32F4 + HAL库为例,核心步骤:

  1. 使能USART的DMA接收(HAL_UART_Receive_DMA)。
  2. 使能IDLE中断(__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE))。
  3. 在USART中断服务函数中检测IDLE标志,计算已接收长度并重启DMA。
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t  frame_ready = 0;

void UART_StartReceive(void)
{
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        HAL_UART_DMAStop(&huart1);

        uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        rx_len = RX_BUF_SIZE - remain;
        frame_ready = 1;

        HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
        __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    }
    HAL_UART_IRQHandler(&huart1);
}

注意:HAL_UART_DMAStop会关闭DMA,重启前需重新配置;部分系列可用HAL_UART_AbortReceive替代。

三、三种帧尾超时判定策略

策略一:固定超时(软件延时/轮询)

思路:IDLE中断触发后,启动一个固定时长的软件定时(如5ms),超时后认为帧结束。

volatile uint32_t idle_tick = 0;

void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        idle_tick = HAL_GetTick();   // 记录空闲时刻
    }
    HAL_UART_IRQHandler(&huart1);
}

// 主循环中判定
if (idle_tick && (HAL_GetTick() - idle_tick) >= 5)
{
    idle_tick = 0;
    // 处理一帧
}
  • 优点:实现简单,无需额外硬件。
  • 缺点:超时值固定,低波特率下可能误判(一帧未收完就超时),高波特率下延迟大。
  • 适用:波特率固定、帧长较短的场景。

策略二:动态超时(按波特率计算)

思路:根据当前波特率计算“一个字节时间”的若干倍作为超时阈值,动态调整。

// 一个字节时间(ms) = 10 * 1000 / baudrate
uint32_t byte_time_ms = (10 * 1000) / 115200;  // 约0.087ms
uint32_t timeout_ms = byte_time_ms * 3 + 1;    // 3字节余量
  • 优点:自适应不同波特率,精度优于固定超时。
  • 缺点:需在运行时获取波特率;对突发噪声仍敏感。
  • 适用:多波特率切换、帧长中等的通用场景。

策略三:硬件定时器触发(推荐)

思路:用独立定时器(如TIM3)做“单次超时”,IDLE中断中重置定时器,定时器溢出中断中置帧完成标志。

void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        __HAL_TIM_SET_COUNTER(&htim3, 0);
        HAL_TIM_Base_Start_IT(&htim3);   // 重启超时定时器
    }
    HAL_UART_IRQHandler(&huart1);
}

void TIM3_IRQHandler(void)
{
    if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE))
    {
        __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_UPDATE);
        HAL_TIM_Base_Stop_IT(&htim3);
        frame_ready = 1;                 // 帧尾确认
    }
}
  • 优点:超时精度由硬件保证(微秒级),不占用CPU轮询,抗干扰强。
  • 缺点:占用一个定时器资源,配置稍复杂。
  • 适用:高可靠性、多帧粘连、低波特率或长帧场景。

四、三种策略对比

| 策略 | 精度 | CPU开销 | 硬件资源 | 抗干扰 | 适用场景 | |------|------|---------|----------|--------|----------| | 固定超时 | 低 | 中(轮询) | 无 | 弱 | 简单固定波特率 | | 动态超时 | 中 | 中 | 无 | 中 | 多波特率通用 | | 硬件定时器 | 高 | 低 | 1个定时器 | 强 | 高可靠长帧 |

五、注意事项

  • IDLE标志清除顺序:先清IDLE再停DMA,避免漏字节。
  • DMA重启时机:处理完数据后再重启,防止覆盖未处理帧。
  • 缓冲区边界:DMA满中断(HT/TC)也要处理,防止溢出。
  • 中断优先级:USART与定时器中断优先级需合理配置,避免嵌套冲突。
  • 低波特率陷阱:9600bps下IDLE判定约1ms,固定超时若小于该值会误判。

六、选型建议

  • 产品原型/调试阶段:策略一快速验证。
  • 通用工业通信:策略二平衡资源与精度。
  • 高可靠、长帧、多帧粘连场景:策略三为首选。

实际工程中,常将IDLE中断 + 硬件定时器超时组合使用:IDLE负责快速响应,定时器负责兜底确认,二者互补可显著提升串口收包鲁棒性。