一、为什么需要帧尾超时判定?
串口不定长接收通常采用 DMA + 空闲中断(IDLE) 方案:DMA自动搬运数据,空闲中断在总线空闲时触发,通知CPU取走数据。但空闲中断的触发条件是一帧数据结束后总线保持空闲一个字符时间(取决于波特率),这带来两个问题:
- 粘包:若两帧数据间隔小于一个字符时间,空闲中断不会触发,两帧被合并。
- 断帧:若一帧数据内部因干扰出现短暂空闲,可能被误判为帧结束。
因此,仅靠空闲中断无法可靠判定帧尾,需要引入超时机制:在收到数据后启动定时器,若超时时间内无新数据,则判定帧结束。
二、三种超时判定策略详解
策略一:固定超时(软件延时/计数器)
原理:在空闲中断或DMA传输完成中断中,启动一个软件定时器(如SysTick计数),设定固定超时值(如5ms)。每次收到新数据时重置计时,超时后置位帧结束标志。
实现要点:
- 使用一个全局变量记录最后接收时间戳。
- 在主循环或定时器中断中轮询检查时间差。
- 超时值需大于最大帧间隔,通常取3~10个字符时间。
代码示例:
#define FIXED_TIMEOUT_MS 5
volatile uint32_t last_rx_tick = 0;
volatile uint8_t frame_ready = 0;
// 在DMA传输完成中断或空闲中断中调用
void on_data_received(void) {
last_rx_tick = HAL_GetTick();
}
// 在SysTick中断或主循环中检查
void check_timeout(void) {
if (!frame_ready && (HAL_GetTick() - last_rx_tick > FIXED_TIMEOUT_MS)) {
frame_ready = 1; // 帧结束
}
}
优点:实现简单,资源占用少。 缺点:超时值固定,无法适应不同波特率或动态帧间隔;高波特率下可能误判,低波特率下响应慢。
策略二:动态超时(基于波特率计算)
原理:根据当前波特率计算一个字符的传输时间,超时值设为N个字符时间(如3.5个字符,参考Modbus标准)。每次收到数据时重置计时器。
实现要点:
- 字符时间 = (起始位+数据位+校验位+停止位) / 波特率。
- 超时值 = 字符时间 × 3.5(可配置)。
- 使用硬件定时器(如TIM)产生精确超时。
代码示例:
// 假设波特率115200,8N1,则字符时间 = 10/115200 ≈ 86.8us
// 3.5字符时间 ≈ 304us,取整为300us
#define CHAR_TIME_US 87
#define TIMEOUT_CHARS 3.5
#define TIMEOUT_US ((uint32_t)(CHAR_TIME_US * TIMEOUT_CHARS))
void on_data_received(void) {
__HAL_TIM_SET_COUNTER(&htim2, 0); // 重置定时器
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器
}
// 定时器超时中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim == &htim2) {
frame_ready = 1;
HAL_TIM_Base_Stop_IT(&htim2);
}
}
优点:适应不同波特率,精度高,符合工业标准。 缺点:需要额外硬件定时器;波特率变化时需重新计算;对低波特率长帧可能超时过短。
策略三:硬件定时器+空闲中断协同(推荐)
原理:结合空闲中断和硬件定时器,空闲中断触发后启动定时器,若定时器超时前无新数据则判定帧结束;若期间有新数据(通过DMA计数变化检测),则重置定时器。
实现要点:
- 空闲中断中记录当前DMA接收长度,启动定时器。
- 定时器超时后,比较DMA当前长度与记录值,若不变则帧结束。
- 使用DMA半传输/传输完成中断辅助检测数据更新。
代码示例:
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t last_dma_cnt = 0;
volatile uint8_t frame_ready = 0;
// 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart == &huart1) {
last_dma_cnt = Size;
__HAL_TIM_SET_COUNTER(&htim3, 0);
HAL_TIM_Base_Start_IT(&htim3);
}
}
// 定时器超时中断
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim == &htim3) {
uint16_t current_cnt = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
if (current_cnt == last_dma_cnt) {
frame_ready = 1; // 无新数据,帧结束
HAL_TIM_Base_Stop_IT(&htim3);
} else {
last_dma_cnt = current_cnt;
__HAL_TIM_SET_COUNTER(&htim3, 0); // 有新数据,重置超时
}
}
}
优点:兼顾实时性与可靠性,避免误判;适用于高波特率和大数据量场景。 缺点:实现稍复杂,需协调DMA计数与定时器。
三、三种策略对比
| 策略 | 精度 | 资源占用 | 适用场景 | 缺点 | |------|------|----------|----------|------| | 固定超时 | 低 | 极低 | 低速、简单协议 | 适应性差 | | 动态超时 | 高 | 中(需定时器) | 固定波特率、工业协议 | 波特率变化需重算 | | 硬件协同 | 高 | 中 | 高速、大数据量、复杂环境 | 实现复杂 |
四、注意事项
- DMA缓冲区管理:使用循环模式时注意覆盖风险,建议双缓冲或及时拷贝。
- 中断优先级:空闲中断和定时器中断优先级需合理设置,避免丢失事件。
- 超时值选择:根据实际帧间隔调整,过小导致断帧,过大导致响应延迟。
- 错误处理:增加溢出检测和错误标志清除,防止DMA卡死。
- 测试验证:用逻辑分析仪或串口助手模拟不同间隔数据,验证策略有效性。
五、总结
固定超时适合快速原型,动态超时适合标准协议,硬件协同策略在可靠性和实时性上最优。实际项目中推荐策略三,结合空闲中断与硬件定时器,通过DMA计数变化动态重置超时,既能应对高速数据流,又能避免粘包断帧。掌握这三种策略,你就能根据项目需求灵活选择,打造稳定的串口通信框架。
暂无评论,来抢沙发~