摘要
串口不定长数据接收是嵌入式常用功能,单纯接收中断无法判定帧结束。本文结合项目实战,精简给出空闲中断、定时器超时、帧头帧尾状态机三种主流方案,代码极简、可直接移植,同时说明选型与常见问题,适合快速落地使用。
一、通用基础定义
统一缓冲区与标志位,所有方案共用:
#define RX_BUF_LEN 256
uint8_t rx_buf[RX_BUF_LEN];
uint16_t rx_len = 0;
uint8_t frame_ok = 0;
二、方案一:串口空闲中断(STM32 首选,最简)

  1. 串口初始化(开启接收 + 空闲中断)
    void USART1_Init(uint32_t baud)
    {
    GPIO/USART/NVIC 基础配置省略
    USART_ITConfig(USART1, USART_IT_RXNE | USART_IT_IDLE, ENABLE);
    USART_Cmd(USART1, ENABLE);
    }

  2. 中断服务函数
    void USART1_IRQHandler(void)
    {
    // 接收字节
    if(USART_GetITStatus(USART1, USART_IT_RXNE))
    {
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    if(rx_len < RX_BUF_LEN)
    rx_buf[rx_len++] = USART_ReceiveData(USART1);
    }
    // 总线空闲 = 一帧接收完成
    if(USART_GetITStatus(USART1, USART_IT_IDLE))
    {
    (void)USART_GetStatus(USART1);
    (void)USART_ReceiveData(USART1); // 空闲中断标准清中断方式
    if(rx_len > 0) frame_ok = 1;
    }
    }

  3. 主循环调用
    int main(void)
    {
    USART1_Init(9600);
    while(1)
    {
    if(frame_ok)
    {
    // 此处解析 rx_buf[0] ~ rx_buf[rx_len-1]
    rx_len = 0;
    frame_ok = 0;
    }
    }
    }
    特点:无额外外设、代码最少;短间隔连发数据会粘帧。
    三、方案二:接收中断 + 定时器超时(通用兼容方案)
    无空闲中断的芯片 / 场景使用,依靠超时判定帧结束。
    1、 定时器 + 串口初始化(精简)
    void TIM_Out_Init(void) // 5ms定时中断
    {
    // 基础分频、中断配置省略
    TIM_Cmd(TIM2, DISABLE);
    }
    2、串口中断
    void USART1_IRQHandler(void)
    {
    if(USART_GetITStatus(USART1, USART_IT_RXNE))
    {
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    if(rx_len < RX_BUF_LEN)
    rx_buf[rx_len++] = USART_ReceiveData(USART1);

     TIM_SetCounter(TIM2, 0);
     TIM_Cmd(TIM2, ENABLE); // 收到数据,重置并开启定时器
    

    }
    }
    3、 定时器中断(超时判帧)
    void TIM2_IRQHandler(void)
    {
    if(TIM_GetITStatus(TIM2, TIM_IT_Update))
    {
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    TIM_Cmd(TIM2, DISABLE);
    if(rx_len > 0) frame_ok = 1;
    }
    }
    特点:通用性最强;占用 1 个定时器,超时时间按需调整。
    四、方案三:帧头帧尾状态机(自定义协议,最稳定)
    自主定义通信协议场景使用,示例:帧头0xAA 0x55、帧尾0x0D 0x0A。
    #define HEAD1 0xAA
    #define HEAD2 0x55
    #define END1 0x0D
    #define END2 0x0A

typedef enum {WAIT_HEAD, REC_DATA, WAIT_END} RxState;
RxState rx_sta = WAIT_HEAD;
uint8_t tmp_dat;

void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE))
{
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
tmp_dat = USART_ReceiveData(USART1);

    switch(rx_sta)
    {
        case WAIT_HEAD:
            if(tmp_dat == HEAD1) rx_sta = REC_DATA;
            break;
        case REC_DATA:
            if(tmp_dat == HEAD2) { rx_len = 0; }
            else if(tmp_dat == END1) { rx_sta = WAIT_END; }
            else if(rx_len < RX_BUF_LEN) { rx_buf[rx_len++] = tmp_dat; }
            break;
        case WAIT_END:
            if(tmp_dat == END2) frame_ok = 1;
            rx_sta = WAIT_HEAD;
            break;
        default: rx_sta = WAIT_HEAD; break;
    }
}

}
特点:抗干扰、不粘帧;需要上下位机统一协议。
五、方案选型 & 常见问题
选型建议
STM32 常规项目 → 空闲中断(首选)
芯片无空闲中断 / 通用 MCU → 定时器超时
自研协议、工业强干扰 → 帧头帧尾状态机
常见问题
丢字节:中断内只做缓存,解析放到主循环,提高中断优先级;
空帧 / 误触发:增加 rx_len > 0 判断过滤;
帧粘连:调大帧间隔 / 缩短定时器超时时间。
六、总结
三套代码均做精简处理,去掉冗余配置,可直接嵌入现有工程。核心原则:中断只收数据,业务解析移出中断,是串口长期稳定运行的关键。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