DMA+空闲中断实现STM32串口不定长收包:帧尾超时判定的三种策略对比
一、为什么需要“帧尾超时判定”
串口收包通常有两种方式:
- 定长收包:长度固定,简单但灵活性差。
- 不定长收包:长度未知,需要靠“帧间隔”判断一帧结束。
STM32的USART提供IDLE(空闲)中断:当接收线上连续出现一个字节时间(约10个bit)的高电平后触发。配合DMA,可在不占用CPU的情况下完成整帧接收。
但IDLE中断存在两个现实问题:
- 触发时刻受波特率影响:115200bps下一个字节约87μs,9600bps下约1.04ms,低波特率时IDLE响应明显滞后。
- 总线噪声/多帧粘连:若两帧间隔小于IDLE判定时间,会被合并成一帧;若噪声导致误触发,会截断数据。
因此,仅靠IDLE中断往往不够,需要引入帧尾超时判定策略来补强。
二、DMA+IDLE基础配置
以STM32F4 + HAL库为例,核心步骤:
- 使能USART的DMA接收(
HAL_UART_Receive_DMA)。 - 使能IDLE中断(
__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE))。 - 在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负责快速响应,定时器负责兜底确认,二者互补可显著提升串口收包鲁棒性。
暂无评论,来抢沙发~