SECS/GEM协议栈源码解析:SECS-II编解码、HSMS通信与GEM状态机
发布时间:2026/9/25 4:53:06来源:尧图网络
简介面向半导体设备与工厂自动化系统集成开发者的SECS/GEM协议源代码工程出自JngHightSpeedSecs完整项目覆盖SECS消息解析与生成、GEM设备端交互模型、事件回调机制、错误处理与日志记录、设备模拟器及测试示例等模块可支撑24小时不间断产线环境下高效稳定的设备接口开发。压缩包共31个文件约722KB以8个头文件、4个cpp实现文件为核心配合5个dll、2个lib运行库及2个exe演示程序另含Visual Studio工程配置与说明文档结构清晰便于直接参考或二次开发。目前已有1102人学习下载。借助该工程开发者可深入理解协议交互细节掌握设备状态上报、命令收发与数据交换的实现路径减少通信异常导致的生产中断适合中高级设备自动化工程师及协议研究人员作为实战参考。1. SECS/GEM 源代码包先搞清楚它解决的是哪一类问题在半导体封测车间设备要接 EAP绕不开 SECS/GEM 这套标准。SECS 负责把报文格式和传输规则定下来GEM 在更高一层管设备状态、事件上报、报警和远程命令。没有它设备商各写各的EAP 侧每接一款设备都要重新实现一套通信协议。这套 JngHightSpeedSecs 源码包正好把 SECS-II 消息编解码、HSMS 通信和 GEM 状态机一次性收敛起来属于 SECSGEM、SECS/GEM 源代码里结构比较完整、注释也跟得上的一类。它适合现场做 SECS/GEM 协议对接、EAP 系统实施部署运维的工程师直接二开也适合想对照 SEMI E5/E30/E37 精读协议原理的研发。它不是按 F5 就跑的演示框是能改、能拆、能上产线的底座。2. 先把协议骨架立住SECS-II 消息编解码与 Transaction 状态机2.1 从 SML 到字节流消息结构可以这样入手联调时一条 SECS 消息通常以 SML 文本形式出现在日志里比如 S1F2 回复S1F2 [2] L A SECS-MODEL-01 A 1.2.0 SML 是给人看的真正在 HSMS 连接上跑的是字节流。一个 HSMS 帧可以拆成三块4 字节长度前缀、10 字节消息头、数据区。消息头里Device ID 占 2 字节Stream 和 Function 各占 1 字节W 位埋在 Stream 字节的最高位PType 和 SType 各占 1 字节最后 4 字节是 System Bytes用来把一次事务的请求和回复对上。PType 固定为 0 表示 SECS-II 数据消息SType 为 0 是数据消息1 到 7 是 HSMS 控制消息。很多人上手时不注意这个区分在 Select 握手阶段就用业务消息去试结果被对端直接 Reject。数据区里的每个数据项都是“格式码 长度 数据”的结构。格式码的高 nibble 表示类型0 是 List、1 是 ASCII、2 是 Binary、4 是 unsigned int、5 是 signed int低 nibble 表示长度字段的规则。规范以 SEMI E5 为准代码里一般会维护这样一张常量表# secs_codec.py —— 简化版 SECS-II 编解码 FMT { L: 0x00, # List A: 0x10, # ASCII B: 0x20, # Binary BOOL: 0x30, U1: 0x04, U2: 0x05, U4: 0x06, U8: 0x07, I1: 0x14, I2: 0x15, I4: 0x16, I8: 0x17, } def _encode_len(size: int) - bytes: # 小于 128 用单字节长度超过则走多字节长度模式 if size 128: return bytes([size]) return b\x81 size.to_bytes(4, big) def encode_item(tag: str, value) - bytes: 把 SECS-II 标签转成字节支持嵌套列表 if tag L: body b.join(encode_item(t[0], t[1]) for t in value) return bytes([FMT[L]]) _encode_len(len(body)) body if tag in (A, B): raw value if isinstance(value, bytes) else value.encode(ascii) return bytes([FMT[tag]]) _encode_len(len(raw)) raw # 数字类型按固定字节数大端打包 size int(tag[1]) return bytes([FMT[tag]]) int(value).to_bytes(size, big)这个简化模型里最值得注意的就是_encode_len。长度小于 128 时单字节就能表达数据项内容超过 128 字节后长度表达规则要切换否则解析端会少读或多读字节导致整帧错位。这是我在多个裁剪版库源码里见过的高频问题。2.2 Transaction 状态机一次请求对应一次回复“SECS 是单向还是双向的”现场被问过很多次。准确说它是双向的而且每个来回都讲究配对。设备侧主动上报 S6F11主机回 S6F12主机侧下发 S2F41设备回 S2F42。一次配对就是一次 Transaction两边靠 System Bytes 关联起来。每个未完成的 Transaction 都该有一个状态机兜底发出 Primary 后进等待状态收到 Reply 后关闭超时进超时态由上层决定是否重发。代码结构一般是这样的# transaction_mgr.py —— 消息事务状态管理 class Transaction: def __init__(self, stream: int, func: int, sysbytes: bytes): self.stream, self.func stream, func self.sysbytes sysbytes self.state WAITING_REPLY self.t3 45.0 def on_reply(self, reply) - bool: # 同一事务的回复只认第一次 if self.state ! WAITING_REPLY: return False self.state COMPLETED return True def on_timeout(self): if self.state WAITING_REPLY: self.state TIMEOUTT3 默认 45 秒这是 SEMI E5 推荐值除非设备商在规格书上特别指定否则不建议改。超时后要不要重发属于业务决策S6F11 这类事件上报丢了可以等下一次S2F41 这种主机命令就必须重发。重发时有一个硬性要求System Bytes 必须和第一次一致否则对端会把同一条命令当成两条处理这是很多“命令被执行了两次”事故的根源。2.3 为什么要动源码现成库和现场需求的差距网上现成的 SECS/GEM 库能跑通基本链路但现场一复杂就露馅。有的不支持多连接EAP 侧热切换直接崩有的把 SECS-I 和 HSMS 混在一起消息体解析慢还有的字节序写死碰上对端指定了不同 Device ID 就抓瞎。这套源码包的优势在于它面向高速通信场景线程模型把解码、心跳、业务处理分开改动有边界不会牵一发动全身。我一般建议保留三个扩展点消息编解码、连接管理、GEM 事件回调。把这三个点通过接口暴露出来现场对接不同设备商时只用改适配层不动核心。这样既避免了把协议栈当成黑匣子用的风险也保留了按规范精读源码做二次定制的空间。3. HSMS 通信层TCP 连接、心跳与端口参数的一次到位配置3.1 从 TCP Socket 到 Select 握手HSMS 的建立流程HSMS 把 SECS 消息跑在 TCP 上常用端口默认 5000也有设备商自定义成 5064具体以设备商配置表为准。TCP 连接建立之后不能立刻传业务消息要先走一遍 Select 握手客户端发起 TCP 连接。客户端发送 Select.req 控制消息SType 为 1。服务端回 Select.rsp状态值为 0 表示成功。连接进入 selected 状态。之后才能发 S1F13/S1F14 或直接用 S1F1 探测设备状态。写接收循环时最容易错的是把 TCP 的一次 read 当成一条完整消息。同一个包里可能装了三条消息也可能一条消息被拆成两个 TCP 包。正确做法是先读满 4 字节长度头再按长度读完整帧def read_frame(sock): hdr _recv_exact(sock, 4) # 长度头一定要读满 4 字节 frame_len int.from_bytes(hdr, big) if frame_len 0 or frame_len MAX_FRAME: raise ProtocolError(非法长度 %d % frame_len) body _recv_exact(sock, frame_len) # 再读消息体 return hdr bodyframe_len统计的是消息头加数据区的总长不含这 4 字节长度前缀。常见错误是把这个长度头也算进消息体长度或者用recv一次返回值直接解析都会让后续所有消息错位。这就是 SECS/GEM 对接里最高发的毛病之一。3.2 心跳与超时参数把 T3/T5/T7/T8 一次设对参数常用值作用T345 s业务消息等待回复超时T55 s连接失败重试间隔T710 sTCP accept 后等待 select 的超时T85 s链路读超时超过判定连接离线Heartbeat30~60 s无业务数据时定期探测链路实测中T8 默认 5 秒非常容易触发误判。业务数据少的机台TCP 层 keepalive 默认要等 2 小时HSMS 又不发心跳连接其实早断了对端并不知道。通常做法是把应用层心跳设为 30 秒读写超时放宽到 10 到 15 秒让链路判断由心跳负责T8 只做兜底。还有一个容易被忽略的点心跳用 S1F1 还是 HSMS 控制消息必须提前跟 EAP 侧对齐。我见过机台侧发 S1F1 保活主机侧在等 HSMS 层的心跳两边都觉得自己活着实际上中间链路已经断了最后靠业务超时才暴露排查耗时很长。3.3 断线重连与多连接管理断线重连推荐指数退避第一次 500ms第二次 1s最多 5s连续重试 10 次后停止并上报错误。不要用固定 1ms 死循环重连一旦对端没起来日志会被刷满EAP 侧 socket 连接数也会被拖垮。重连成功不等于业务恢复。TCP 恢复后HSMS 连接状态还是未 selected必须重新走一遍 Select 握手再根据 GEM 状态机确认设备在线状态。有些源码包重连后直接沿用旧的 Session ID这会让对端拒绝消息。所以重连逻辑里要强制更新 Session ID并把旧事务全部置为超时避免一段消息等回复等到天荒地老。4. GEM 状态机与事件上报从 State Model 到 S6F11 的完整链路4.1 Equipment 状态模型Offline / Online-Remote 的迁移来源GEM 的核心是状态机。现场实施不需要把 SEMI E30 全部状态背下来但至少要能区分以下三个状态设备状态含义转换条件Equipment Offline设备不接收主机指令手动切离线网络断开Online-Local本地操作事件正常上报上电默认状态Online-Remote主机可控制设备收到切换命令且切换成功联调时第一个要确认的就是设备落在哪个状态。有的设备上电后停在 Offline必须由操作人员在 HMI 上切一次 Online有的设备则要求主机先发 S1F13 建立通信再发 OnLine 命令切入 Remote否则只报事件不执行指令。这些行为差异不写进 SEMI 标准散落在各家设备说明书里。接手新机台时先把状态迁移表从文档里抄出来比直接猜命令靠谱得多。4.2 S6F11 事件上报的代码走读事件上报链路通常长这样机台 PLC 产生事件设备服务层调用协议栈接口协议栈把数据打包成 S6F11 发给主机主机回 S6F12 确认。源码里可以封装成这样一个接口def report_event(event_id: int, data_list: list, conn): # data_list 是 [(tag, value), ...]例如 [(U4, 1001), (A, RUN)] body [ (L, [ (U4, event_id), (L, data_list), ]) ] msg SecsMessage(stream6, function11, bodybody) conn.send_primary(msg, wait_replyTrue)这里有个容易踩的坑wait_replyTrue会让调用线程阻塞在 T3 超时上。如果主机实现不标准或者回复丢了事件上报线程会被卡住 45 秒进而堵住后续事件。所以事件上报必须用独立线程池或者把wait_reply改成 False丢事件靠补偿机制兜住。生产环境里事件上报和主业务流程混在同一条线程是设备像死机一样的常见原因。4.3 S2F41 远程命令处理主机下发远程命令时S2F41 的 body 里带的是命令名加参数列表。接收侧的典型处理逻辑是def handle_host_command(cmd_name: str, params: list, machine) - int: # 返回 0 表示成功非 0 表示失败 if cmd_name START_PROCESS: return 0 if machine.start(params) else 1 if cmd_name ABORT_PROCESS: return 0 if machine.abort() else 1 # 未注册命令也要正常回复否则主机等 T3 超时 return 0这个函数最容易被忽视的一点是命令名不认识时也要回 S2F42。如果直接把未知命令丢进异常分支主机侧会出现大量 S2F41 timeoutEAP 甚至会判定设备状态异常。协议交互里“不回复”比“回复错误”严重得多。4.4 Report 与 Link 配置表把事件映射集中管理S2F33/S2F35 负责让主机配置数据上报关系。设备侧需要维护一张事件 ID、Report ID、数据项之间的映射表{ events: { 2001: {name: EVENT_START, report_ids: [RPT01]}, 2003: {name: EVENT_FINISH, report_ids: [RPT03]} }, reports: { RPT01: {data_items: [SVID1, SVID3]}, RPT03: {data_items: [SVID10, SVID11]} } }现场联调前把这张表和对方 EAP 工程师逐条对一遍能省掉大量“为什么事件没上来”“为什么数据项是空的”的沟通成本。注意Report ID 和事件 ID 的命名规则各设备商不统一不要拿上一家机台的配置直接套下一家。5. 避坑指南SECS/GEM 对接调试中的六个经典翻车现场现场翻车不丢人丢人的是同一个坑踩完还踩。下面六条是我在测机、上线和运维阶段反复见到的每条按“现象、原因、解决”列出来遇到类似问题可以直接照方抓药。1. 现象连接建立后EAP 日志反复打 S1F13 timeout每 45 秒一次状态始终起不来。原因通常是 HSMS 连接还没完成 Select 握手就直接发了 S1F13 业务消息或者是端口和 EAP 侧不一致比如机台配 5064、EAP 监听 5000。解决先看网络包确认 Select.rsp 是否返回且状态为 0再核对端口、IP、Device ID 三项配置。S1F13 不是 TCP 连上就能发的它必须排在 Select 之后。2. 现象S6F11 事件消息解析乱码body 数据跟头部对不上。原因是接收循环把 TCP 的每次 read 当成一条完整消息没按“长度头 消息体”拆帧。解决按上一章read_frame的方式先把 4 字节长度头读满再读完整消息体。长度头必须是 4 字节大端不能用 2 字节这是 HSMS 和 SECS-I 在帧结构上的关键区别。3. 现象主机下发 S2F41设备执行了两次同一个动作。原因是 T3 超时后重发了命令但原回复在网络上晚到设备侧先把两次请求都当成新事务处理了。解决接收侧维护一张 System Bytes 去重表字节相同且状态仍然 WAITING 的直接丢弃重发时 System Bytes 必须和第一次保持一致。4. 现象数据项长度超过 4096 后对端解析全部乱掉。原因是编解码层在长度字段上只给了 2 字节溢出后截断。解决长度处理统一走 4 字节大端编码侧长度超过 128 字节就要切换长度表达规则解码侧要按规则解析不能固定读 1 字节。这事看起来基础但真的在外购库里见过不止一次。5. 现象设备 GUI 显示连接正常EAP 侧却判定离线。原因是 TCP 半开连接心跳没有配置。设备看着 socket 还在但对端早超时关闭了。解决两端统一心跳机制建议应用层 30 秒一次读超时放宽到 10 到 15 秒联调时故意拔网线做一次断线测试确认双方都能在 1 分钟内感知链路变化。6. 现象测机时一切正常上线后偶发 Reject。原因通常是重连后 Session ID 沿用旧值或者 Device ID 不匹配。解决把完整消息头打进日志包括 Device ID 和 System Bytes重连后强制更新 Session ID并重新执行 Select 握手。不要只记“连接断开重连成功”这种粗粒度日志协议层的对错只有看消息头才分辨得出来。6. 用模拟器把整套流程跑顺十六进制转储验证的一个硬习惯真机联调之前我习惯先用模拟器把整套源码流程跑一遍。搜“secs/gem 模拟器”能找到不少用起来顺手的是 python-secs2 这类库拉起的轻量调试端配置好监听端口和设备模式之后可以直接作为 TCP 服务端或客户端工作。步骤不复杂先把模拟器起在一个端口上再让源码包里的 HSMS 层主动连过去完成 Select 握手后发 S1F13看到 S1F14 的 COMMACK 为 0接着手动构造一条 S6F11在模拟器侧收下来看 SML 展开再确认 S6F12 自动回出来。这套流程跑通说明编解码、事务管理、心跳三个核心模块没有结构性问题剩下的才是和具体设备的适配问题。很多现场问题看起来像玄学实际上把帧抓出来读一遍立刻清楚。模拟器联跑时有两个点值得验证一是把模拟器设置成不回复 S6F11观察 T3 超时后重发逻辑是否按预期工作二是强行断开 TCP 连接观察重连退避和 Session ID 更新是否符合预期。真正到真机出问题时我一般直接抓十六进制帧来定位。比如下面这帧是一条 S6F1100000000 0000 0024 0000 0001 06 0b 00 00 0000 0001 ...$............对应解读前 4 字节0000 0024是消息总长 360000是 Device ID06是 Stream 60b是 Function 1100是 PType00是 SType最后 4 字节是 System Bytes。如果想用脚本做快速定位可以这样打印消息头def dump_frame(frame: bytes): length int.from_bytes(frame[0:4], big) device_id int.from_bytes(frame[4:6], big) sf frame[6:8] ptype, stype frame[8], frame[9] sysbytes frame[10:14] print(flen{length} dev{device_id} fstream{sf[0] 0x7f:02x} func{sf[1]:02x} fw_bit{sf[0] 7} PType{ptype} SType{stype} fSysBytes{sysbytes.hex()})从前 14 个字节就能判断消息方向、事务标识和是否需要回复再配合 SML 展开的数据区基本可以过滤掉七成以上的协议问题。从那以后我每对接一台新设备都会强制走一遍模拟器跑通、日志转储盯一遍头部、状态迁移表和 EAP 工程师逐条核对确认最后才上真机。这些步骤单独看都不起眼合在一起能省掉现场大量的冤枉时间。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网