☰
高速SECS/GEM源码解析:半导体设备接入与HSMS传输实战
2026/9/25 1:12:33 网站建设 项目流程

简介:SECS(半导体设备通信标准)与GEM(通用设备模型)是半导体制造设备与fab信息系统之间的核心通信协议,广泛应用于设备端EAP对接与自动化集成。这套名为JngHightSpeedSecs的源码工程面向设备自动化开发人员,提供了可编译运行的MFC示例工程SecsExampleJng,便于从消息收发、HSMS高速通信到GEM交互流程逐层理解协议落地方式;压缩包共31个文件、约722KB,包含h/cpp源码、sln/vcxproj工程配置,以及SuperHighSpeedHSMS.dll、Communication.dll等已编译库和可执行示例程序,另附ReadMe.txt帮助上手,可直接对照学习或用Visual Studio打开工程二次开发。已有1102人学习浏览。代码在SECS消息处理模块划分、事件回调、错误处理与日志记录等方面提供了具体落地参考,可帮助工程师掌握SEMI通信细节,开发出能在7×24小时生产环境中稳定运行的设备接口,减少通信中断对产线的影响,示例程序也能快速验证通信效果,提升开发效率。

1. 高速 SECS/GEM 源码包解决的是产线设备接入的第一公里

半导体封测设备的自动化接口绕不开 SECS/GEM,而“JngHightSpeedSecs_SECSGEM_SECS_SECS源代码”这类标题指向的就是一套能把设备端和主机端跑起来的 SECS/GEM 源代码实现。接调机测机时,最怕的不是消息不会发,而是协议栈黑匣子化:连接时好时坏、事件上报丢帧、心跳一抖整条线断。这套源码要解决的是“连得上、扛得住、查得清”三件事,适用对象是设备厂商的嵌入式工程师、EAP 系统的现场实施和运维人员,以及做 MES 集成的开发。先把协议分层和边界想明白,再动手写代码,现场才不至于反复翻车。

2. 从传输层到应用层:SECS/GEM 协议的职责边界与选型理由

2.1 HSMS 传输层是性能起点:端口 5000、会话 ID 与三种连接状态

SECS/GEM 能跑高速,前提是传输层选对。老设备走 SECS-I,即 RS-232C 半双工,一帧数据最多 244 字节,S2F42 这类带配方或 Trace 数据的大消息经常发到超时。新设备基本都走 HSMS(SEMI E37),也就是 TCP/IP 之上的 SECS 消息服务。HSMS 没有串口那堆流控和奇偶校验,双方建立 TCP 连接后,用“4 字节长度 + 10 字节消息头”做分帧,速率跟网卡走,这才是“高速”二字的底气。

设备端和主机端的连接建立有两种角色。常见做法是设备做 HSMS-Server,在 5000 端口监听,主机作为 HSMS-Client 主动连过来;也有设备做 Client 反向拨号到主机的场景,多见于设备在防火墙后面、只能出站的工厂。两种角色在源码实现上差别很小,只是把 accept 换成 connect,但现场网络配置差别很大,实施前要先确认设备支持哪种。

HSMS 的会话状态分为未选择、选择和通信中。TCP 连接建立后,双方先交换 Select.req/Select.rsp,成功后进入已选择状态,之后才能收发 SECS-II 的数据消息。设备 ID 放在消息头前两个字节的低 14 位,主机靠它区分同一端口后面的多台设备。一台设备可以同时与多个 Host 建多条 TCP 连接,也可以只连一个主机,这是高速场景里做并发设计的基础,后面第 4 章会讲到它怎么变成坑。

2.2 SECS-II 报文结构:消息头十个字节和 SxFy 消息语义

SECS-II(SEMI E5)定义的是“消息内容”的统一格式。一条消息由 10 字节消息头和后面的数据项组成,数据项按格式码嵌套成树形结构,类似 TLV 但更灵活。消息头的字段分布如下:

