news 2026/5/25 6:08:11

状态机设计模式优雅的进行通信解包~

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
状态机设计模式优雅的进行通信解包~


正文


大家好,我是bug菌~

在早年玩单片机的时候,最开始接触到的通信协议基本上都是串口通信协议了吧,那时候拿到一个通信需求无非想着怎么设计一个不错的通信协议,然后写出来一套惊艳的解析算法,在实践过程中你肯定遇到过粘包/断帧的问题,而且你还参考过非常多的别人的项目,协议解析部分写得像一团乱麻,层层嵌套的if-else,全局变量满天飞,加一个新字段就要动半片代码,出了问题根本无从排查,一旦出现干扰导致数据错位,整个接收链路就会彻底瘫痪,只能采用重启大法。

其实随着编程经验的丰富,这些解析过程基本上都是一个有限状态机(FSM),所以直接套着写就行了~

1

太失败

如下这种代码我相信大家在早期入门的时候都是这么干的,甚至有些工作好几年的工程师还采用这种办法去处理:

void uart_rx_isr(uint8_t data) { static uint8_t buf[64]; static uint8_t cnt=0; if(data == 0xAA && cnt==0) { // 起始符1 buf[cnt++] = data; } elseif(data == 0x55 && cnt==1) { // 起始符2 buf[cnt++] = data; } elseif(cnt==2) { // 长度 buf[cnt++] = data; } elseif(cnt < buf[2]+3) { // 数据 buf[cnt++] = data; } elseif(cnt == buf[2]+3) { // 校验 if(checksum(buf, cnt) == data) { process_packet(buf, cnt); } cnt=0; } else { cnt=0; // 错误,重置 } }

代码看起来似乎挺整洁的,其实禁不起推敲,遇到一些粘包或者断帧,要么就把有用部分给扔了,要么就永远卡在了中间状态只能复位。代码的可复用性也非常差,比如改动一点协议,似乎整个逻辑都需要重写~

这里的设计问题在于这么硬的解析方式,把"时序逻辑"和"业务逻辑"混在了一起,徒增复杂度。而状态机的核心思想,就是把复杂的时序过程拆解为若干个独立、互斥的状态,每个状态只处理一件事,根据当前输入决定下一个状态。

Context (上下文):维护当前状态实例,对外暴露操作,将调用转发给状态对象。

State (抽象状态):定义所有具体状态必须实现的方法。

ConcreteState (具体状态):封装该状态下的行为,并可调用context.setState()进行状态转移。

2

状态抽象

任何基于帧的串行协议,无论格式如何复杂,其解包过程也都基本上可以抽象为以下5个状态:

SYNC_IDLE

这是解包的入口状态,很多人在这里只是简单的判断了一下"当前字节是否等于同步字",相等就进入下一状态,否则丢弃,这样做还是太暴力了,比如说同步字有两个字节0xAA55,当你只收到0xAA就直接丢弃了可不行,正确是的做法是 : 维护一个滑动窗口,逐个字节比对同步字序列。

当收到0xAA时进入"半同步"状态,下一个字节如果是0x55则完全同步;如果不是,则将当前字节作为新窗口的第一个字节继续比对。这样即使同步字被拆分成两个中断接收,也能正确识别,这才是比较健壮的同步字解析。

PARSE_HEAD

同步完成后,进入包头解析状态。包头通常包含长度、地址、命令字等关键信息。这个状态主要是处理包头的合法性(如地址是否匹配、命令字是否支持)和提取数据负载的长度。长度不够继续等待其他数据到来,数据长度超了就继续往后解析。

RECV_DATA

这里其实就是根据解析头中的数据负载长度把数据接受完整,千万不要假设数据会一次性全部到达,一旦接收完整就可以进入校验了。

VERIFY_CRC

这里是最后一道关卡。根据协议要求计算校验值(和校验、CRC16、CRC32等),与包尾的校验字段比对。

这里有些人校验不过就直接把整帧都丢了,其实有些异常情况前面是一些脏数据刚好匹配到了同步字,其实可以回退到SYNC_IDLE状态,从第二个字节开始重新搜索同步字。这样可以最大限度地保留后续数据,避免因单个错误字节导致整个数据流失步。

ERROR/TIMEOUT

这两个异常处理是最容易丢的,他们都会导致状态机重置,只是触发条件不同:

  • ERROR:由明确的错误条件触发(如包头非法、长度越界、校验失败)

  • TIMEOUT:在指定时间内没有收到新的数据,防止状态机永远卡在某个中间状态

3

解包伪代码

下面只是一个简单的伪代码示例供参考。协议相关的逻辑通过回调函数注入,状态机本身不依赖任何具体协议,而且通常是将状态机与环形缓冲区结合使用。

串口中断只负责将数据写入环形缓冲区,而主循环中从环形缓冲区逐个读取字节,喂给状态机就可以了~

1、大致的数据结构是这样的~

// 解包状态枚举 typedefenum { FSM_SYNC_IDLE, // 空闲,搜索同步字 FSM_PARSE_HEAD, // 解析包头 FSM_RECV_DATA, // 接收数据负载 FSM_VERIFY_CRC, // 校验数据 FSM_COMPLETE, // 解包完成 FSM_ERROR // 错误状态 } fsm_state_t; // 协议操作回调函数表 typedefstruct { // 检查是否为同步字,返回同步字长度,0表示不是 uint8_t (*is_sync)(constuint8_t *buf, uint16_t len); // 解析包头,返回数据负载长度,0表示包头错误 uint16_t (*parse_head)(constuint8_t *buf, uint16_t head_len); // 计算校验值,返回0表示校验成功 int (*verify)(constuint8_t *buf, uint16_t total_len); } proto_ops_t; // 解包上下文结构体(核心) typedefstruct { // 状态机相关 fsm_state_t state; // 当前状态 uint16_t sync_len; // 同步字长度 uint16_t head_len; // 包头长度 uint16_t data_len; // 数据负载长度 uint16_t total_len; // 包总长度 uint16_t recv_cnt; // 已接收字节数 uint32_t last_tick; // 最后一次接收数据的时间戳 // 缓冲区相关 uint8_t *rx_buf; // 接收缓冲区指针 uint16_t buf_size; // 缓冲区总大小 // 协议相关 constproto_ops_t *ops; // 协议操作函数表 void (*callback)(constuint8_t *buf, uint16_t len, void *arg); // 解包完成回调 void *arg; // 回调参数 } fsm_parser_t;

2、框架代码

// 初始化解包器 void fsm_parser_init(fsm_parser_t *parser, uint8_t *rx_buf, uint16_t buf_size, const proto_ops_t *ops, void (*callback)(const uint8_t *, uint16_t, void *), void *arg) { parser->state = FSM_SYNC_IDLE; parser->rx_buf = rx_buf; parser->buf_size = buf_size; parser->ops = ops; parser->callback = callback; parser->arg = arg; parser->last_tick = get_system_tick(); } // 状态机核心处理函数,每次从环形缓冲区读取一个字节调用 void fsm_parser_process(fsm_parser_t *parser, uint8_t data) { // 更新时间戳 parser->last_tick = get_system_tick(); switch(parser->state) { case FSM_SYNC_IDLE: ....... // 将字节放入缓冲区 // 检查是否为同步字 call parser->ops->is_sync(); //检测成功转移到下一个状态 parser->state = FSM_PARSE_HEAD; ....... break; case FSM_PARSE_HEAD: ....... // 接续接收数据 // 尝试解析包头,获取数据长度 call parser->ops->parse_head(parser->rx_buf, parser->recv_cnt); ....... //异常检测可以转移到故障状态 parser->state = FSM_ERROR; ....... //检测成功转移到下一个状态 parser->state = FSM_RECV_DATA; break; case FSM_RECV_DATA: ....... parser->rx_buf[parser->recv_cnt++] = data; // 数据接收完成,进入校验状态 if(parser->recv_cnt == parser->total_len) { parser->state = FSM_VERIFY_CRC; } break; case FSM_VERIFY_CRC: ....... if(parser->ops->verify(parser->rx_buf, parser->total_len) == 0) { // 校验成功,调用回调函数 if(parser->callback) { parser->callback(parser->rx_buf, parser->total_len, parser->arg); } parser->state = FSM_COMPLETE; } else { parser->state = FSM_ERROR; } break; default: parser->state = FSM_ERROR; break; } // 错误或完成状态,重置状态机 if(parser->state == FSM_ERROR || parser->state == FSM_COMPLETE) { parser->state = FSM_SYNC_IDLE; parser->recv_cnt = 0; } } // 超时检查函数,定期调用 void fsm_parser_check_timeout(fsm_parser_t *parser, uint32_t timeout_ms) { if(parser->state != FSM_SYNC_IDLE) { if(get_system_tick() - parser->last_tick > timeout_ms) { parser->state = FSM_SYNC_IDLE; parser->recv_cnt = 0; } } }

3、框架的使用

比如你现在要适配具体协议,协议的形式大概是:

只需实现对应的回调函数即可:

// 检查同步字 uint8_t my_proto_is_sync(const uint8_t *buf, uint16_t len) { //检查0xAA 0x55,如果匹配成功则返回1,否则返回0 } // 解析包头 uint16_t my_proto_parse_head(const uint8_t *buf, uint16_t head_len) { } // 校验函数 int my_proto_verify(const uint8_t *buf, uint16_t total_len) { //对应的校验函数 } // 定义协议操作表 constproto_ops_t my_proto_ops = { .is_sync = my_proto_is_sync, .parse_head = my_proto_parse_head, .verify = my_proto_verify };

而且你还可以通过不同的proto_ops_t实例支持多种协议,

以上的代码和框架还是比较简陋的,对于缓冲区可以采用线性数组,也可以采用环形缓冲区,这样的话接收数据直接入缓存区,然后状态入口读取环形缓存区数据即可,这在RTOS中用得比较多,这样可以使得中断上下文执行时间短一点~

4

最后

其实大家只需要有一个思想,从面向过程的ifelse转移到面向状态的状态机思想就可以利用状态机框架去设计程序了,而且每个状态职责单一,逻辑清晰,便于测试和维护~

最后

好了,今天就跟大家分享这么多了,如果你觉得有所收获,一定记得点个点赞、收藏、关注、标星~

bug菌唯一、永久、免费分享嵌入式技术知识平台~

推荐专辑 点击蓝色字体即可跳转

MCU进阶专辑

嵌入式C语言进阶专辑

“bug说”专辑

专辑|Linux应用程序编程大全

专辑|学点网络知识

专辑|手撕C语言

专辑|手撕C++语言

专辑|经验分享

专辑|电能控制技术

专辑 | 从单片机到Linux

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 6:01:00

(干货整理)实测好用的AI写作辅助网站,毕业党收藏备用

毕业季论文写作真的这么难&#xff1f;选题纠结、文献找不全、写到一半卡壳、查重反复修改、格式总出错…… 这份实测推荐的AI论文工具合集&#xff0c;覆盖中英文写作、全流程辅助、专项功能&#xff0c;免费和高性价比都有&#xff0c;从开题到定稿全程护航&#xff0c;毕业生…

作者头像 李华
网站建设 2026/5/25 5:59:05

图机器学习在农药生态毒性预测中的应用与挑战

1. 项目概述&#xff1a;当图机器学习遇见农药设计农药&#xff0c;这个听起来有些“硬核”的词汇&#xff0c;其实是我们现代农业的基石。从除草剂到杀虫剂&#xff0c;它们守护着全球的粮食安全。但硬币的另一面是&#xff0c;农药的生态毒性问题日益凸显&#xff0c;尤其是对…

作者头像 李华