☰
SECS/GEM协议详解:E5/E37/E30分层与二进制帧实践
2026/10/27 10:38:01 网站建设 项目流程

简介:《SECS/GEM 中文详解》是一份面向半导体及工业设备自动化从业者的协议学习文档,适合设备集成、上位机开发与产线运维人员梳理 SECS/GEM 的整体框架。针对网上资料零散、说明不细的问题,文档用10个章节系统讲解 SECS 功能,从协议介绍、消息日志、常用术语,到 Streams and Functions 的基础定义、系统保留与自定义分类、指令含义及使用场景,并展开设备集成成本、网络带宽利用、数据密度、无数据翻译、环路保证与安全等要点,同时覆盖设备状态报告、控制命令与数据传输等典型应用。资源包共1个 PDF 文件,约955KB,目录层级清晰,便于按章节查阅和对照实践。目前已有2272人学习下载,适合需要系统理解设备通信协议、对照目录查漏补缺的工程技术人员,可作为入门与速查材料。

1. 32 字节的报告撑起一条无人化产线:SECS/GEM 到底在省什么

一份事件报告,真实数据只有 32 字节:两个 4 字节整数、两个 8 字节浮点、一个 4 字节事件 ID、一个 4 字节报告 ID。装进 SECS/GEM 的帧里,算上 10 字节头和长度字段,总量还不到 50 字节;换成同样语义的 JSON,光键名就把体积翻好几倍。这份 SECS/GEM 中文详解里反复回到的,就是这条主线:一条产线上每秒可能滚过上万条这样的消息,字节数最后都会变成交换机的背板带宽和主机的解析开销。真正值得搞懂的,是这套协议为什么能在九十年代定型、今天反而比通用格式更贴合产线——它只掌握了两个词,抠字节、抠确定性。

2. 三层拆分:E5 管语义、E37 管传输、E30 管底线

2.1 三条标准各锁一段,谁也不侵入谁

SECS/GEM 不是一份文档,是三份标准捏在一起的合称,各自负责协议栈的不同层面,边界划得很硬:

标准名称负责的部分换掉它会发生什么
SEMI E5SECS-II消息结构 + 标准消息库(说什么)语义全变,所有主机软件重写
SEMI E37HSMS在 TCP/IP 上承载 SECS-II 的二进制帧(怎么送)语义不变,只换传输通道
SEMI E30GEM最低要求、可选功能、用例与用户场景(必须会什么)合规性判定失效,互操作没法谈

这种切法的动机很实际。早期的 SECS-I 跑在串口上,靠字节计数和握手保证完整;后来产线换以太网,只需要把传输层换成 HSMS,上层那几百条 SxFy 消息一个字都不用改。反过来,标准要新增一类业务语义,也只动 E5 的消息库,HSMS 的帧格式纹丝不动。我一般会把它理解成一次很早的「关注点分离」实践:语义的演进速度和传输的演进速度本来就不一样,绑在一起就是互相拖累。

E30 的位置更微妙。它不定义任何一个字节怎么排,只定义「一台自称符合 GEM 的设备,至少得会哪些动作」。状态变量、设备常量、采集事件、报警、远程命令、时钟、控制状态(Online/Offline、Local/Remote),这些是底线;配方管理、假脱机、终端服务、跟踪数据,全部标成可选。产线上设备千差万别,一台只有十几个采集事件的简单机台和一台能发五千个事件的复杂机台,都得能用同一套技术描述,这个分层就是把「最小合规」和「能力上限」解耦。

2.2 一条消息的解剖:10 字节头加长度字段

HSMS 帧的结构非常克制。头固定 10 字节,字段排布如下:

偏移字节数字段作用
02Session ID / Device ID区分是哪台设备、哪条连接;控制消息用全 1
21低 1 位 W-bit + 高 7 位 StreamW-bit 标记「这条要不要回复」
31Function与 Stream 组合成 SxFy
41PType协议类型,SECS-II 固定为 0
51SType会话类型:数据消息为 0,Select/Linktest 等控制消息非 0
64System Bytes事务号,回复必须原样带回

头后面跟长度字段,再后面才是消息体。这里有个容易记岔的地方:长度字段的长度是可变的,1 到 4 字节,取决于消息体多大;HSMS 这一层通常固定成 3 字节,能表达 16MB 量级,够用且对齐简单。10 字节头加长度字段,就是这条消息全部的固定开销——没有标签名,没有分隔符,没有转义。剩下每一个字节都在装真实数据。