偏移长度字段说明
02会话 IDbit15 为 W 位,低 14 位是设备 ID
21流号Stream,1~127
31功能号Function,0~255
42头号/块号HSMS 下一般填 0
64系统字节事务 ID,用于配对请求和响应

SxFy 是 SECS 里的核心寻址方式。S1F1 是“主机问设备是谁”,S1F2 是设备应答身份信息;S1F13/S1F14 用于建立通信;S6F11 是事件上报,S2F41/S2F42 是远程命令收发。数据项的类型由格式码标识,常见的有 List、Binary、ASCII、Boolean、U1/U2/U4 等,可以层层嵌套,例如 S6F11 的 Data 字段经常是<L <B 事件ID> <L 子项...>>。

很多人容易把 SECS 理解成“主机下发指令给设备”的单向协议,实际不是。SECS 是双向的,设备可以主动上报事件和告警,主机也可以随时请求数据。后面做数据采集时,很多业务逻辑本质是两边约定“谁来发哪个 SxFy、回哪个 SxFy”,再配合数据项格式做解析,理解了双向性,源码才不会写偏。

2.3 GEM 补上的标准行为:状态机、事件收集和远程命令

只有 SECS-II 还不够接产线,因为语法之外还有“行为”需要统一。SEMI E30/GEM 把这套行为模板化,包括设备状态机、事件收集、告警管理、远程命令、配方管理和数据采集。标题里的“GEM 源代码”指的就是实现这一整套状态转换和回调逻辑的代码,而不是单纯收发消息。

GEM 状态机是重点。设备有一个“控制状态”(Control State):OFFLINE、ONLINE、PAUSED,还有一个“设备状态”(Device State):DISABLE、ENABLE。设备在 ENABLE 下才能正常处理主机命令,离线状态收到上线请求要能自动转入 Online。事件收集则要求设备支持主机动态配置:主机用 S2F37 启停事件,设备按 S6F11 主动上报。远程命令走 S2F41 收参数、S2F42 返回执行结果。

选型时注意 SECS/GEM 的实现深度差异。有些设备只做了 SECS-II 消息收发,没做 GEM 状态机,这种设备接 MES 时,行为逻辑全写在 Host 端,项目后期痛不欲生。我更倾向在源码层面把 GEM 状态做成固定枚举,而不是现场临时补,否则状态一跳错,连日志都无从查起。

3. 手写一套可复现的 SECS/GEM 最小源码:连接、编解码和心跳

3.1 从零起 HSMS 服务端:监听、握手和设备 ID 校验

用 Python 写最小 HSMS 服务端不算复杂。先监听 5000 端口,收到连接后处理 Select.req,回 Select.rsp,把连接状态标记为已选择。HSMS 控制消息的类型码放在 10 字节消息头的字节 2-3,Select.req 是 0x0001,Select.rsp 是 0x0002,Linktest.req 是 0x0007,Linktest.rsp 是 0x0008。

import socket import struct import threading # HSMS 控制消息类型码 CTL_SELECT_REQ = 0x0001 CTL_SELECT_RSP = 0x0002 CTL_LINKTEST_REQ = 0x0007 CTL_LINKTEST_RSP = 0x0008 # HSMS 消息头: 会话ID(2) + 类型/流功能(2) + 保留(2) + 系统字节(4) HEADER = struct.Struct(">HHHI") def recv_exact(sock, n): buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("connection closed") buf += chunk return buf def handle_client(conn, expected_dev_id): while True: # 先收 4 字节长度, 再收完整消息体 length = struct.unpack(">I", recv_exact(conn, 4))[0] body = recv_exact(conn, length) # 解析消息头 session_raw, msg_type, _rsv, sys_bytes = HEADER.unpack(body[:10]) session_id = session_raw & 0x3FFF # 低 14 位是设备 ID # 设备 ID 不匹配直接断开, 防止主机串线连错设备 if session_id != expected_dev_id: conn.close() return if msg_type == CTL_SELECT_REQ: rsp = HEADER.pack(0x0000, CTL_SELECT_RSP, 0x0000, 0x00000000) conn.sendall(struct.pack(">I", len(rsp)) + rsp) elif msg_type == CTL_LINKTEST_REQ: rsp = HEADER.pack(0x0000, CTL_LINKTEST_RSP, 0x0000, 0x00000000) conn.sendall(struct.pack(">I", len(rsp)) + rsp) def main(): srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", 5000)) srv.listen(8) while True: conn, addr = srv.accept() threading.Thread(target=handle_client, args=(conn, 0x0001), daemon=True).start() if __name__ == "__main__": main()

