FreeRTOS 通信任务设计(3)---基于状态机的串口协议帧解析

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、什么是状态机?

二、状态机逐字节解析原理

三、核心实现代码解析

总结


前言

在上一篇文章中,我们实现了 UART+DMA 环形缓冲 + 空闲中断 + FreeRTOS 流缓冲区 的高效串口接收方案,成功解决了串口通信中的丢包、粘包、高 CPU 占用三大难题。

但流缓冲区只负责安全、稳定地传递字节流 ,它并不关心数据格式,也不保证一次就能读到一整帧完整数据 。这个时候就需要依靠协议解析状态机把这一段段无序的字节流,拼接成正确的协议帖了。


一、什么是状态机?

状态机是一种描述 "系统在不同阶段如何响应输入并发生转变" 的设计方法。它的核心思想是:

系统在任何时刻都处于一个明确的状态,当外部事件(如收到新字节)发生时,系统会根据当前状态和输入,执行相应的操作,并切换到下一个状态。

在没有状态机的情况下,解析逻辑往往是一大段嵌套的if-elseswitch语句,随着协议复杂度增加,代码会迅速变得臃肿、难以维护,且容易遗漏边界条件。而状态机则完美解决了这一问题。

针对我们的通信协议,解析过程可以拆分为一系列有序的状态,每个状态负责解析协议的一个字段。状态流转如下:

复制代码
WAIT_SOF1 → WAIT_SOF2 → VER → MSG_ID → FLAGS → SEQ → LEN_LOW → LEN_HIGH → PAYLOAD → CRC_LOW → CRC_HIGH → RESET
cpp 复制代码
/* 解析状态机定义 */
typedef enum {
    ST_WAIT_SOF1 = 0,   // 等待帧头第一字节 0xAA
    ST_WAIT_SOF2,       // 等待帧头第二字节 0x55
    ST_VER,             // 解析协议版本号
    ST_MSG_ID,          // 解析消息ID
    ST_FLAGS,           // 解析标志位
    ST_SEQ,             // 解析序列号
    ST_LEN_L,           // 解析数据长度低字节(小端)
    ST_LEN_H,           // 解析数据长度高字节(小端)
    ST_PAYLOAD,         // 解析有效载荷
    ST_CRC_L,           // 解析CRC校验低字节
    ST_CRC_H            // 解析CRC校验高字节
} ParseState_t;

二、状态机逐字节解析原理

串口通信的本质是字节流,MCU 收到的数据只是一串连续的字节,解析器无法预知哪一段是一帧完整数据。因此,解析器必须具备从任意字节开始,逐步恢复同步的能力。

状态机逐字节解析正是为了解决这个问题而设计的:

1、每收到一个字节,解析器都会根据当前所处的状态,判断这个字节意味着什么,并执行相应的逻辑。
2、解析器内部始终维护一个 "当前状态" 变量,记录自己解析到协议的哪个位置(如等待帧头、解析版本号、接收 payload 等)。
3、当新的字节到来时,解析器会根据当前状态和字节值,更新内部变量,并切换到下一个合适的状态。

三、核心实现代码解析