2.3 为什么 Session ID 和 System Bytes 是两个独立字段

新手常把这两个当成冗余。它们解决的是完全不同的问题:Session ID 回答「这条消息属于哪条连接」,System Bytes 回答「这条消息属于哪次对话」。一台主机同时连着几十台设备,每台设备的连接各有一个 Session ID;而在同一条连接上,主机可能并发发出好几个请求,每个请求分配一个递增的 System Bytes,设备回复时把这个 4 字节原样抄回来,主机靠它把回复和请求对上山头。

这套配对机制带来的直接好处就是环路保证:任何一个请求要么收到匹配的回复,要么在超时窗口里被判死,不存在「发出去就石沉大海、程序永远卡在等回复」的中间态。产线软件最怕的就是这种悬空等待,一条挂死的线程能让整条线的调度停摆。资源文档里单独拿一节讲环路保证,原因就在这——它不是协议附赠的礼貌,而是整个控制逻辑能写成同步代码的前提。

2.4 GEM 的能力矩阵:必备项少,可选项多

GEM 把功能拆成必备和可选,落地时形成一张能力矩阵。必备的那部分大致是:状态变量上报、设备常量读写、采集事件与事件报告、报警管理、远程命令、时钟、控制状态机。可选的那部分包括配方管理、假脱机、终端服务、跟踪数据采集、错误消息、加工程序管理等。

这个设计有个隐性代价,后面第 6 章会展开:两台都宣称符合 GEM 的设备,实际能力可能差出一大截,互操作必须靠额外的能力协商和对接文档兜底。但换来的是覆盖率——从只有十几个事件的小机台,到五千个事件的大型设备,都能落在同一套框架里。

3. 把数据压进一个字节:格式码与数据密度

3.1 item 头:一个字节里塞进类型和长度宽度

SECS-II 的消息体是树。列表套列表,叶子是具体数据项。每个数据项前面有一个 item 头,第一个字节这样切:高 6 位是格式码,低 2 位记录后面跟几个字节来表示长度。低 2 位的取值是「长度字节数减一」,所以 0 表示 1 字节长度,1 表示 2 字节,2 表示 3 字节。

格式码本身是一张固定的表,常见取值按八进制记:

格式码类型单元素字节数
0o00List(列表,装子项)无值,只有元素个数
0o10Binary1
0o11Boolean1
0o20ASCII1
0o30 / 0o31 / 0o32 / 0o34有符号整数 I8 / I1 / I2 / I42 / 1 / 2 / 4
0o40 / 0o44浮点 F8 / F48 / 4
0o50 / 0o51 / 0o52 / 0o54无符号整数 U8 / U1 / U2 / U48 / 1 / 2 / 4

关键在于「类型」和「数据」是绑在一起的。一个 4 字节整数,在网络上的完整表示是 1 字节类型 + 1 字节长度 + 4 字节值 = 6 字节。多出来的 2 字节就是给类型和边界的全部税负,没有别的东西了。下面这段编码逻辑,基本就是所有实现里那个 encode item 的核心:

import struct # 格式码到 (struct 格式串, 单元素字节数) 的映射 FMT = { 0o30: ('>q', 8), 0o31: ('>b', 1), 0o32: ('>h', 2), 0o34: ('>i', 4), 0o40: ('>d', 8), 0o44: ('>f', 4), 0o50: ('>Q', 8), 0o51: ('>B', 1), 0o52: ('>H', 2), 0o54: ('>I', 4), } def encode_item(code, value): """把单个数据项编码成 SECS-II 字节串;value 为标量或同类型序列""" if code == 0o00: # 列表:长度字段表示元素个数,后面拼子项 body = b''.join(value) count = len(value) else: fmt, width = FMT[code] seq = value if isinstance(value, (list, tuple)) else [value] body = b''.join(struct.pack(fmt, v) for v in seq) # 大端,网络序 count = len(seq) # 长度字段是“元素个数”,不是字节数 # 选最短的长度字段宽度:1/2/3 字节,低 2 位存 “宽度-1” if count <= 0xFF: len_bytes, nlb = struct.pack('>B', count), 0 elif count <= 0xFFFF: len_bytes, nlb = struct.pack('>H', count), 1 else: len_bytes, nlb = struct.pack('>I', count)[1:], 2 # 取低 3 字节 header = bytes([(code << 2) | nlb]) # 高 6 位格式码,低 2 位宽度 return header + len_bytes + body