这段代码只处理控制消息,不解析 SECS-II 数据体,但已经覆盖了 HSMS 通信建立的骨架。recv_exact 是必要的,TCP 是流式协议,不按长度读满就解析,粘包和半包问题能把人搞疯。Select.rsp 的消息头要按标准结构回,只填类型码、其余字段补零,很多初学者把后面的字段随意填成设备信息,主机端会直接拒收。设备 ID 参数按工厂规划写,同一端口多台设备时,这里就是第一道防线。

如果设备做 HSMS-Client 主动连主机,代码差别只在连接方向:把 accept 换成 connect,建连后同样先处理 Select 流程。我一般建议能用 Client 就尽量 Client 模式,这样可以少在防火墙上给每台设备开入站端口,现场部署省很多事。

3.2 SECS-II 编解码:字节流和嵌套数据结构怎么互转

SECS-II 的数据项是带格式码和长度的嵌套结构。每个数据项由一个字节的格式码、宽度可变的长度字段和内容组成。长度字段的宽度由格式码的高位两位决定,常见规则是 0 对应 1 字节,1 对应 2 字节,2 对应 4 字节。List 的长度字段表示子项数量而不是字节数,这点特别容易写错。

# 常用格式码(完整字节值) FMT_LIST = 0x01 # List, 长度字段是子项个数 FMT_BINARY = 0x25 # Binary, 高位两位指示 4 字节长度, 适合大块数据 FMT_ASCII = 0x41 # ASCII 字符串 FMT_BOOL = 0x09 # Boolean def read_item(data, pos): fmt = data[pos] pos += 1 # 长度字段宽度由格式码高位决定, 0->1, 1->2, 2->4 width = 1 << ((fmt >> 6) & 0x03) length = int.from_bytes(data[pos:pos + width], "big") pos += width if fmt == FMT_LIST: items = [] for _ in range(length): item, pos = read_item(data, pos) items.append(item) return items, pos elif fmt == FMT_ASCII: return data[pos:pos + length].decode("ascii", errors="replace"), pos + length elif fmt == FMT_BINARY: blob = data[pos:pos + length] return blob, pos + length elif fmt == FMT_BOOL: return data[pos] != 0, pos + length else: return data[pos:pos + length], pos + length

参数说明里最关键的是 length 字段的宽度。有些设备固件实现不规范,长度字段不按格式码高位走,统一用一字节,这种设备碰到超过 255 字节的 Binary 数据就会解析错位,表现为主机收到的字段全部错位但又不报错,排查极其痛苦。ASCII 解码建议加上 errors="replace",老设备偶尔会在字符串里塞控制字符,直接 decode 抛异常会把整个接收线程打死。

写完解码器必须马上写对应的编码器。我用 S1F1/S1F2 做第一个验证:S1F2 的响应体是<L <A 设备名> <A 软件版本> ...>,如果编码器里 List 的嵌套顺序不对,主机解析出来的字段全部对应错位。这种错位在报文日志里很难看出来,因为每个字段类型都合法。

3.3 心跳与重传:网络抖动下怎么保证链路不假死

