STM32 USART接收中断:如何判断数据接收完成?

引言

在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();
}

五、性能优化建议

  1. 使用双缓冲区:一个用于接收,一个用于处理,避免数据竞争

  2. 合理设置超时时间:根据波特率调整,通常为3-5个字符时间

  3. 避免在中断中长时间处理:只做必要的标志设置,数据处理放在主循环

  4. 使用RTOS的消息队列:在中断中发送消息,在任务中处理

总结

判断USART接收数据是否完成有多种方法,选择哪种方法取决于:

  • 数据格式(固定长度/可变长度)

  • 协议要求

  • 系统资源

  • 可靠性要求

对于大多数应用,超时判断法长度字段法 的组合是最佳选择。对于高性能要求,DMA+IDLE中断是不二之选。

相关推荐
Joshua-a14 小时前
慢时钟域到快时钟域问题(打拍法)(自用)
单片机·嵌入式硬件
benjiangliu14 小时前
STM32教程-03-STM32总线矩阵和系统框图
stm32·嵌入式硬件·矩阵
报错小能手14 小时前
线程池学习(一) 理解操作系统 进程 线程 协程及上下文切换
学习
pps-key14 小时前
麻雀AI:一个能自己学习交易的智能体
人工智能·学习
YangYang9YangYan14 小时前
2026年大专大数据与会计专业核心证书推荐
大数据·学习·数据分析
炽烈小老头14 小时前
【 每天学习一点算法 2026/01/04】打家劫舍
学习·算法
漏刻有时14 小时前
微信小程序学习实录13:网络PDF文件的下载、本地缓存、预览、保存及主动转发
网络·学习·微信小程序
毛小茛14 小时前
芋道管理系统学习——简介
学习
漏刻有时14 小时前
微信小程序学习实录12:wx.serviceMarket.invokeService接口OCR识别营业执照和银行卡
学习·微信小程序·ocr