ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于C++的SECS/GEM协议栈实现:从SECS-I到HSMS的完整设计

基于C++的SECS/GEM协议栈实现:从SECS-I到HSMS的完整设计 简介一份面向半导体设备自动化开发者的 C 版 SECS/GEM 协议实现源码包覆盖 SECS1、SECS2、HSMS 与 GEM 设备模型可用来构建设备仿真器、主机系统或设备控制器解决晶圆制造场景中设备与上位机之间的通信难题。压缩包共 110 个文件其中 57 个 h 头文件与 52 个 cpp 源文件构成完整工程骨架另有 1 个接口文件整体约 190KB代码结构紧凑便于直接阅读与二次开发。源码模块涵盖 SECS 消息编解码、Socket/串口通信、事务管理、设备模型、事件分发与错误处理等关键环节分层清晰、模块化程度高能够帮助开发者快速理解标准协议的交互流程、状态同步机制以及设备与主机的适配方法并可作为进一步开发量产系统的基础。已有 6967 人学习下载适合需要掌握 SECS/GEM 通信细节的嵌入式工程师、MES 开发人员及相关专业学生。 做半导体设备软件这行的朋友对GEM、SECS1、SECS2、HSMS这几个词肯定不陌生。这四层协议是设备与工厂MES系统之间通信的事实标准也是设备自动化(Equipment Automation)最底层的通信底座。我这次自己动手用C完整实现了一套兼容这几份SEMI标准的协议栈源代码涵盖串口版SECS-I、TCP版HSMS-SS、SECS-II消息编解码以及GEM层基本状态模型和常用消息处理。这篇文章把整套协议栈的整体设计、核心实现和实际调试中踩过的坑都整理出来给准备自研设备端通信软件或者需要和半导体机台做联调的工程师参考。1. 项目概况这套协议栈到底在做什么1.1 为什么放弃商业SDK选择自己写市面上一提到SECS/GEM通信绝大多数设备厂商第一反应就是买商业协议栈比如Cimetrix的Connectivity系列、国内的某些封装驱动。商业方案确实省事拿过来配置一下设备模型就能跑但它有几个问题在设备端场景里很别扭授权费按机台数量收量一大成本完全不低整个协议栈是个黑盒出了问题只能提工单等支持最致命的是不好定制比如设备内部固件要直接集成通信模块、需要在收到配方后立刻做一层业务校验、要对接自研的调度系统这时候被商业库牵着走特别难受。开源方案又是另一个极端。Python、Java的实现偏主机端和上位机工具真正适合嵌到设备控制器里的C实现很少而且大多只覆盖其中一两层协议要么没有GEM状态模型要么SECS-II的数据类型支持不全。所以从半导体设备软件工程师的角度看自己按SEMI标准文档实现一套C协议栈其实是一件性价比很高的事协议本身有公开规范核心难点不在“懂不懂协议”而在工程化过程中的细节取舍。1.2 分层架构把SECS1/HSMS统一到一条传输接口上这套协议栈在架构上严格按四层拆分传输层SECS-I / HSMS、消息层SECS-II、应用层GEM、业务接口层。这样一个设计背后有个很实际的原因——同一套上层业务逻辑要能跑在两种完全不同的物理传输上。老设备、8寸线、某些专用模块还在用RS-232串口跑SECS-I新设备尤其是新建产线基本全部走以太网用HSMS。如果业务层直接绑定传输方式那两套逻辑就要维护两遍C代码里复制粘贴改改的后果谁都懂。所以我把传输层抽象成ITransport接口SECS-I和HSMS各自实现同一套send/receive/open/close语义SECS-II消息编解码完全复用。上层业务面对的是“消息”而不是“串口还是socket”。1.3 为什么最终选了C语言选型其实没什么悬念。设备控制器常见的是Windows工控机或者Linux嵌入式环境C在这两种平台上都能做到一套核心代码、少量平台适配内存管理可控协议栈作为常驻服务可以长时间稳定运行而且和底层设备交互串口、网卡、共享内存、OPC UA等C/C是最顺手的。再加上模板和类型系统的支持编解码层的代码可以写得既安全又高效这一点在后面的实现细节里会体现。2. 传输层核心设计SECS-I与HSMS双栈实现2.1 SECS-I串口半双工下的块传输机制SECS-I在物理上就是一根RS-232串口线半双工、波特率通常9600。协议将一条完整消息拆成一个个Block块每个Block头部10字节数据部分最多244字节最后一个块用E位Endbit标记结束。发送方每发一块必须等接收方的ACK/NACK确认只有收到NACK才需要重传同一块。实现的时候有几个细节特别要命。第一是字节填充串口链路上出现控制字符比如DLE、STX、ETX时必须做转义否则接收方会把数据里的字节误判成帧边界。这一块我建议单独封装一个FrameCodec不要揉进传输层主逻辑里否则后续排查字节错位问题会非常痛苦。第二是一组定时器T1等块确认、T2块间延迟、T3消息间等待、T4互锁超时每个定时器对应独立状态用统一的定时器轮询线程管理。第三是Block编号我见过不少设备在块数超过0x7FFF时的处理完全不对标准里这块有明确的回绕规则但实测很多上位机实现是兼容的自己设备端做严谨些没有坏处。2.2 HSMSTCP全双工、心跳与连接模式HSMS基于TCP/IP全双工、不需要拆块一条SECS-II消息可以整包直接承载。HSMS消息头固定16字节关键字段包括SessionID2字节、SType消息类型0为数据消息1到7为控制消息、PTypeSECS-II消息固定为0、Stream/Function/W-bit组合、以及4字节SystemBytes。控制消息里最常用的就是Select.Request/Select.Response建立会话、Linktest.Request/Linktest.Response链路保活、Separate.Request断开会话。连接模式分主动和被动。我需要强调主动/被动的叫法容易混淆主动方(Active)是发起TCP连接的一方被动方(Passive)是监听等待连接的一方。真正会导致联调失败的点是“谁来发起Select握手”SEMI标准里规定主动方必然发起Select被动方配合响应。实测最典型的报错场景是设备侧把自己配置成被动方但Host也配置成被动方两边都在等网络层通但应用层就是建立不了会话。这个在排障时是首先要确认的。T7/T8心跳参数也值得留意。T7是空闲多久主动发LinktestT8是发了Linktest后多久收不到响应就判定连接断开。有些Host实现的心跳间隔短设备端默认T7调得长会出现Host认为链路断了、设备还浑然不觉的情况。我建议设备端T7初始值就按SEMI E37默认来不做特殊优化等联调时再根据Host侧要求调整。2.3 传输层抽象ITransport接口如何设计看一段简化的接口定义class ITransport { public: virtual ~ITransport() default; virtual bool open(const TransportConfig cfg) 0; virtual void close() 0; virtual bool isConnected() const 0; virtual bool sendBlock(const uint8_t* data, size_t len) 0; virtual void setReceiveCallback(std::functionvoid(const uint8_t*, size_t) cb) 0; };SECS-I的sendBlock负责组帧、填充、加块头、等确认HSMS的sendBlock就是往TCP缓冲区写数据。上层拿到的是完整的SECS-II消息字节流完全不关心它底下的帧格式差异。这个抽象是整个协议栈能保持单一代码链路的核心我强烈建议做传输层时先把这套接口定死不要边写边改否则串口和TCP两个实现会越写越不一样。3. SECS-II消息编解码与SML解析3.1 数据类型与编码规则SECS-II是消息内容的编码标准它定义了一个层级化的Item树结构每个Item由一个Format Byte开头后跟长度信息再跟数据内容。我实现的类型覆盖是这样的Format类型说明0x00List子项列表递归结构0x20Binary无符号字节数组0x30Boolean布尔值数组0x40ASCII字符串0x50JIS-8日文字符集字符串0x60 / 0x70 / 0x80 / 0x90I8 / I1 / I2 / I4有符号整数0xA0 / 0xB0F8 / F4浮点数0xC0 / 0xD0 / 0xE0 / 0xF0U8 / U1 / U2 / U4无符号整数长度编码的规则是如果长度小于128字节直接用Format Byte后的一个字节表示如果超过127字节用0x81后跟2字节长度、0x82后跟3字节长度、0x83后跟4字节长度这种带偏移的形式。这个规则看着简单但实测极容易在“高位字节顺序”上出问题所有长度字段都是大端字节序和网络字节序一致。3.2 DataItem树与编解码器我用一个DataItem类表达所有类型List表示子节点集合其他类型直接在原始字节缓冲区上做解释class DataItem { public: DataType type() const { return type_; } const std::vectorDataItem children() const { return children_; } std::vectoruint8_t encode() const; static DataItem decode(const uint8_t* data, size_t len, size_t consumed); private: DataType type_; std::vectoruint8_t raw_; std::vectorDataItem children_; };编解码器我写成递归函数encode把Item树序列化成字节流decode从字节流还原成Item树。整个回路可以直接用“先encode再decode必须还原出同构树”来做单元测试。这一步是在工程里最有价值的地方协议栈所有层都依赖这段编解码逻辑如果这里有BUG上层GEM的状态机再对也是白搭。3.3 SML解析运行时解析还是代码生成SEMI官方标准的SMLStatement Message List描述文件是人类可读的消息结构定义形如S1F1 W、S1F2 .这类。实际项目中最舒服的开发方式是用SML定义好消息格式然后让工具自动生成C结构体或者运行时解析表。我最终选择了运行时解析因为设备端消息类型不会固定Host侧随时可能要求新增一个自定义Stream消息代码生成虽然效率高、类型安全更强但每次改消息定义都需要重新编译部署对设备现场运维不友好。运行时解析则在启动时加载SML文件把消息模板注册到消息分发器里新增消息只需要改配置文件。代价是调用点需要做一层动态类型检查这在C里用std::any或者std::variant可以控制得很干净。4. GEM应用层状态模型与标准消息处理4.1 状态模型两个维度要分开看GEM标准定义的设备状态我一开始也理解混了。它其实是两个正交的维度控制状态Control State是OFFLINE、LOCAL、REMOTE三态。设备在OFFLINE时不接受远程操作LOCAL是本地操作REMOTE是允许Host远程控制。通信状态Communication State是COMMUNICATING和NOT COMMUNICATING两态。注意一个设备可以同时处于REMOTE和NOT COMMUNICATING这表示它允许远程控制但TCP链路断了报警照样要本地显示。我在实现里建了两个独立状态机用事件驱动的方式切换比如收到S1F1/S1F2建立通信时通信状态切到COMMUNICATING收到S1F13/S1F14时控制状态从OFFLINE/LOCAL切到REMOTE。两个状态机之间不要用全局变量强耦合否则后维护阶段每个状态迁移都要瞻前顾后。4.2 标准消息分发与处理框架消息分发我用了一个非常朴素的注册表using MessageHandler std::functionvoid(const Secs2Message msg); class GemDispatcher { public: void registerHandler(int stream, int function, MessageHandler handler); void dispatch(const Secs2Message msg); private: std::mapstd::pairint, int, MessageHandler handlers_; };收到任何消息先查注册表再回调到具体处理函数。GEM常用的标准消息包括S1F1/F2通信建立、S1F3/F4状态数据查询、S2F13/F14设备变量、S5F1/F2报警上报、S6F11/F12事件上报等。这个注册表模式的工作量集中在一开始的消息类别枚举后面新增消息就是写一个handler函数然后注册扩展性很好。4.3 事件、报警与数据收集GEM层除了被动的消息回复还有主动上报能力。设备侧在某个状态变化时比如腔体温度越限、配方加载完成会主动发S6F11事件报告Host收到后回S6F12。事件IDCEID需要和定义的事件报告表对上而事件报告表是通过S2F33/S2F35由Host动态配置的。这部分实现有个典型的坑设备端如果没实现S2F33/F35Host会认为设备不支持GEM高级功能而拒绝整线认证。所以我实现的GEM层把“动态事件收集”做成了默认开启设备内置一个报告定义表Host通过S2F33下发定义报告、S2F35链接事件报告、S2F37启停数据收集设备端把Host的操作映射到内部表里后续事件上报从表里取配置序列化数据。这块逻辑是纯数据驱动的不涉及业务逻辑写起来稳测试也好测。5. C工程落地从协议栈到可运行Demo5.1 工程结构与关键类整个工程我按库和示例程序拆开目录结构如下secs-gem/ ├── include/ # 对外头文件 │ ├── transport/ # ITransport, Secs1Transport, HsmsSession │ ├── secs2/ # DataItem, Secs2Codec, SmlParser │ └── gem/ # GemStateMachine, GemDispatcher, EventManager ├── src/ ├── tests/ # 单测与消息编解码回环测试 ├── examples/ # 设备端Demo和Host端Demo └── sml/ # 标准消息的SML定义文件关键类除了前面说到的ITransport、DataItem、GemDispatcher还有一个Secs2Message承载完整消息10字节消息头 Item树以及HsmsSession管理TCP连接建立、Select握手、心跳和断线重连。设备端Demo的设计是一个简单的“温控设备”支持Host查询温度、上报温度越限报警这个Demo只用了不到800行业务代码完整演示了从TCP建链到事件上报整条链路。5.2 线程模型与生命周期协议栈线程模型要简单清晰。我用了三个线程发送线程负责消息出站和重传、接收线程负责读串口或socket解帧后交给分发器、定时器线程处理T3/T5/T7/T8等全部超时计时。业务回调不在接收线程里执行而是投递到业务线程的事件队列里防止业务处理阻塞协议栈接收。这个设计花了些功夫但非常值现场跑半年没出现过卡死或消息丢失。连接断开的生命周期管理也要提前规划。HSMS断线后不能立即释放对象因为可能还有未完成的消息事务在等回复。我以HsmsSession为所有权单元断线后先进入“关闭中”状态等未确认事务超时后再真正释放资源这样上层拿到的session指针一直是有效的只是isConnected()返回false。5.3 编译、移植与联调代码在WindowsMSVC和LinuxGCC/Clang双平台编译串口部分用了一个轻量的平台层封装Windows下走CreateFile/ReadFileLinux下走open/tcsetattrsocket部分用标准BSD socket两边差异小。编译建议打开-Wall -Wextra -Werror协议栈代码对警告零容忍能省掉大量隐晦bug。联调环节我先写了一个自己的Host模拟器直接构造SECS-II消息字节流来测设备端再用Python的开源库secsgem作为第三方Host做过一轮互操作测试它支持HSMS主动和被动模式用来验证我这边的消息编码是不是标准。最后找了一台支持GEM的老型号温控器用真实设备跑通了完整的S1F13建立连接、S2F13查询变量、S6F11上报事件流程。三次测试暴露的问题形态完全不同自测偏逻辑第三方库偏标准兼容真机偏边界条件三个环节都不能省。6. 常见问题与排查技巧实录6.1 连接建立类问题最常遇到的就是前面说的主动/被动模式不匹配表现为设备侧TCP已经ESTABLISHED但双方一直不发Select或者设备发了Select之后等不到Select.Response。排查第一步永远是看抓包里的SType和系统字节确认Hex Data里的SessionID、SType、PType是否符合预期。我一开始用自己写的日志函数打印字节流调试后来发现Wireshark对HSMS协议有解析支持能直接把16字节头字段解出来定位效率提升非常明显。另一个高频问题是T6超时。T6是控制消息比如Select的超时计时器如果一方迟迟不响应主动方会重试并最终断开。很多团队的调试日志只记“T6 timeout”就没了然后无从下手。我建议日志里必须把超时的消息头完整打出来包括SessionID、SType、SystemBytes第一时间就知道是哪条控制消息没人理。6.2 消息编解码与数据不匹配问题字节序问题是最阴险的。HSMS的SessionID和SystemBytes、SECS-II的长度字段都是大端一旦某个地方用了小端编码消息发出去逻辑完全错乱但TCP层完全正常。我排查过一个诡异现场Host收到设备上报S6F11后回复S6F12但设备侧一直认为SystemBytes对不上后来发现是我的SystemBytes生成时用了本机小端序写入自作聪明导致全链路对不上。解决方案是编码器里统一封装一个writeU32BE函数禁止任何地方直接memcpy一个整数到字节流。分块重组成问题也是常见来源。设备端如果消息过大比如配方S7F1下载几MB内容HSMS虽然不用拆块但TCP分包是不可避免的接收方必须处理“一条消息分多次到达”和“多条消息一次性到达”两种情况。我的做法是接收循环里维护一个累积缓冲区先解析16字节头拿到消息总长度再等缓冲区够长再整体消费严格避免在循环里直接按recv返回值当作消息边界。6.3 调试经验与工具链日志是协议栈调试的生命线。我最终采用的日志格式是每条入站/出站消息一行包含时间戳精确到毫秒、方向RX/TX、消息名如S6F11、SystemBytes、以及编码后的关键字段摘要。配合一个脚本按SystemBytes排序基本能肉眼追踪一条事务的完整生命周期。加日志的原则是输出关键头和业务摘要不要输出完整Item树否则生产环境下日志量大到没法看。单元测试我额外建议做三件小事一是编解码回环测试随机生成Item树编解码后必须还原二是状态机迁移测试把GEM的每一个合法/非法迁移都写成测试用例三是连接断开恢复测试模拟TCP半开连接、断电重连、对端主动关闭等场景。这三类测试在接入新Host时非常有价值几乎能挡住70%以上的现场兼容性问题。最后再分享一个我自己的体会协议栈实现本身难度没那么高真正花时间的是把各种Host的兼容性边界摸清楚。不同上位机对GEM细节的理解有差异有的Host建链后马上发S1F3不等你S1F1完整确认有的Host对S6F11的回复只给S6F12不带CEID这些都不是标准文档里能学到的。如果你也在做这套东西拿标准实现当基线再准备一套能灵活调参数的测试环境会比咬着文档死磕有效得多。本文还有配套的精品资源点击获取
返回列表