HSMS 在 TCP 层之上没有业务保活,链路是否活着全靠 Linktest 消息。主机定期发 Linktest.req,设备在超时时间内回 Linktest.rsp,收不到就判定断线并重连。心跳包重传的源码逻辑不难,难在参数,连得太频浪费带宽,连得太疏链路假死时间过长,现场表现为“连接还在但消息都发不出去”。

import socket import time import threading class LinktestManager: def __init__(self, conn, interval=45, timeout=10): self.conn = conn self.interval = interval self.timeout = timeout self._last_ok = time.time() self._alive = True def send_linktest(self): # Linktest.req: 类型码 0x0007, 10 字节头 req = HEADER.pack(0x0000, 0x0007, 0x0000, 0x00000000) try: self.conn.sendall(struct.pack(">I", len(req)) + req) self.conn.settimeout(self.timeout) resp = self.conn.recv(1024) if resp: self._last_ok = time.time() except socket.timeout: self._alive = False def loop(self): while self._alive: time.sleep(self.interval) self.send_linktest() def is_alive(self): return self._alive

心跳间隔的常见设置是 45 秒,对应很多 EAP 系统的默认表,网络稳定时没问题。但如果现场交换机或安全设备把空闲连接老化时间设成 30 秒,45 秒心跳就会周期性断链。我的习惯是把 interval 放进配置文件,首次上线先用 20 秒跑半天,再根据日志调整到 30 秒或 45 秒。检测到断线后,不要做复杂的指数退避重传,直接关闭旧 socket,重新走 Select 握手更简单可靠。

SECS-II 的事务重传是另一层逻辑:发了一个 SxFy 请求后,规定时间内没收到响应,要重发相同系统字节的消息。实现时用一个字典把系统字节和发送时间、重发次数存起来,超时后取出重发。这里最容易犯的错误是重发时重新生成系统字节,导致设备端把重发当成新事务,响应也对不上。

4. 高速场景的四个避坑点:并发、解析、心跳和事件上报

4.1 设备只允许最小连接数:并发的第一个坑

现象:把服务端代码写成能接受几千个 TCP 连接,到现场发现设备固件只允许两个连接。第三个连接要么被设备直接踢掉,要么一直挂在 SYN 状态,主机端来回报错。

原因:设备端的 SECS 实现往往不是通用 TCP 服务器,而是按固定会话数分配的。常见设计是一个连接给 EAP 主机,另一个预留给人机界面或工程诊断,并没有开放无限连接的能力。看似源码里 accept 随便调,实则是设备资源的硬限制。

解决:实施前先查设备参数里的连接数上限,通常标注为会话数或连接槽位。EAP 只维持一条主连接,需要临时诊断时复用这条会话,不要另开连接去跑 S2F41。源码里的连接管理写成有限连接池,超出上限直接拒绝并记录告警,比无限 accept 在线上更可控。

4.2 解析性能陷阱:大 Binary 和字符串转码拖慢收包

现象:设备每秒上报几十个事件,或者 Trace 数据里带几百 KB Binary,代码处理一帧要上百毫秒。主机端消息积压,最后 TCP 接收缓冲溢出,日志里出现“收到半个包”的异常。

原因:大量 SECS 源码对 Binary 用逐字节 append,对字符串反复 decode,再把整个 List 反复拷贝。我做过基准测试,一个 1MB 的 Binary 负载,逐字节处理比整块切片慢二十倍以上。问题不在协议,在写法。

解决:解析时对 Binary 直接整块切片,返回 bytes 或内存视图,后续计算不要二次拷贝。事件上报的 S6F11 如果嵌套很深,只解到需要的层级就停,不要递归到底。解析线程只做入队和 ACK,真正的业务计算放到消费线程,避免一条大消息卡住整个接收循环。

4.3 高频采集下的事件丢帧:缓冲溢出和优先级倒置

现象:设备端把 Trace 采集频率调到 10ms 一组,主机处理不过来,事件开始丢。而且不是丢后面的,是最早的事件先丢,趋势分析里缺了头一段。

