引言
在STM32的串口通信开发中,中断方式接收数据是最常见的方式之一。然而,很多开发者都会遇到一个关键问题:"如何判断一帧数据已经接收完成?" 今天我们就来深入探讨这个问题,并提供几种实用的解决方案。
一、为什么需要判断数据接收完成?
在串口通信中,数据是以字节流的形式传输的。当我们在中断服务函数中每次只接收一个字节时,需要一种机制来判断当前接收的数据是否构成一个完整的消息帧。常见的应用场景包括:
-
接收不定长数据帧
-
解析协议数据包(如Modbus、自定义协议等)
-
处理命令行指令
-
接收传感器数据
二、基本原理
在USART接收中断服务函数中,我们通常会这样开始:
cs
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收到的数据
uint8_t rx_data = USART_ReceiveData(USART1);
// ... 处理数据
}
}
但这里只接收了一个字节,如何知道数据接收完成了呢?
三、四种判断数据接收完成的方法
方法1:超时判断法
这是最常用的方法之一,通过判断相邻两个字节之间的时间间隔来判断数据是否接收完成。
cs
// 定义接收结构体
typedef struct {
uint8_t buffer[256];
uint16_t index;
uint8_t flag;
uint32_t last_time;
} UART_RxTypeDef;
UART_RxTypeDef uart1_rx;
// 中断服务函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 记录当前时间(可以使用SysTick或定时器)
uart1_rx.last_time = SysTick->VAL;
// 存储数据
if(uart1_rx.index < 256)
{
uart1_rx.buffer[uart1_rx.index++] = data;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
// 主循环或定时器中断中检查超时
void Check_UART_Timeout(void)
{
uint32_t current_time = SysTick->VAL;
uint32_t time_diff = abs(current_time - uart1_rx.last_time);
// 如果超过设定的超时时间(如10ms)
if(time_diff > 10000 && uart1_rx.index > 0) // 10ms超时
{
uart1_rx.flag = 1; // 标记数据接收完成
}
}
优点:
-
适用于不定长数据
-
实现相对简单
缺点:
-
需要额外的定时器资源
-
超时时间需要根据波特率调整
方法2:特定帧头帧尾法
这种方法适用于有固定格式的协议。
cs
#define FRAME_HEADER 0xAA
#define FRAME_FOOTER 0x55
typedef enum {
RX_STATE_IDLE,
RX_STATE_HEADER,
RX_STATE_DATA,
RX_STATE_COMPLETE
} RxStateTypeDef;
void USART1_IRQHandler(void)
{
static RxStateTypeDef rx_state = RX_STATE_IDLE;
static uint8_t rx_index = 0;
static uint8_t rx_buffer[256];
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
switch(rx_state)
{
case RX_STATE_IDLE:
if(data == FRAME_HEADER)
{
rx_index = 0;
rx_state = RX_STATE_HEADER;
}
break;
case RX_STATE_HEADER:
rx_buffer[rx_index++] = data;
rx_state = RX_STATE_DATA;
break;
case RX_STATE_DATA:
if(data == FRAME_FOOTER)
{
rx_state = RX_STATE_COMPLETE;
// 数据接收完成处理
Process_Complete_Frame(rx_buffer, rx_index);
}
else if(rx_index < 255)
{
rx_buffer[rx_index++] = data;
}
else
{
// 缓冲区溢出,重置状态
rx_state = RX_STATE_IDLE;
}
break;
default:
rx_state = RX_STATE_IDLE;
break;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
优点:
-
可靠性高
-
适合有固定格式的协议
缺点:
-
数据中不能出现与帧头帧尾相同的字符
-
需要转义机制或使用字节填充
方法3:固定长度法
如果数据长度是固定的,这种方法最简单。
cs
#define FIXED_LENGTH 10
void USART1_IRQHandler(void)
{
static uint8_t rx_buffer[FIXED_LENGTH];
static uint8_t rx_count = 0;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
rx_buffer[rx_count++] = USART_ReceiveData(USART1);
if(rx_count >= FIXED_LENGTH)
{
// 数据接收完成
Process_Complete_Frame(rx_buffer, FIXED_LENGTH);
rx_count = 0;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
优点:
-
实现简单
-
效率高
缺点:
-
只适用于固定长度数据
-
缺乏灵活性
方法4:长度字段法
在数据包中包含长度信息,这是最专业的方法。
cs
typedef struct {
uint8_t header; // 帧头
uint8_t length; // 数据长度
uint8_t data[255]; // 数据
uint8_t checksum; // 校验和
} UART_FrameTypeDef;
void USART1_IRQHandler(void)
{
static uint8_t rx_state = 0;
static uint8_t rx_length = 0;
static uint8_t rx_count = 0;
static uint8_t rx_buffer[256];
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
switch(rx_state)
{
case 0: // 等待帧头
if(data == 0xAA)
{
rx_state = 1;
rx_count = 0;
}
break;
case 1: // 获取长度
rx_length = data;
if(rx_length > 0 && rx_length <= 255)
{
rx_state = 2;
}
else
{
rx_state = 0; // 长度错误,重新开始
}
break;
case 2: // 接收数据
rx_buffer[rx_count++] = data;
if(rx_count >= rx_length)
{
rx_state = 3;
}
break;
case 3: // 接收校验和
if(Verify_Checksum(rx_buffer, rx_length, data))
{
// 数据接收完成且校验通过
Process_Complete_Frame(rx_buffer, rx_length);
}
rx_state = 0;
break;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
优点:
-
灵活,支持变长数据
-
可靠性高
缺点:
-
实现相对复杂
-
需要处理异常情况
四、实战建议
1. 结合使用多种方法
在实际项目中,我推荐结合使用超时判断和协议解析。例如:
-
使用超时机制作为安全保障
-
使用协议解析作为主要判断依据
2. 使用DMA+IDLE中断(高级方法)
对于STM32的高端型号,可以使用DMA配合IDLE中断,这是最高效的方法:
cs
// 启用IDLE中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
void USART1_IRQHandler(void)
{
// 接收中断
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// DMA自动接收,无需在此处理
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
// IDLE中断 - 检测到总线空闲
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
// 清除IDLE中断标志(先读USART_SR,再读USART_DR)
volatile uint32_t temp = USART1->SR;
temp = USART1->DR;
(void)temp;
// 获取DMA接收的数据长度
uint16_t rx_len = BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
if(rx_len > 0)
{
// 处理接收到的数据
Process_Complete_Frame(dma_buffer, rx_len);
// 重置DMA
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, BUFFER_SIZE);
DMA_Cmd(DMA1_Channel5, ENABLE);
}
}
}
3. 错误处理
不要忘记处理通信错误:
cs
if(USART_GetITStatus(USART1, USART_IT_ORE) != RESET ||
USART_GetITStatus(USART1, USART_IT_NE) != RESET ||
USART_GetITStatus(USART1, USART_IT_FE) != RESET)
{
// 处理溢出、噪声、帧错误
USART_ClearITPendingBit(USART1, USART_IT_ORE | USART_IT_NE | USART_IT_FE);
// 重置接收状态
Reset_Rx_State();
}
五、性能优化建议
-
使用双缓冲区:一个用于接收,一个用于处理,避免数据竞争
-
合理设置超时时间:根据波特率调整,通常为3-5个字符时间
-
避免在中断中长时间处理:只做必要的标志设置,数据处理放在主循环
-
使用RTOS的消息队列:在中断中发送消息,在任务中处理
总结
判断USART接收数据是否完成有多种方法,选择哪种方法取决于:
-
数据格式(固定长度/可变长度)
-
协议要求
-
系统资源
-
可靠性要求
对于大多数应用,超时判断法 和长度字段法 的组合是最佳选择。对于高性能要求,DMA+IDLE中断是不二之选。