当前位置: 首页 > article >正文

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

正文大家好我是bug菌~在早年玩单片机的时候最开始接触到的通信协议基本上都是串口通信协议了吧那时候拿到一个通信需求无非想着怎么设计一个不错的通信协议然后写出来一套惊艳的解析算法在实践过程中你肯定遇到过粘包/断帧的问题而且你还参考过非常多的别人的项目协议解析部分写得像一团乱麻层层嵌套的if-else全局变量满天飞加一个新字段就要动半片代码出了问题根本无从排查一旦出现干扰导致数据错位整个接收链路就会彻底瘫痪只能采用重启大法。其实随着编程经验的丰富这些解析过程基本上都是一个有限状态机(FSM)所以直接套着写就行了~1太失败如下这种代码我相信大家在早期入门的时候都是这么干的甚至有些工作好几年的工程师还采用这种办法去处理void uart_rx_isr(uint8_t data) { static uint8_t buf[64]; static uint8_t cnt0; if(data 0xAA cnt0) { // 起始符1 buf[cnt] data; } elseif(data 0x55 cnt1) { // 起始符2 buf[cnt] data; } elseif(cnt2) { // 长度 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); } cnt0; } else { cnt0; // 错误重置 } }代码看起来似乎挺整洁的其实禁不起推敲遇到一些粘包或者断帧要么就把有用部分给扔了要么就永远卡在了中间状态只能复位。代码的可复用性也非常差比如改动一点协议似乎整个逻辑都需要重写~这里的设计问题在于这么硬的解析方式把时序逻辑和业务逻辑混在了一起徒增复杂度。而状态机的核心思想就是把复杂的时序过程拆解为若干个独立、互斥的状态每个状态只处理一件事根据当前输入决定下一个状态。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

相关文章:

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

正文大家好,我是bug菌~在早年玩单片机的时候,最开始接触到的通信协议基本上都是串口通信协议了吧,那时候拿到一个通信需求无非想着怎么设计一个不错的通信协议,然后写出来一套惊艳的解析算法,在实践过程中你肯定遇到过…...

CentOS 7最小化安装后,复制粘贴和网络配置的保姆级教程(附图形界面切换)

CentOS 7最小化安装后的生存指南:从零配置到高效开发环境搭建刚完成CentOS 7最小化安装的新手用户,往往会陷入一种"手足无措"的状态——既无法从宿主机复制粘贴命令,又无法连接网络更新系统。这种困境就像被丢进一个没有工具的荒岛…...

Transformer模型推理性能实测:PyTorch+A10 GPU与MLX+Apple Silicon对比

1. 项目概述与背景最近在部署几个基于Transformer的NLP服务时,遇到了一个经典的选择题:是继续沿用我们团队熟悉的PyTorch NVIDIA GPU方案,还是尝试拥抱苹果生态,用MLX框架在Mac上跑推理?这个问题在团队内部引发了不小…...

从华为EulerOS到openEuler:一个国产操作系统的开源之路与社区生态

从华为EulerOS到openEuler:一个国产操作系统的开源之路与社区生态在开源软件的世界里,每一个成功项目的背后都有一段独特的故事。当华为决定将其内部使用的EulerOS操作系统开源为openEuler时,这不仅是一个技术决策,更是一次关于开…...

DYNAMIX:基于强化学习的动态批处理优化,破解分布式训练效率与精度困局

1. 项目概述与核心痛点在分布式机器学习(DML)的实际部署中,有一个参数总是让工程师们又爱又恨,那就是批处理大小(Batch Size)。它不像学习率那样有丰富的理论指导,也不像网络结构那样有清晰的演…...

纯前端到底要不要学 Java

最近被问了好几次:纯前端有没有必要学 Java。这问题其实没有标准答案,得看你现在在做什么、后面想往哪走。如果你平时的工作就是调 RESTful 接口、拿数据渲染页面,后端全给你包好了,那 Java 不学完全没问题。把 React、Vue 这些前…...

脉冲神经网络在工业预测性维护中的低功耗应用

1. 脉冲神经网络在工业预测性维护中的低功耗革命在工业物联网(IIoT)领域,设备健康监测一直面临着能耗与精度的双重挑战。传统振动监测方案需要将高分辨率数据上传云端分析,不仅产生巨大通信开销,更限制了电池供电设备的续航能力。我们团队最近…...

双线性系统与RNN架构演进:从理论到实践

1. 双线性系统基础与RNN架构演进 双线性系统作为控制理论中的重要模型类别,其数学本质是状态变量与控制输入的乘积项构成的动态系统。这类系统在形式上可以表示为: dx/dt Ax Bu Nxu y Cx Du其中Nxu项就是典型的双线性耦合项。这种结构在保持线性系…...

Google I/O 2026 | 开发者主题演讲精华集锦

作者 / Google I/O 团队AI 已不再只是提供辅助,而是迈向了能够在整个工作流中独立处理复杂任务的智能体阶段。在今年的 I/O 大会上,我们发布了 Gemini 3.5 系列模型,并升级了我们的 "智能体优先" 式开发平台 Antigravity&#xff0…...

RTX51多任务环境下printf安全调用方案解析

1. RTX51多任务环境下printf的安全调用方案在RTX51实时操作系统中,多个任务同时调用标准库函数printf时会出现"多重调用警告"(Warning 15: MULTIPLE CALL TO SEGMENT)。这个看似简单的调试输出问题,实际上涉及RTOS任务调度、函数重入、内存管理…...

手把手教你用Linux命令‘偷看’UEFI启动日志,排查系统启动失败问题

实战指南:用Linux命令深度解析UEFI启动日志当你的Linux系统卡在启动界面,或是反复重启无法进入桌面时,那种焦虑感每个运维人员都深有体会。UEFI启动过程就像一场精心编排的交响乐,任何一个环节出错都可能导致系统启动失败。本文将…...

别再乱删了!一文理清Unity工程里Assets、Library等6个核心文件夹的作用与关系

Unity工程目录深度解析:从Assets到UserSettings的完整指南在Unity开发过程中,工程目录结构就像一座精心设计的建筑,每个文件夹都有其特定的功能和存在意义。对于刚接触Unity的开发者来说,理解这些文件夹的作用和相互关系&#xff…...

Unity WebGL项目内存爆了别慌!用Profiler揪出2048大贴图,5分钟搞定优化

Unity WebGL内存优化实战:用Profiler精准定位2048大贴图当Unity WebGL项目在浏览器中运行时突然弹出"Out Of Memory"错误,不少开发者会感到手足无措。这种内存溢出问题往往源于未被注意到的资源"巨无霸"——比如一张20482048的高清贴…...

不止于播放:用Unity Video Player的RenderTexture模式,轻松实现游戏内电视、监控屏效果

超越基础播放:用Unity VideoPlayer打造沉浸式动态屏幕效果在游戏开发中,环境细节往往是区分平庸与卓越作品的关键。想象一下:玩家走进一个废弃的安全屋,墙上的监控屏幕闪烁着模糊的画面;或是科幻基地中,数据…...

别再为Unity视频播放发愁了!Video Player从创建到避坑,保姆级教程带你搞定

Unity视频播放全攻略:从基础配置到高级避坑技巧在游戏开发中,视频播放功能看似简单,却暗藏诸多玄机。无论是开场动画、过场剧情还是UI背景,流畅的视频体验直接影响玩家第一印象。本文将带你深入Unity Video Player的每一个细节&am…...

CVE-2025-48976:Apache Commons FileUpload 协议解析层内存崩溃漏洞深度解析

1. 这个漏洞不是“上传文件被黑了”,而是整个解析逻辑崩了Apache Commons FileUpload 是 Java 生态里最老牌、最被信任的文件上传处理库之一,从 2003 年发布第一个稳定版起,它就稳稳地嵌在 Struts2、Spring MVC(早期)、…...

UE5 RPG实战:告别旧输入系统,用增强输入(Enhanced Input)优雅触发你的技能

UE5 RPG开发实战:用增强输入系统重构技能触发逻辑在虚幻引擎5的RPG开发中,输入管理一直是困扰中高级开发者的痛点。当角色拥有数十个技能、多种状态(步行、骑马、施法等)时,传统的输入系统往往导致代码臃肿、难以维护。…...

告别卡顿!用IL2CPP优化你的Unity游戏:性能提升与包体瘦身实测

告别卡顿!用IL2CPP优化你的Unity游戏:性能提升与包体瘦身实测最近在优化一款Unity游戏时,我发现了一个令人头疼的问题:游戏在低端设备上频繁卡顿,包体大小也超出了预期。经过一番探索,我决定尝试将脚本后端…...

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

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

Unity异步编程新选择:用R3和NuGetForUnity搞定响应式事件流(附AOT兼容性测试)

Unity异步编程新选择:R3与NuGetForUnity的深度实践指南引言:为什么我们需要更好的事件处理方案?在Unity开发中,事件驱动编程早已成为构建复杂交互系统的核心范式。从传统的UnityEvent到协程(Coroutine),再到曾经风靡一…...

Godot 4.2 2D游戏开发:用TileMap图层一键搞定游戏地图的可行走区域

Godot 4.2 2D游戏开发:用TileMap图层一键搞定游戏地图的可行走区域在2D游戏开发中,地图设计往往是最耗时的环节之一。传统方法需要开发者手动绘制碰撞体或编写复杂的导航逻辑,而Godot 4.2的TileMap导航层功能彻底改变了这一局面。想象一下&am…...

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

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

告别手动拼图!用Unity TileMap的Fill Box和Picker工具,5分钟搞定复杂地形

告别手动拼图!用Unity TileMap的Fill Box和Picker工具高效构建复杂地形在2D游戏开发中,地形设计往往是耗时又繁琐的环节。想象一下,你需要手动放置数百个草地、水域或砖块瓦片来构建游戏世界,这不仅容易出错,还会消耗大…...

避开Unity TileMap新手坑:关于Tile Palette编辑模式的那个‘小星星’到底怎么用?

Unity TileMap深度解析:揭秘Tile Palette编辑模式中‘小星星’的实战应用在Unity的2D游戏开发中,TileMap系统无疑是构建关卡和场景的利器。然而,许多初学者在使用Tile Palette时,常常被左上角那个神秘的‘Edit’按钮和旁边的‘*’…...

Unity 2D游戏地图制作:从零上手Tile Palette的7个核心工具(附快捷键清单)

Unity 2D游戏地图制作:从零上手Tile Palette的7个核心工具(附快捷键清单)在独立游戏开发领域,2D游戏因其独特的艺术风格和相对较低的开发门槛,始终保持着旺盛的生命力。无论是复古风格的平台跳跃游戏,还是精…...

UE4.27 + PICO 3 避坑实录:从Android环境配置到VR插件集成的完整流程

UE4.27 PICO 3 开发全流程:从环境搭建到VR部署的深度避坑指南第一次将UE4项目部署到PICO 3的经历,就像在迷宫里摸索——每个转角都可能遇到意想不到的陷阱。作为过来人,我整理了这份涵盖环境配置、SDK集成、插件调试全流程的实战手册&#x…...

Houdini刚体破碎VAT导出到UE5:从静态碎片到动态 Niagara 粒子群的实战转换

Houdini刚体破碎VAT导出到UE5:从静态碎片到动态 Niagara 粒子群的实战转换在影视级实时特效制作中,大规模刚体破碎效果一直是个技术难点。传统方法需要消耗大量计算资源来处理每个碎片的物理模拟,而Vertex Animation Texture(VAT&…...

别再死记硬背了!用‘橡皮筋’和‘电线杆’比喻,5分钟彻底搞懂Unity UI锚点(Anchors)

用生活化比喻破解Unity UI锚点:橡皮筋与电线杆的魔法刚接触Unity UI系统时,那个神秘的四三角锚点控件总让人望而生畏。官方文档里冷冰冰的MinX/MaxY参数,就像一道数学题般令人头疼。但当我偶然发现这两个生活比喻后,一切突然变得清…...

《AI推理优化实战:从高延迟高成本到高效低耗,企业级AI落地必备技术》

随着大模型、AI应用规模化落地,行业发展重心已经从“模型训练”全面转向“模型推理”。2026年AI产业的核心痛点不再是模型训练精度不足,而是推理成本过高、响应延迟过长、算力资源浪费。很多企业落地AI应用时,面临大模型推理速度慢、并发量低…...

告别传统地形!用Unreal Engine的Voxel Plugin手把手教你做可破坏的无限世界(含动态NavMesh配置)

告别传统地形!用Unreal Engine的Voxel Plugin打造可破坏的无限世界在游戏开发领域,地形系统一直是构建虚拟世界的基石。传统Landscape系统虽然成熟稳定,但面对日益增长的玩家对交互性和自由度的需求,静态地形已经难以满足现代沙盒…...