这段代码里有两处容易被忽略的细节。第一,长度字段写的是元素个数而不是字节数:一个含 100 个 F8 的列表,长度字段写 100,实际负载 800 字节。接收方拿个数乘以类型宽度自己算总长,省下了表达字节数的额外位宽。第二,浮点和整数一律大端,显式指定>,不依赖运行机器的字节序。这两点加起来,就是后面「无数据翻译」能成立的技术基础。

3.2 数据密度:负载越大,开销占比越接近零

按上面的结构,可以推一下开销占比。固定开销是 10 字节头加 1 到 3 字节长度字段,约 12 字节;每个叶子项再付约 2 字节的 item 头税。

场景实际数据SECS/GEM 约总量开销占比同语义 JSON 约总量
一条典型事件报告32 字节45 到 50 字节三成上下120 字节以上
一条含 100 个浮点的报告800 字节810 到 820 字节不足两个点1000 字节以上(键名重复)

上表是量级估算,不是精确基准,但趋势很清楚:固定开销基本恒定,负载一变大就被摊薄。资源文档里给的那组实测也指向同一个结论——把示例消息从几个标量扩到 100 个 8 字节浮点之后,SECS/GEM 的数据密度爬到 78%,JSON 和 XML 之间的相对关系几乎没动,因为它们每一位数据都要拖着键名或标签名走。

这个差异在单条消息上看是小事,放到整线规模就是另一回事。一台设备可能配了几十个采集事件、上百个跟踪数据项,主机侧还挂着统计工艺控制、预测性维护、前馈反馈控制一堆应用,每秒的消息量很容易上万。每条省下几十字节,乘上去就是网卡和解析器的吞吐差别。所谓「非常有效地使用网络带宽」,落到底层就是这个。

3.3 无数据翻译:二进制直通换来的确定性

「无数据翻译」这一条经常被当成宣传语忽略过去,其实它是个硬约束。SECS-II 里的 F8 就是 IEEE754 双精度,在设备侧内存里怎么存,到主机侧内存里就怎么读,中间没有字符串化,没有本地化数字格式,没有千位分隔符,也没有「12.5 序列化成 12.5000001」这类经典翻车。整数同理,大端固定,不存在字节序协商。

代价是调试不友好。抓包看到的是二进制,得靠工具反解。但产线软件要的不是好读,是不出错。设备端算出来的腔室温度是 87.35 度,主机拿到的必须还是 87.35,中间多一层格式转换就多一层踩坑机会。资源里把「无数据翻译」和「数据密度小」并列,本质上说的是一件事:少一次转换,少一次出错的可能。

顺带一提,二进制并不意味着不可读。SECS-II 的结构是完全自描述的,类型和长度都在流里,写个几十行的解析器就能还原成树状结构,再导出成 JSON 或 XML 做人工排查,这就是消息日志工具在做的事。

4. 事务与状态机:SxFy、W-bit、超时和连接状态

4.1 SxFy 命名法:流是话题,功能是动词

Stream 可以理解成业务话题的分类,Function 是话题下的具体动作。整个编号体系有个极省心的规律:奇数 Function 是请求,偶数 Function 是成对的回复,请求号加一就是回复号。

Stream话题常用消息
S1设备状态与管理状态变量、设备常量、控制状态切换
S2设备控制与诊断定义报告、链接事件、远程命令
S5报警报警上报、报警使能、报警列表
S6数据收集事件报告、跟踪数据、多块数据
S7配方管理配方上传下载、配方目录
S9错误消息无法识别的流/功能/数据、事务超时
S10终端服务操作员消息显示与确认

S6F11 是设备主动上报事件报告,S6F12 就是主机确认收到。S2F41 是主机下发远程命令,S2F42 回执行结果。记住这个奇偶规律,看日志时的反应速度会快很多。

4.2 系统保留与自定义:给私有扩展留一段编号

标准 Stream 之外还有一段预留编号,专门给厂商自定义消息用,S64 就是常用的自定义承载流。这个设计的动机很朴素:设备厂商总会遇到标准消息表达不了的需求,如果每次都去推动标准组织改文档,周期按年算。留一段私有编号,厂商自己定义结构和语义,双方对文档即可,标准本身不用动。

代价是私有段容易变成各家方言。同一个 S64F1,在不同厂商的机台上可能完全是两回事。血泪经验是:任何涉及 S64 的对接,必须先把编号和结构对齐成一份书面约定,别指望「大概一样」。

