简介:《SECS/GEM标准规范-金南瓜》面向半导体设备自动化领域的设备工程师、上位机开发者和制造执行系统集成人员,重点解决不同厂商设备与主机系统之间的标准化通信与互操作问题。文档以金南瓜科技的SECS/GEM方案为线索,系统梳理通讯状态模型、控制状态模型和设备处理状态,并展开事件通知、在线识别、动态事件报告配置、变量数据采集、跟踪数据采集、状态数据采集、报警管理、远程控制及设备常数等能力,同时涉及TCP/IP通信超时、变量和主机命令等实现细节。压缩包内共1个PDF文件,约1006KB,规格说明书目录层级完整,便于按状态模型、设备能力与通信协议章节查阅。已有746人学习,适合在设备接口联调、GEM适配、产线数据采集与自动化控制方案评估时作为规格参考。
1. SECS/GEM 的第一性原理:为什么设备侧永远在等主机发话
第一次接触 SECS/GEM 的人,通常会把它理解成“跑在 TCP 上的 HTTP”:设备是服务端,主机发请求,设备回响应,接上就好。真正翻过设备端实现的人会得到相反的结论:设备在这套体系里几乎不主动说话,它只做两件事——记住主机配置给它的报告清单,以及在状态变化时按清单吐出数据。所有“你希望设备做什么”,都要先经过一次主机发起的、带事务号的消息交换。
这个被动模型不是设计缺陷。自动化产线里,设备网络可能随时断、多厂商设备之间互不信任、主机和设备的启动顺序无法保证,这些都是当年必须正面处理的问题。SECS/GEM 用一套显式状态机把这些不确定性收窄:连没连上是一个状态,谁控制谁是一个状态,设备自己跑到哪一步又是一个状态。三个状态模型互相正交,任何一个动作都要先问“当前在哪三个状态上”。搞懂这套状态机,比背一百条消息定义更有用。
2. 通信状态机:S1F13 建链时那个“初始已过期”的定时器
通信状态模型描述的是设备与主机之间链路是否可用。它看起来只有“开/关”两档,实际展开后是一个嵌套结构,用 Harel 记法画出来能占大半页纸。嵌套的原因只有一个:链路可用不等于通信建立,通信建立又分为“谁先发起”两条并行路径。
2.1 DISABLED 与 ENABLED:为什么通信开关要放在最顶层
DISABLED 的定义很硬:SECS-II 与主机之间不存在通信,操作员从 ENABLED 切到 DISABLED,所有 SECS-II 通信立即停止。注意“立即”两个字,它意味着 DISABLED 不是断网,socket 可能还连着,但设备不再处理任何 SECS-II 消息。系统初始化时的默认值可以是 DISABLED,也可以是 ENABLED,具体由设备厂商配置。
把这一档独立成顶层状态,是为了给操作员一个绝对可靠的总开关。设备面板上有个“禁止通信”的物理或逻辑开关,维护时按下它,主机发什么都进不来,不用去猜当前是网络问题还是配置问题。ENABLED 之下才展开 COMMUNICATING 与 NOT COMMUNICATING 两个子状态,也就是“允许通信但还没连上”和“已经连上”。
2.2 NOT COMMUNICATING 里的两个 AND 子状态
NOT COMMUNICATING 内部是并行结构,同时存在 HOST-INITIATED CONNECT 和 EQUIPMENT-INITIATED CONNECT 两个子状态,两者都处于活动状态。这一点最容易被误读成“二选一”,实际是“双管齐下”:设备一边等着主机发 S1F13 过来,一边自己也在往外发 S1F13,谁先握手成功就走谁的路径。
这种设计解决的是启动顺序不确定的问题。主机可能比设备先起来,也可能反过来;如果是设备先起来,它不能傻等主机来敲门,必须主动尝试。两条路径最终都汇入 COMMUNICATING,但进入方式不同:主机发起路径下,设备收到 S1F13 后回一条 S1F14,把 COMMACK 置 0,通信即告建立;设备发起路径下,设备自己发 S1F13,等到主机回 S1F14 且 COMMACK 为 0 才算成功。
2.3 WAIT CRA 与 WAIT DELAY:CommDelay 与“过期”初值
设备发起路径内部还有两个子状态。进入 NOT COMMUNICATING 时,只要 EQUIPMENT-INITIATED CONNECT 首次变为活动状态,状态机就迁移到 WAIT CRA,并把 CommDelay 定时器设置为“过期”。这个初值很关键:它让设备在进入未连接状态的瞬间就发出第一条 S1F13,不做任何等待。
如果这次连接事务失败,迁移到 WAIT DELAY,启动 CommDelay 定时器,同时把发送队列里的所有消息出队;如果设备支持,出队的消息要按生成顺序放进 spool 缓冲。等定时器到期,再回到 WAIT CRA 重发 S1F13。WAIT DELAY 期间如果收到任何非 S1F13 的消息,规则是丢弃、不回复、把 CommDelay 重新置为“过期”、发 S1F13——文档的原话是“表明这是一个建立通信的机会”。
这两条规则合起来就是一套退避策略:首次尝试零延迟,失败后按 CommDelay 间隔重试,中途收到主机消息就视为对端存活、立刻抢先握手。常见的实现思路是把 CommDelay 做成设备常数,默认几秒到十几秒,运维可以通过主机下发调整。
# 通信状态机的简化实现,只保留 GEM 明确要求的状态迁移 class CommState: DISABLED = "DISABLED" NOT_COMM = "NOT_COMM" # ENABLED 下尚未建链 COMM = "COMM" # COMMUNICATING def __init__(self, comm_delay=10.0): self.state = self.DISABLED self.comm_delay = comm_delay # CommDelay 定时器,单位秒 self.delay_expired = True # 进入 NOT_COMM 时初始为 True self.tx_queue = [] # 待发消息队列 def on_system_init(self, default_enabled=True): self.state = self.NOT_COMM if default_enabled else self.DISABLED if self.state == self.NOT_COMM: # 规则 5:进入设备发起路径立刻把定时器置"过期"并发 S1F13 self.delay_expired = True self.send_s1f13() def send_s1f13(self): # 带 W-bit 的 primary message,等待 S1F14 self.tx_queue.append({"sf": "S1F13", "wbit": True}) def on_recv(self, msg): if self.state != self.NOT_COMM: return if msg.sf == "S1F13": # 主机发起路径:回 COMMACK=0 后直接进 COMMUNICATING self.reply(msg, "S1F14", commack=0) self.state = self.COMM elif msg.sf == "S1F14": if msg.commack == 0: self.state = self.COMM # 规则 9 else: # 连接事务失败,进入 WAIT DELAY 并清空发送队列 self.delay_expired = False self.tx_queue.clear() elif not self.delay_expired: # 规则 8:WAIT DELAY 期间收到非 S1F13,丢弃且不回复 self.delay_expired = True self.send_s1f13()这段代码只做两件事:判定是否允许建链、决定何时发 S1F13。delay_expired的初值对应文档里那句“CommDelay 定时器设置为过期”,作用是让首次重试的等待时间为零。on_recv里处理 S1F13 时不检查当前处于哪个子状态,因为两个 AND 子状态始终并行有效,主机任何时候敲门都能接。
注意:CommDelay 与 HSMS 层的 T5、T6、T7 不是一回事。前者管“业务层多久重试建链”,后者管“TCP 会话的选活与分离”,混用会让超时排查彻底失去方向。
3. 控制状态机:OFF-LINE 下回 SxF0 为什么不算报错
控制状态模型定义的是主机与设备之间的协作级别,也就是“谁说了算”。它和通信状态正交:链路可能已经通了,但控制权还在设备本地;也可能控制权给了主机,链路却刚断。
3.1 OFF-LINE、ON-LINE 与 LOCAL、REMOTE 的四象限
OFF-LINE 下,设备对来自主机的任何 primary message 都回 Sx,F0,只有 S1F13 和 S1F17 例外。Sx,F0 是 Function 0 的意思,表示这个功能不被接受或不支持,它不是错误码消息,而是一种标准化的否定应答——报文本身格式完全合法,只是设备告诉你“现在不做”。
ON-LINE 下再分 LOCAL 和 REMOTE。LOCAL 表示操作员在设备面板前控制,主机可以继续收事件报告、查变量,但涉及动作的命令要受限;REMOTE 表示控制权交给主机,远程命令可以下发。这四个象限的组合决定了同一条 S2F41 在不同时刻的返回值完全不同,所以联调时第一件事永远是确认当前控制状态,而不是怀疑报文拼错。
3.2 S1F15 / S1F17 与 S1F1 / S1F2 的分工
S1F15 是主机请求设备进入 OFF-LINE,设备回 S1F16,OFLACK 表示接受与否。S1F17 是主机请求设备进入 ON-LINE,回 S1F18,ONLACK 有三个取值:0 表示接受,1 表示不接受,2 表示已经在线。这三个值必须都处理,只判 0 是最常见的翻车点之一。
S1F1 是 Are You There,回 S1F2 附带设备型号和软件版本。它和建链无关,任何状态下都应该应答,作用是在链路看起来正常但行为异常时确认对端进程是否还活着。很多实现把 S1F1 也塞进 OFF-LINE 的拦截名单,结果主机探活失败,误判成断链。
3.3 控制权为什么留在设备侧
控制状态机里最反直觉的一条:主机不能单方面把设备拽到 ON-LINE,它只能“请求”,设备可以回 ONLACK=1 拒绝。原因在于设备面板前可能站着操作员,正在手动调试;主机一声令下就夺走控制权,物理上可能造成危险。所以控制权的转移被设计成协商,拒绝是合法结果,不是故障。
# OFF-LINE 下的消息分派:SxF0 是兜底应答,不是抛异常 ALWAYS_ALLOWED = {"S1F13", "S1F17", "S1F1"} def dispatch(control_state, msg): # control_state: "OFFLINE" / "ONLINE_LOCAL" / "ONLINE_REMOTE" if control_state == "OFFLINE" and msg.sf not in ALWAYS_ALLOWED: # 保持 stream 编号,function 置 0 return {"sf": f"S{msg.stream}F0", "wbit": False} handler = HANDLERS.get(msg.sf) if handler is None: return {"sf": f"S{msg.stream}F0", "wbit": False} return handler(msg)分派表把“状态允许”和“功能存在”拆成两层判断。第一层看控制状态,第二层看有没有对应 handler,两层都以 SxF0 收尾。这种写法的好处是设备永远不会因为收到不认识的消息而断开连接——错误被吸收进正常报文流里,这也是 SECS-II 能长期跑在无人值守产线上的原因之一。
4. 事件报告配置:S2F33、S2F35、S2F37 为什么必须拆成三步
GEM 最核心的能力是“设备主动上报”,而主动上报的全部灵活性都压在三层间接寻址上。理解这三层,基本就理解了 GEM 一半的设计哲学。
4.1 RPTID、CEID、VID 三层间接寻址
VID 是变量编号,代表一个具体的数据点,比如腔体温度、批次号、阀门状态。RPTID 是报告编号,本质是一个 VID 列表,规定“这份报告里装哪些变量”。CEID 是采集事件编号,代表设备上发生的一件事,比如“进片完成”“报警发生”“配方切换”。
三层之间的关系是:事件发生 → 找到挂在该事件上的报告 → 按报告里的变量列表取值 → 打包上报。中间加一层报告,是为了复用:同一组变量可以被多个事件共用,不用在每个事件里重复列一遍。这也是为什么 S2F35 的报文结构是“事件 → 报告列表”,一个事件可以触发多份报告。
4.2 定义、链接、使能三步为什么不能合并
S2F33 定义报告:给一个 RPTID,配上它的 VID 列表。S2F35 链接事件:给一个 CEID,挂上若干 RPTID。S2F37 使能开关:给一组 CEID,打开或关闭上报。
拆成三步的直接收益是修改粒度。主机想给某份报告加一个变量,只需要重发涉及该 RPTID 的那条 S2F33,不用动事件链接;想临时压掉高频事件,发一条 S2F37 关掉即可,配置本身原样保留,随时能开回来。如果合成一条消息,任何微调都要整体重下,设备在高节拍运行时根本承受不起。
更深一层的原因是配置是易失的。设备断电重启后,报告定义、事件链接、使能状态全部丢失,主机必须在每次建链完成后重新下发全套配置。三步分离让这套重放过程可以分阶段做、分阶段校验,出问题能定位到是哪一步没生效。
| 消息 | 作用 | 关键字段 | 语义 |
|---|---|---|---|
| S2F33 | 定义报告内容 | RPTID、VID 列表 | 覆盖式,RPTID 为 0 表示清空全部 |
| S2F35 | 事件挂接报告 | CEID、RPTID 列表 | 覆盖式,先解绑再绑定 |
| S2F37 | 使能/禁用上报 | CEED、CEID 列表 | 状态式,CEID 为空表示全部 |
| S6F11 | 设备上报事件 | CEID、RPTID、变量值 | 非幂等,必须回 S6F12 |
4.3 S6F11 的报文骨架与 S6F12 的语义
S6F11 的载荷是 DATAID、CEID,然后是一个列表,每项包含 RPTID 和该报告对应的变量值列表。DATAID 在多数实现里固定为 0,它的历史用途是关联一次事务,现在基本退化成一个占位字段。
S6F11 W <L [3] <U4 0> // DATAID,通常固定 0 <U4 3001> // CEID:进片完成 <L [1] <L [2] <U4 1001> // RPTID:报告 1001 <L [3] <A "LOT20240927A"> // VID 2001 批次号 <F8 182.35> // VID 2002 腔体温度 <BOOLEAN 1> // VID 2003 阀门状态 > > > >S6F11 带 W-bit,主机必须回 S6F12。ACKC6 为 0 表示接受,1 表示报告未定义,2 表示其他错误。这个 ACK 不是礼貌性的:设备在等它,T3 超时会触发重发,主机就会收到同一条事件两次。很多主机侧把 S6F12 做成异步发出,队列一堵就重复上报,最后统计出来的产量比实际多,这类问题在报表上极难发现。
提示:主机在配置前应该先用 S1F11 拉状态变量名单、用 S2F29 拉设备常数名单,确认自己引用的 VID 和 CEID 真实存在。盲目下发 S2F33 会收到 LACK 非 0 的 S2F34,而设备不会因此断开,配置就这样静默地没有生效。
5. 联调排查:T3 超时、COMMACK 与格式码的五个翻车现场
协议本身的规则不多,但每条规则背后都有一个能让人耗掉一整天的坑。下面五条是按出现频率排的,基本覆盖了从建链到上报的全链路。
5.1 T3 超时重发导致远程命令被执行两次
现象:主机下发 S2F41 让设备执行一次阀门动作,设备日志显示执行了一次,主机侧却记录两次,甚至物理动作做了两遍。
原因:S2F41 带 W-bit,设备处理慢、S2F42 回得晚,主机 T3 默认 45 秒超时后重发。SECS-II 靠 System Bytes 匹配事务,如果重发时重新生成了 System Bytes,设备就把它当新请求;即使复用,设备侧如果没有对重复事务做缓存应答,也会再执行一次。
解决:重发一律复用原 System Bytes;设备侧维护一张短期事务表,收到相同 System Bytes 的请求直接回缓存的应答,不重复执行。远程命令这类非幂等操作,必须按这个方式处理。
5.2 COMMACK 非 0 就当成建链失败
现象:主机收到 S1F14,看到 COMMACK 不是 0,状态机不迁移,链路永远建不起来,但抓包显示两边都在正常收发。
原因:COMMACK 有三个值,0 是接受,1 是不接受,2 是已经在线。很多实现只判 0,把 2 也当成失败,于是主机反复发 S1F13,设备反复回 2,死循环。
解决:0 和 2 都要迁移到 COMMUNICATING,只是路径不同;只有 1 才需要走失败退避。设备侧同样要处理主机回 2 的情况。
5.3 S2F35 链接了未定义的 CEID
现象:主机依次发了 S2F33、S2F35、S2F37,三条都收到了应答,但设备一条 S6F11 都不发。
原因:S2F35 的应答 S2F36 里 LACK 有四个取值,0 接受,1 表示 RPTID 不存在,2 表示 CEID 不存在,3 表示两者都不存在。主机侧只看“收到回复”,没看 LACK,链接实际全部失败。
解决:每一条配置消息的 ACK 码都要落日志并断言;配置前用名单类消息确认 CEID 和 RPTID 的合法集合;配置后用 S2F37 查询或触发一次测试事件验证。
5.4 格式码选错,主机侧解析出空列表
现象:某个变量在主机的界面上永远是 0 或空,但设备侧日志显示值正常发出。
原因:SECS-II 的格式码区分得很细,U4 是无符号整数,I4 是有符号整数,A 是 ASCII,BOOLEAN 是单字节布尔。设备用 U4 发、主机按 A 解,长度对不上就直接丢弃。另一种情况是单元素变量,设备发成裸标量,主机按 L 解析,取不到值。
解决:在数据字典里把每个 VID 的格式码固定下来,单元素变量统一用列表包裹,两边都按同一份字典实现;联调时抓原始报文比对格式码字节,不要只看业务层日志。
5.5 ALCD 当成整数比较,清除报警变成新报警
现象:设备侧发出报警又清除,主机侧认为发生了两次报警,报警统计翻倍。
原因:S5F1 里的 ALCD 是一个字节,最高位置 1 表示报警发生、置 0 表示报警清除,低 7 位是报警类别。如果主机把整个字节当数值比较,0x81 和 0x01 就是两个不同的值,于是清除被识别成新报警。
解决:按位判断最高位,低 7 位单独取类别;同一 ALID 的 set 和 clear 必须做状态配对,不能各自计数。
注意:以上五个坑有一个共同点——错误都发生在协议层内部,链路始终是通的,所以“能连上”从来不等于“配对了”。排查顺序应该是状态机、ACK 码、格式码,最后才是业务逻辑。
6. 设计取舍:间接寻址与被动模型的代价,以及怎么验证实现
这套标准能活三十多年,靠的不是性能,而是“谁都不用改自己”。设备厂商可以随时增加一个变量,只要分配一个新的 VID 并在名单里报出来,主机不用重新编译;主机可以随时调整要采集哪些数据,只要重发三条配置消息,设备不用升级固件。代价也很明确:主机必须维护一份从 VID 到业务字段的映射表,配置易失导致每次重连都要重放,间接寻址让一次简单取值要绕三层。
更值得想清楚的是被动模型。设备只在被使能后才上报,这让主机完全掌控节拍,也意味着主机挂掉设备就变哑,没有任何本地兜底。设计者在实时性和可控性之间选了后者,因为产线上“数据重复”比“数据延迟”更难收拾。同理,E37.1 的单会话模式只允许一个活动会话,牺牲了多主机并行,换来的是状态简单、没有并发写入的歧义。
验证自己的实现,我一般按三步走。第一步抓包回放:录一段从 S1F13 到第一条 S6F11 的完整报文流,回放给设备侧,看状态迁移是否逐条对上,尤其注意 NOT COMMUNICATING 下两个并行子状态有没有都触发。第二步故障注入:故意不回 S6F12 测 T3 重发,故意在 WAIT DELAY 期间插一条无关消息看是否被丢弃,故意让 S2F35 引用不存在的 RPTID 看 LACK 是否被正确置位。第三步断电重放:把设备断电重启,验证主机能否在不人工干预的情况下重建全部报告配置。
# 用抓包工具把某一时段的报文导出,按 System Bytes 去重后统计 # 重点看三件事:重发的 primary 是否复用 System Bytes、SxF0 出现在哪些 stream、S6F11 与 S6F12 是否一一对应 tshark -r session.pcap -Y "tcp.port == 5000" -T fields \ -e frame.time -e tcp.payload > secs_raw.txt上面这条命令只是把原始载荷落成可读的时间序列,真正的分析要靠按 System Bytes 建索引。重发复用、SxF0 分布、S6F11 与 S6F12 的配对率,这三个指标基本能定位八成问题。
从那以后我每次接新设备,都先抓一段从 S1F13 到第一条 S6F11 的报文,把 CEID、RPTID、VID 三张表对齐了再谈业务逻辑——顺序反过来,后面全是返工。这份规格说明书把状态模型、消息子集、数据字典和场景时序都逐条列了出来,尤其是通信状态迁移表和各类 ACK 码取值,对照着看比在抓包文件里瞎猜快得多,需要的话可以直接取用。希望帮到你。
本文还有配套的精品资源,点击获取