原因:TCP 反压最终作用在设备侧发送队列。设备端的队列设计往往是最新覆盖最旧,这对高频 Trace 数据来说方向就反了,越新的数据越重要,旧的丢了反而不影响实时监控。

解决:把设备端事件队列拆成两类。高频 Trace 队列允许丢旧保新,只用环形缓冲存最近一批;低频告警和数据采集结果必须可靠上报,走独立队列并支持重传。主机端对应给 S6F11 开独立接收线程,快速入队后立即回 ACK,业务解析放慢一点没关系,别把设备端发送堵死。

4.4 心跳参数是现场玄学:间隔不对,链路时好时坏

现象:联调时一切正常,上线后每隔固定时间断一次。重连后又正常,过一会又断。TCP 层看得到 RST,SECS 层没有任何报错。

原因:网络设备,比如交换机、工业防火墙,空闲老化时间比 Linktest 间隔短,导致链路先被中间设备杀掉。很多 EAP 默认 45 秒心跳,但现场交换机老化时间设成 30 秒,这个冲突在代码层面完全看不出来,只能算时间参数的巧合。

解决:先把 Linktest 间隔降到 20 秒试,再不行就在 TCP 层开 KeepAlive 兜底,链路两侧都设置。现场如果发现“每隔 N 分钟必断”的规律,优先把 N 和心跳间隔、交换机老化时间放在一起算,很多是周期共振。我遇到过 N 正好是心跳间隔三倍的案例,光看代码一辈子找不到原因。

5. 用模拟器和压测脚本站稳再上产线:验证方法与现场习惯

5.1 标准模拟器互通验证

对接测机前不要拿真实设备练手,先跑通 SECS/GEM 模拟器。把源码里的设备角色起成模拟器,主机用现成的 SECS/GEM 模拟器连接,先跑 S1F13 建立通信,再手动触发一个 S6F11 事件,确认消息头、会话 ID 和系统字节都正确。互通测试通过后,把模拟器切到主机侧,验证设备端收到 S2F41 远程命令时状态机转换是否正常。做完这两轮,再上真实设备,问题会少一大半。

5.2 压测怎么抓真实吞吐

用脚本模拟主机高频请求,或者让设备循环上报 S6F11:

# 快速压测: 连续发 1000 帧 S6F11, 统计 RTT for i in range(1000): msg = build_secs2_message(device_id=1, stream=6, func=11) t0 = time.time() send_and_wait(msg) # 内部按系统字节配对响应 rtt.append(time.time() - t0) print("avg_rtt_ms", sum(rtt) / len(rtt) * 1000) print("p99_rtt_ms", sorted(rtt)[int(len(rtt) * 0.99)] * 1000)

把平均 RTT 和 p99 RTT 记下来,作为这条产线的基线。跑压测时把日志级别调到 WARNING,避免高 IO 的日志写入把结果带偏。测完清空模拟器的仿真事件,别让积压消息在正式联调时混进来,这些都是血泪经验换来的。

5.3 现场实施的两个习惯

第一,所有 SxFy 消息都落日志,日志里必须带系统字节,这样请求和响应能配对查。第二,心跳间隔、重传超时、设备 ID、连接上限这些参数全部外置到配置文件,不要写死在代码里。我吃过亏:现场改一个超时参数要重新编译,换到另一台设备忘同步,最终线上参数和代码对不上,排查时连后悔药都没有。

这套源码方向值不值得长期投入?如果公司持续有封测或晶圆设备对接需求,值得把 SECS/GEM 源码吃透并沉淀成内部公共库,后续 EAP 系统的现场实施、部署及日常运维工作会轻松非常多;如果只是单台设备一次性项目,用成熟模拟器加最小源码验证就够。我的习惯是先把最容易出问题的三处,也就是心跳间隔、长度字段解析、连接上限,全部写成带默认值的配置项,再交到现场。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询