cpp 复制代码
static void parser_feed_byte(uint8_t b) {
    switch (s_state) {
        case ST_WAIT_SOF1:
            if (b == 0xAA) {
                s_state = ST_WAIT_SOF2;
            }
            break;

        case ST_WAIT_SOF2:
            if (b == 0x55) {
                // 帧头匹配成功,开始解析并计算CRC
                s_crc = 0xFFFF;
                s_state = ST_VER;
            } else if (b == 0xAA) {
                // 处理连续帧头 0xAA 0xAA 0x55,第二个 0xAA 作为新帧头
                s_state = ST_WAIT_SOF2;
            } else {
                // 帧头错误,回到初始状态
                s_state = ST_WAIT_SOF1;
            }
            break;

        case ST_VER:
            s_ver = b;
            s_crc = crc16_ccitt_update(s_crc, b);
            s_state = ST_MSG_ID;
            break;

        case ST_MSG_ID:
            s_msg_id = b;
            s_crc = crc16_ccitt_update(s_crc, b);
            s_state = ST_FLAGS;
            break;
        
        // ... 中间状态逻辑省略 ...

        case ST_PAYLOAD:
            s_payload[s_payload_idx++] = b;
            s_crc = crc16_ccitt_update(s_crc, b);
            if (s_payload_idx >= s_len) {
                // payload接收完毕,开始接收CRC
                s_state = ST_CRC_L;
            }
            break;

        case ST_CRC_L:
            s_crc_rx_l = b;
            s_state = ST_CRC_H;
            break;

        case ST_CRC_H:
            s_crc_rx_h = b;
            // 对比本地计算的CRC和接收的CRC
            uint16_t crc_rx = (s_crc_rx_h << 8) | s_crc_rx_l;
            if (crc_rx == s_crc) {
                // CRC校验成功,调用帧回调函数
                on_frame_cb(&s_frame);
            }
            // 无论校验成功或失败,都重置状态机,准备下一帧
            parser_reset();
            break;
    }
}

帧头同步与容错

在ST_WAIT_SOF2状态中,除了匹配0x55,还额外处理了连续帧头的情况(如0xAA 0xAA 0x55),第二个0xAA会被当作新帧头,提高了解析器的容错能力。
边解析边计算 CRC

从ST_VER状态开始,每解析一个字节,都会调用crc16_ccitt_update更新本地 CRC 值,无需缓存整帧数据,节省内存开销。
长度校验与缓冲区保护

在解析LEN字段时,会检查解析出的长度是否超过设定的最大值,防止后续 payload 解析时出现缓冲区溢出。
CRC 校验与状态重置

当解析到CRC_H状态时,会对比本地计算的 CRC 值和接收到的 CRC 值。无论校验成功或失败,解析器都会调用parser_reset()重置状态,准备接收下一帧数据。


总结

本文详细讲解了状态机的核心概念与逐字节解析原理,并基于自定义串口通信协议实现了一套鲁棒的解析状态机。通过等待帧头、解析固定字段、接收数据载荷、校验 CRC的完整流程,状态机能够从无序的串口字节流中精准同步、提取并校验一帧完整数据,同时具备帧头容错、异常自动重置等能力,完美解决了串口通信中的粘包、丢包、解析错误等问题。

相关推荐
【ql君】qlexcel3 小时前
可跑在STM32上的EtherCAT主机协议栈
stm32·soem·ethercat·igh·协议栈
m0_502724953 小时前
qt键盘钩子完善
stm32·qt·计算机外设
CinzWS3 小时前
电源管理(上):动态功耗管理与时钟门控——ARMv8的“省电魔法“
嵌入式·芯片验证·原型验证·a53
zzh9204 小时前
20元代做Proteus仿真|51单片机/STM32花样流水灯|心形/圆形/按键切换|从上到下从左到右
stm32·51单片机·proteus
济6174 小时前
FreeRTOS 通信任务设计(4终)----从字节流到有效帧的完美闭环
stm32·嵌入式·freertos
Hello_Embed5 小时前
嵌入式上位机开发入门(二十四):Paho MQTT 嵌入式客户端源码分析
网络·单片机·网络协议·tcp/ip·嵌入式
-Springer-14 小时前
STM32 学习 —— 个人学习笔记11-1(SPI 通信协议及 W25Q64 简介 & 软件 SPI 读写 W25Q64)
笔记·stm32·学习
LN花开富贵14 小时前
【ROS】鱼香ROS2学习笔记一
linux·笔记·python·学习·嵌入式·ros·agv
yrx02030714 小时前
串口空闲中断+DMA接收+环形缓冲区 && 串口DMA发送+环形缓冲区
stm32·单片机