一、为什么需要帧尾超时判定?
串口通信中,一帧数据往往长度不定,且没有固定结束符。STM32的USART外设提供空闲中断(IDLE):当总线在接收完一个字节后保持空闲(无新起始位)达1个字符时间,即触发IDLE标志。配合DMA,可实现“数据自动搬运+帧结束通知”的高效接收。
但IDLE中断的触发存在滞后性:它发生在总线空闲后,而非最后一个字节接收完成的瞬间。对于高实时性场景,仅靠IDLE可能无法精确判定帧尾;且当帧间隔极短或数据量很大时,IDLE可能被延迟响应。因此,需要结合超时机制来可靠判定帧尾。
二、三种策略原理与对比
策略1:纯空闲中断(IDLE Only)
原理:DMA循环/正常模式接收,使能IDLE中断。每次IDLE触发后,读取DMA剩余计数,计算已接收长度,拷贝数据并重启DMA。
优点:实现简单,CPU占用极低。 缺点:无法处理“帧内超时”场景(如对方发送一半后停顿),且IDLE响应受中断优先级影响,极端情况下可能丢失帧尾判定。
适用场景:数据帧连续发送、帧间间隔明显的低速通信。
策略2:定时器辅助超时(IDLE + TIM)
原理:在IDLE中断中启动一个单次定时器(如3.5个字符时间),若定时器超时前没有新数据到达,则判定帧结束;若期间收到新数据(通过DMA计数变化或接收中断),则重置定时器。
优点:可灵活设置超时阈值,适应不同波特率;能处理帧内停顿。 缺点:需要额外定时器资源,中断频繁时增加CPU负荷。
适用场景:Modbus RTU等有明确帧间隔要求的协议。
策略3:DMA传输完成+空闲中断(DMA TC + IDLE)
原理:配置DMA为正常模式,缓冲区大小等于最大帧长。当DMA传输完成(TC)或IDLE触发时,均视为帧结束候选,结合软件状态机判定。
优点:双重保障,避免IDLE丢失;适合长帧。 缺点:缓冲区需按最大帧长分配,内存占用大;TC中断可能早于实际帧尾(若帧长恰好等于缓冲区)。
适用场景:固定最大长度的协议,或对可靠性要求极高的场合。
三、配置步骤(以STM32F4 HAL库为例)
3.1 硬件初始化
- 使能USART和DMA时钟,配置GPIO为复用功能。
- USART参数:波特率115200,8N1,使能接收。
- DMA配置:外设到内存,循环模式(策略1/2)或正常模式(策略3),数据宽度字节,优先级高。
3.2 中断配置
- 使能USART的IDLE中断(
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE))。 - 策略2需配置一个基本定时器,单次模式,超时时间=3.5字符时间(如115200bps下约0.3ms)。
- 策略3需使能DMA传输完成中断。
3.3 关键代码实现
策略1:纯空闲中断
#define BUF_SIZE 256
uint8_t rx_buf[BUF_SIZE];
volatile uint16_t rx_len = 0;
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
rx_len = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 处理数据 rx_buf[0..rx_len-1]
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE); // 重启
}
}
策略2:IDLE + 定时器超时
TIM_HandleTypeDef htim6;
volatile uint8_t frame_ready = 0;
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 启动单次定时器,超时时间=3.5字符
__HAL_TIM_SET_COUNTER(&htim6, 0);
HAL_TIM_Base_Start_IT(&htim6);
}
}
void TIM6_IRQHandler(void) {
if (__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE)) {
__HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
HAL_TIM_Base_Stop_IT(&htim6);
// 判定帧结束
HAL_UART_DMAStop(&huart1);
rx_len = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
frame_ready = 1;
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
}
策略3:DMA TC + IDLE
volatile uint8_t dma_tc_flag = 0;
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
if (!dma_tc_flag) { // 避免重复处理
HAL_UART_DMAStop(&huart1);
rx_len = BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 处理数据
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
dma_tc_flag = 0;
}
}
void DMA2_Stream2_IRQHandler(void) {
if (__HAL_DMA_GET_FLAG(huart1.hdmarx, __HAL_DMA_GET_TC_FLAG_INDEX(huart1.hdmarx))) {
__HAL_DMA_CLEAR_FLAG(huart1.hdmarx, __HAL_DMA_GET_TC_FLAG_INDEX(huart1.hdmarx));
dma_tc_flag = 1;
// 可在此直接处理或等待IDLE
}
}
四、注意事项与避坑指南
- DMA计数器读取时机:必须在停止DMA后读取,否则计数器可能继续变化。
-
IDLE标志清除:需先读SR再读DR,或使用HAL宏
__HAL_UART_CLEAR_IDLEFLAG。 -
缓冲区对齐:DMA缓冲区建议使用
__attribute__((aligned(4)))确保对齐。 - 中断优先级:IDLE中断优先级应高于定时器,避免超时误判。
- 策略2超时时间:通常取3.5个字符时间(Modbus规范),需根据波特率计算。
- 策略3内存开销:缓冲区大小应大于最大帧长,避免TC过早触发。
- 循环模式陷阱:策略1/2使用循环DMA时,若数据超过缓冲区会覆盖,需确保缓冲区足够大或及时处理。
五、选型建议
- 简单应用:策略1足够,代码量最少。
- 协议严格(如Modbus):策略2最可靠,超时时间可精确控制。
- 高可靠性长帧:策略3双保险,但注意内存与TC误判。
实际项目中,可结合策略2与策略3:IDLE触发后启动定时器,定时器超时或DMA TC均判定帧结束,形成多重保障。
暂无评论,来抢沙发~