
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
❄专栏传送门 :《产品测评专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
前言
在上一篇文章中,我们实现了 UART+DMA 环形缓冲 + 空闲中断 + FreeRTOS 流缓冲区 的高效串口接收方案,成功解决了串口通信中的丢包、粘包、高 CPU 占用三大难题。
但流缓冲区只负责安全、稳定地传递字节流 ,它并不关心数据格式,也不保证一次就能读到一整帧完整数据 。这个时候就需要依靠协议解析状态机把这一段段无序的字节流,拼接成正确的协议帖了。
一、什么是状态机?
状态机是一种描述 "系统在不同阶段如何响应输入并发生转变" 的设计方法。它的核心思想是:
系统在任何时刻都处于一个明确的状态,当外部事件(如收到新字节)发生时,系统会根据当前状态和输入,执行相应的操作,并切换到下一个状态。
在没有状态机的情况下,解析逻辑往往是一大段嵌套的if-else或switch语句,随着协议复杂度增加,代码会迅速变得臃肿、难以维护,且容易遗漏边界条件。而状态机则完美解决了这一问题。
针对我们的通信协议,解析过程可以拆分为一系列有序的状态,每个状态负责解析协议的一个字段。状态流转如下:
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的完整流程,状态机能够从无序的串口字节流中精准同步、提取并校验一帧完整数据,同时具备帧头容错、异常自动重置等能力,完美解决了串口通信中的粘包、丢包、解析错误等问题。