简介:面向半导体设备与工厂自动化系统集成开发者的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 = 'TIMEOUT'T3 默认 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 一次设对
| 参数 | 常用值 | 作用 |
|---|---|---|
| T3 | 45 s | 业务消息等待回复超时 |
| T5 | 5 s | 连接失败重试间隔 |
| T7 | 10 s | TCP accept 后等待 select 的超时 |
| T8 | 5 s | 链路读超时,超过判定连接离线 |
| Heartbeat | 30~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(stream=6, function=11, body=body) conn.send_primary(msg, wait_reply=True)这里有个容易踩的坑:wait_reply=True会让调用线程阻塞在 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 timeout,EAP 甚至会判定设备状态异常。协议交互里“不回复”比“回复错误”严重得多。
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 更新是否符合预期。
真正到真机出问题时,我一般直接抓十六进制帧来定位。比如下面这帧是一条 S6F11:
00000000 0000 0024 0000 0001 06 0b 00 00 0000 0001 ...$............对应解读:前 4 字节0000 0024是消息总长 36;0000是 Device ID;06是 Stream 6,0b是 Function 11;00是 PType,00是 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(f'len={length} dev={device_id} ' f'stream={sf[0] & 0x7f:02x} func={sf[1]:02x} ' f'w_bit={sf[0] >> 7} PType={ptype} SType={stype} ' f'SysBytes={sysbytes.hex()}')从前 14 个字节就能判断消息方向、事务标识和是否需要回复,再配合 SML 展开的数据区,基本可以过滤掉七成以上的协议问题。
从那以后,我每对接一台新设备,都会强制走一遍:模拟器跑通、日志转储盯一遍头部、状态迁移表和 EAP 工程师逐条核对确认,最后才上真机。这些步骤单独看都不起眼,合在一起能省掉现场大量的冤枉时间。希望帮到你。
本文还有配套的精品资源,点击获取