4.3 W-bit 与事务配对:不回 Reply 会怎样

W-bit 是头上那一位置位。置 1 表示发送方期待回复,置 0 表示这是单向通知,不需要回。设备主动上报的报警、跟踪数据,很多是 W-bit 为 0 的单向消息;主机下发的命令、参数读写,基本都是 W-bit 为 1。

置 1 之后,System Bytes 就成了这次事务的身份证。发送方在本地事务表里登记,等待回复时按事务号匹配,收到就把这一条摘掉,超时窗口到了还没等到就判失败并发 S9F9 事务超时。事务表的最小实现大致是这样:

import itertools, time class TransactionTable: def __init__(self, t3=45.0): self.t3 = t3 # T3:等待回复的超时,默认 45 秒 self._seq = itertools.count(1) # System Bytes 递增分配 self._pending = {} # system_bytes -> (发送时刻, 原始请求) def open(self, request_bytes): sbid = next(self._seq) & 0xFFFFFFFF # 4 字节回绕,别用 64 位 self._pending[sbid] = (time.monotonic(), request_bytes) return sbid def claim(self, reply_bytes): sbid = int.from_bytes(reply_bytes[6:10], 'big') # 回复必须带回同一个事务号 return self._pending.pop(sbid, None) # 匹配成功即摘除,防止重复消费 def sweep(self): now = time.monotonic() for sbid, (t0, req) in list(self._pending.items()): if now - t0 > self.t3: # 超时:清账 + 上报 S9F9 del self._pending[sbid] yield sbid, req

open里的 4 字节回绕是个真实坑:事务号用满会绕回去,如果表里塞了大量长期未回的事务,新号可能撞上旧号,claim就会摘错条目。工程上的做法是控制未决事务数量,并在回绕时跳过仍在表里的号。sweep是兜底,定期扫一遍超时项,保证「发出去等不到回复」这件事一定会被显式处理,而不是静默堆在内存里。

4.4 HSMS 的连接状态:Selected 之前别发数据

TCP 连上不等于能发业务消息。HSMS 在 TCP 之上又叠了一层会话状态:

从 Not Connected 建立 TCP 之后进入 Connected,但此时是 Not Selected,只能发控制消息。设备或主机任一方发 Select.req,对方回 Select.rsp,双方进入 Selected,数据消息才允许通行。中间任何一方发 Deselect.req 就退回 Not Selected。Linktest 用来在空闲期探活,不改变状态。

这段设计解决的是「连接可用性」和「通信可用性」的区分。TCP 连接还在,但对端上位机可能正在重启、正在切换配方、正在做自己的初始化,这时候塞数据过去只会得到一堆错误回复。用一层显式的 Selected 状态,把「你准备好接客了」这件事变成协议里可以确认的事实。

4.5 超时不是玄学:T3 到 T8 各守一段

超时参数是这套协议里最像玄学、实际最讲道理的部分。每个定时器守的是一段特定边界,互不重叠:

定时器常见默认值守的是哪一段
T345 秒发出 W-bit 为 1 的请求后,等回复的时长
T510 秒连接已断,等重新建立完成的时长
T65 秒控制事务(Select、Deselect、Linktest)的回复等待
T710 秒进入 Not Selected 后,等再次被 Select 的时长
T85 秒单个消息传输过程中,字符之间的最大间隔

T3 给得最宽,因为对端可能在执行一条耗时命令;T6 给得紧,因为控制事务本该是毫秒级。T7 是最容易被忽略的一个:如果一条连接在 Not Selected 状态下待满 T7,实现应该主动断开重连,否则会留下一条「TCP 活着但双方都不说话」的僵尸连接占着资源。T8 则防的是半包——发了半个消息就卡住的场景,靠字符间隔判断对端是不是已经废了。

5. 避坑:联调现场最常见的五条翻车记录

5.1 事务层:消息发出去了,程序卡在等回复

现象:主机下发 S2F41 之后主线程不返回,日志里能看到请求已发出,没有任何回复,也没报错,就这么挂着。

原因:这条请求的 W-bit 被置成了 0。设备端收到后发现「对方没要求回复」,按标准就不回复,主机侧却在事务表里死等。另一种同源情况是设备端回了,但 System Bytes 被写成了固定值 0,主机按事务号匹配不上,回复被当成野包丢弃。

解决:抓包先看第二字节的最低位,确认 W-bit 与业务意图一致;再看回复消息的第 6 到 9 字节是否与请求完全一致。这两项对上了,问题基本就解决了。排查时把事务表内容打出来,未决条目一目了然。

5.2 数据层:报告收到了,但值是空的

现象:S6F11 事件报告能收到,结构也对,就是里面的变量值全是默认值或空列表。

原因:主机只在启动时发过一次 S2F33(定义报告)和 S2F35(把报告链接到事件),但之后设备重启过,报告定义丢了;或者设备端的采集事件没有被 S2F37 使能,上报的还是空壳。GEM 里「定义」「链接」「使能」是三个独立动作,少一步都不会有数据。

解决:把这三步做成启动握手的固定序列,每次连接进入 Selected 后重新走一遍,而不是只在第一次连上时做。加上幂等处理,重复定义不会出错。

5.3 数据层:编号对上了,语义对不上

现象:设备上报的某个变量,主机拿到的数值明显不对,差了几个数量级。

原因:VID 或 CEID 的定义只对了编号,没对单位。设备侧以毫秒为单位上报时间,主机按秒解析。这类问题在字段名一致的情况下极难发现,因为数据「看起来是合理的」。

解决:对接文档必须精确到每个数据项的格式码、单位、量纲和取值范围,而不是只列编号。模拟量尤其要标注原始单位和换算方式。

5.4 存储层:断网重连之后,事件丢了一大截

现象:网络闪断几十秒,恢复后设备继续正常上报,但断连期间发生的事件全部消失了,主机侧报表出现空洞。

原因:设备没有启用假脱机功能。假脱机的作用是把发不出去的事件消息落到非易失性存储里,重连后按原顺序补发。没启用的话,发送失败就是丢弃,而设备端通常只打一条日志,不报警。

解决:先确认设备是否支持假脱机、存储配额多大、满了之后的策略是丢弃最旧还是拒绝新增。配额和策略比「支不支持」更重要——配一个小存储还不如不配,因为它会给你一个「有兜底」的错觉。

5.5 时间层:日志能对上,顺序对不上

现象:排查问题时把两侧日志按时间戳排序,发现因果关系完全反了,主机记录的「下发命令」居然早于设备记录的「上报状态」。

原因:两侧时钟不同步,或者其中一侧用了本地时区而另一侧用了 UTC。SECS/GEM 有专门的时钟消息用于对时,但很多设备只实现读取,不支持被设置。

解决:不要拿两侧时间戳直接对比。以 System Bytes 和消息序号为因果依据,时间戳只作参考。需要严格时序时,先走一轮对时,并明确约定时区。

6. 设计取舍:GEM 为什么把「可选」写进标准

必备加可选这个分层,短期看是妥协,长期看是这套协议活得久的主要原因。半导体产线上的设备跨度极大:一台几十年机龄的老设备只有几个状态点,一台新机台能发五千个采集事件。如果标准要求全实现,老设备根本进不来;如果标准只要求最少的那点,新设备的能力又没处表达。E30 的做法是把「最小合规」压到很低,剩下的全部标成可选,由设备厂商按需实现,主机侧通过能力协商去发现对方到底会什么。

这个取舍的代价也很实在:合规不等于可对接。两台都贴了 GEM 标签的设备,实际行为可能完全不同。所以工程上真正决定成败的不是「符不符合标准」,而是三份东西——设备能力清单、数据项定义表、事件与报告映射表。这三份对不齐,协议再规范也白搭。

验证思路也由此而来。我会建议用一套固定的对拍流程:连接进入 Selected 后,先跑完整的握手序列(清报告、定义报告、链接事件、使能事件),然后触发一个已知事件,比对上报的报告结构和数值;再故意发一条非法格式的消息,看对方是否按规范回 S9F7;最后拔网线三十秒,看假脱机的补发是否完整、顺序是否正确。这三步跑通,这套接口的上线风险基本可控。

初级对接常见的误区是把 GEM 当成一个「开箱即用的通用接口」。它不是接口,它是一份约定模板,具体长什么样得双方填。这也解释了为什么原始文档里那几节讲数据密度、无数据翻译、环路保证的内容放在最前面——它们描述的是一种确定性风格,而整份文档后面所有章节,都是在这个风格下展开的具体条目。

从那以后我每次接手一个新的设备对接,都强制走一遍那三步对拍,不管对方文档写得多漂亮、不管对方工程师说「我们完全符合 GEM」。文档会骗人,抓包不会。希望帮到你。

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

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

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

立即咨询