干这行的人应该都有体会,头一回接触SECS协议,十有八九是被项目逼的。设备已经躺在车间里了,EAP(Equipment Automation Program,设备自动化程序)的上位机软件写着写着卡住了,SECS/GEM那几个缩写翻来覆去查了不少资料,真正能用来指导落地的却没几篇。我自己当年从纯IT转过来做半导体设备互联,拿到手的第一个任务就是“把设备接进MES系统”,那时候连S1F13是干什么的都不知道,硬着头皮啃了半个月SEMI标准文档,才把最基础的通信链路调通。所以这系列教程,我不会给你翻译标准文档,而是以“我要把一台设备跑起来、让Host和Equipment能对上话”这个目标为线索,把SECS协议的核心逻辑、消息结构、实操踩坑全部梳理清楚。第一期先讲基础框架和第一个能跑的通信Demo,适合刚接手半导体、光伏、锂电设备对接项目的软件工程师,也适合想了解工厂自动化底层通信原理的开发者。
1. 先搞清楚SECS协议家族到底在解决什么问题
1.1 会议室里聊的“SECS/GEM”,其实是一整套协议族
很多刚入门的人会把SECS当作一个协议,实际上它是一整套标准体系,SEMI组织(Semiconductor Equipment and Materials International,国际半导体设备与材料协会)从上世纪80年代开始持续发布。核心标准包括:
- SEMI E4(SECS-I):定义了基于RS-232串口的物理传输协议,规定了数据怎么拆成块、怎么校验、超时重发等底层机制。
- SEMI E5(SECS-II):定义了消息层协议,解决的是“消息长什么样”——消息头怎么编码、数据项用什么格式、Stream/Function如何分配。
- SEMI E37(HSMS):定义了基于TCP/IP的传输协议,取代慢速串口,成为今天工厂自动化系统里最常用的传输方式。
- SEMI E30(GEM):在SECS-II之上做了行为层面的约定,比如设备的状态模型、报警怎么上报、配方怎么管理,让不同厂商的设备在行为上保持高度一致。
用快递类比的话,SECS-I和HSMS是“运输货车”,负责把包裹从A点拉到B点;SECS-II是“包裹里的标准装箱单”,规定了什么货物必须放在什么位置、怎么打标签;GEM则是“快递公司服务流程规范”,规定上门取件要在什么时间、签收要盖什么章。四者配合,才能让设备厂商和EAP开发商在不直接沟通的情况下也能实现对接。
1.2 为什么设备厂商都愿意遵守这套标准
制造业设备种类极其繁杂——薄膜沉积、光刻、刻蚀、清洗、检测,每家厂商的机型都不一样。如果没有统一标准,EAP开发商每接入一种新设备,就要写一套专门的通信驱动,耗费巨大且极难维护。SECS/GEM的价值在于:它让设备和上位机之间形成了一套“通用语言”。设备厂商只要实现SECS/GEM标准,EAP开发商就可以用相对统一的代码框架去对接不同设备,差异化部分收敛到配置文件和少量的私有消息适配。
不过要提醒的是,标准归标准,现实中每家设备厂商对标准的理解和落地程度参差不齐。有些设备把S6F11事件上报的实现方式写得跟标准样例不太一样,有些设备在S1F13握手时返回的数据结构有特殊约定。所以在实操中,设备厂商提供的通信规格书(通常是一份SML文件加若干说明文档)往往比SEMI标准正文更值得信赖。
1.3 通信的两种角色:谁主动,谁被动
SECS协议里一台设备(Equipment)对应一台Host主机,通信双方有明确的角色分工:
- Host(主机/上位机):通常是EAP、MES(Manufacturing Execution System,制造执行系统)或者专门的设备监控平台。它既做服务端监听设备连接,也会作为客户端主动下发指令。
- Equipment(设备端):生产设备上的PC或控制器。设备上电后需要通过HSMS连接到Host,完成握手后,双方才能开始交换业务消息。
从网络连接模式看,HSMS有两种方向:设备端主动连接Host(ACTIVE模式,最常见),或者Host主动连接设备(PASSIVE模式)。这个选择不是随意的,通常由工厂的网络架构决定。如果设备在产线局域网内、Host中心化部署,那设备主动找Host是常态;如果设备处于隔离网段且不允许主动外连,可能就要Host反连了。
2. 消息层核心概念与报文解剖
2.1 Stream和Function:给消息编号的“门牌系统”
SECS-II消息层用Stream/Function二元组来标识每一条消息的具体用途。Stream是消息的大类,Function是具体的子功能。例如:
- S1F13:通信建立请求(Establish Communications Request),由设备端发起握手。
- S1F14:通信建立确认(Establish Communications Acknowledge),Host收到S1F13后回复。
- S6F11:事件报告发送(Event Report Send),设备主动上报事件,例如“抓片器到位”“腔体抽真空完成”。
- S2F17:日期时间请求(Date and Time Request),Host查询设备当前时间。
其中,S1F13是“Primary Message”(主消息),S1F14是对应的“Secondary Message”(次消息)。在协议交互里,Primary消息表示一次对话的发起方,Secondary消息表示应答方。判断哪条消息是Primary,关键看消息头里的W位(Wait Bit)。
2.2 W位:判断消息是请求还是应答
W位是消息头Stream字节的最高位(bit 7)。规则非常简单:
- Primary消息发起时,W位置1,表示“我在请求你,请给我回复”。
- Secondary消息回复时,W位必须为0,表示“这是响应,不是新的请求”。
举个例子:设备发S1F13时,消息头的Stream字段值是0x81(二进制1000 0001),其中低7位是Stream=1,最高位W=1。Host回复S1F14时,Stream字段值是0x01,W位为0。如果W位搞反了,接收方很可能直接把消息当协议错误丢弃。
另外,消息头里还有一个Message ID字段,由Primary消息发起方分配,通常是自增的。Secondary消息必须把Message ID原样带回,这样发起方才能把请求和响应配对。高并发场景下如果Message ID管理不当,很容易出现响应错配的问题。
2.3 10字节消息头逐字节拆解
以HSMS传输的SECS-II消息为例,一条消息的完整结构是:4字节的总长度 + 10字节的消息头 + 可选的Data区。消息头的10字节分布如下:
| 字节位置 | 字段 | 说明 |
|---|---|---|
| 0-1 | Session ID | 会话ID。对SECS-II消息来说,通常就是Device ID(设备ID) |
| 2 | Stream | 低7位是Stream号,bit7是W位 |
| 3 | Function | Function号 |
| 4-5 | Message ID | 消息ID,大端序,由请求方分配 |
| 6 | PType | 消息类型,0表示SECS-II消息 |
| 7 | SType | 0表示SECS-II业务消息,非0表示HSMS控制消息(如Select、Deregister) |
| 8-9 | 保留 | 通常为0 |
这里特别容易混淆的是Session ID和Device ID的关系。在绝大多数设备对接场景里,一个TCP连接只对应一台设备,Session ID就等于配置的Device ID;但对支持多路复用(一个Host连接多个设备)的HSMS实现来说,Session ID用于区分不同设备。如果你的设备规格书里提到“Session ID”和“Device ID”,一定要确认清楚设备侧对这两个值的处理方式。
2.4 数据项与格式码:消息正文的编码规则
SECS-II消息的Data区由若干个数据项(Data Item)组成,每个数据项由三部分组成:格式码(Format Code)、长度(Length)、数据(Data)。常见的格式码如下:
| 格式码 | 类型 | 示例说明 |
|---|---|---|
| 0x20 | List | 列表,可包含多个子数据项 |
| 0x25 | ASCII | 字符串,如设备型号“EQUIP-01” |
| 0x29 | Binary | 二进制数据 |
| 0x40 | INT1(1字节有符号整数) | 小整数 |
| 0x41 | INT2(2字节有符号整数) | 短整数 |
| 0x42 | INT4(4字节有符号整数) | 常规整数 |
| 0x44 | UINT1(1字节无符号整数) | 通常用于状态码 |
| 0x48 | FLOAT4(4字节浮点数) | 测量值等 |
长度字段的编码规则稍微特殊:当数据长度小于256字节时,用一个字节表示;当长度超过255字节时,用三个字节表示,第一个字节固定为0x81,后两个字节是大端序的真实长度。实战中很多报文长度并不长,但如果你处理的设备有大量配方数据或历史追溯数据,就一定会遇到3字节长度编码的情况。
2.5 一条SML消息长什么样
设备厂商通常会提供一份SML文件(SECS Message Language,SECS消息语言),用来描述设备支持的消息模板。SML内容大概长这样:
S1F13 { <MDLN "EQUIP-01"> <SOFTREV "1.0.0"> }这表示S1F13这条消息携带两个数据项:MDLN(设备型号)和SOFTREV(软件版本),都是ASCII字符串。在开发EAP或设备端软件时,SML文件是双方核对消息结构的“合同”。收到一份SML后,先不要急着写代码,把每个消息的Stream/Function、数据项名称、类型、顺序都逐条理清楚,能省掉后续大量联调时间。
3. 开发前的准备工作
3.1 明确你的角色:设备端还是主机端
开发SECS通信程序,第一步不是写代码,而是搞清楚你站在协议交互的哪一边。如果做的是设备端,你的任务是响应Host的各种请求,主动上报设备状态;如果做的是Host端(EAP),你的任务主要是监听设备连接、解析设备上报的数据、下发控制指令。角色不同,选用的开发库、关注的消息类型、调试手段都会不一样。
这里特别想说一句:很多工程师在刚接触SECS时,会把大量精力花在研究协议的细枝末节上,结果迟迟写不出一个能通信的程序。正确的做法是先用最简单的方式跑通一条消息(比如S1F13/S1F14),再逐步扩展业务功能。协议细节是在踩坑中慢慢熟悉的,不是靠死啃文档啃出来的。
3.2 开源库和商业SDK怎么选
市面上主流的SECS/GEM开发方案分两类:
- 开源库:比如Python的secsgem、C#的Secs4Net、Java的某些开源HSMS实现。优点是免费、源码透明、可以自己调试底层细节;缺点是文档参差不齐,遇到问题需要翻源码。
- 商业SDK:比如很多半导体设备厂商和EAP集成商使用的商业组件,优点是有技术支持、稳定性验证过;缺点是收费不低,而且有些闭源组件出了问题不好排查。
对学习阶段和Demo验证来说,开源库完全够用。我在后面的示例中故意用“裸socket”方式实现,目的就是让你看清消息组装和解析的每一步,搞懂原理后再用开源库就非常快了。
3.3 搭建一个最小联调环境
在写代码之前,建议先准备好三样东西:
- SECS/GEM模拟器。网上能搜到不少免费的模拟器,有些可以直接模拟设备端,有些可以模拟Host端。建议至少准备两种:一个用来模拟对端,一个作为调试参考。
- 抓包工具。Wireshark对HSMS这种基于TCP的协议非常有用,可以按端口过滤,直接看到消息的十六进制内容。必要时写个小脚本把报文按HSMS格式解析出来,方便核对。
- 一份待对接设备的SML或通信规格书。如果没有真实设备,就从模拟器自带的SML入手。
我的经验是,环境搭建阶段就花时间把模拟器的连接、消息发送、断线重连等基本操作玩熟,后面面对真实设备时心里会稳很多。很多联调现场的问题,本质上就是“对端协议行为和预期不一致”,而模拟器能帮你提前预演这些差异。
4. 用Python跑通第一个SECS通信
4.1 通信流程速览:先握手,再干活
在最常见的设备主动连接Host场景下,完整通信流程是:
- 设备端和Host建立TCP连接。
- 设备端发送HSMS控制消息Select Request,请求建立HSMS会话。
- Host回复Select Response,结果码为0表示成功。
- 设备端发送S1F13(通信建立请求)。
- Host回复S1F14(通信建立确认)。
- 握手完成,双方开始收发S6F11、S2F17等业务消息。
下面我用Python的socket实现一个最小Host端Demo,重点演示第4、5步。为了让你看清消息结构,这里不依赖任何SECS库,直接操作字节。
4.2 最小Host端实现(可运行的Demo)
import socket import struct LISTEN_IP = "0.0.0.0" LISTEN_PORT = 5000 def recv_n(conn, n): data = b"" while len(data) < n: chunk = conn.recv(n - len(data)) if not chunk: raise ConnectionError("connection closed by peer") data += chunk return data def recv_hsms(conn): # 先读4字节总长度,再读完整消息 len_bytes = recv_n(conn, 4) total_len = struct.unpack(">I", len_bytes)[0] message = recv_n(conn, total_len) header = message[:10] body = message[10:] return header, body def send_hsms(conn, header, body=b""): packet = struct.pack(">I", 10 + len(body)) + header + body conn.sendall(packet) # 创建TCP服务端 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((LISTEN_IP, LISTEN_PORT)) server.listen(1) print("Host listening on port", LISTEN_PORT) conn, addr = server.accept() print("Device connected from", addr) # 第1步:接收Select Request(HSMS控制消息,SType=1) header, body = recv_hsms(conn) session_id, stream_fn, func, msg_id = struct.unpack(">HBBH", header[:6]) s_type = header[7] print("Select Request, SType=%d, MsgID=%d" % (s_type, msg_id)) if s_type == 1: # 回复Select Response,SType=2,Body为一个字节0表示成功 resp_header = struct.pack(">HBBH", 0xFFFF, 0x00, 0x00, msg_id) + b"\x00\x02\x00\x00" send_hsms(conn, resp_header, b"\x00") # 第2步:接收S1F13(SECS-II业务消息,SType=0) header, body = recv_hsms(conn) session_id, stream_fn, func, msg_id = struct.unpack(">HBBH", header[:6]) w_bit = (stream_fn & 0x80) >> 7 stream = stream_fn & 0x7F print("Received S%dF%d, W=%d, DeviceID=%d" % (stream, func, w_bit, session_id)) if stream == 1 and func == 13 and w_bit == 1: # 第3步:回复S1F14,W位清0,Message ID原样返回 resp_header = struct.pack(">HBBH", session_id, 0x01, 0x0E, msg_id) + b"\x00" * 4 send_hsms(conn, resp_header) print("Replied S1F14") # 保持连接,等待后续消息 try: while True: header, body = recv_hsms(conn) session_id, stream_fn, func, msg_id = struct.unpack(">HBBH", header[:6]) stream = stream_fn & 0x7F w_bit = (stream_fn & 0x80) >> 7 print("Received S%dF%d, W=%d, body_len=%d" % (stream, func, w_bit, len(body))) except Exception as e: print("Connection closed:", e) finally: conn.close() server.close()这段代码的核心逻辑是:先等TCP连接,然后依次处理Select Request和S1F13,最后回复S1F14。代码里最关键的几个点,再提醒一下:
- 消息头第8和第9字节覆盖了SType和保留字段,发送Select Response时Header要拼成
struct.pack(">HBBH", ...) + b"\x00\x02\x00\x00",顺序是PType=0、SType=2、保留=0。写错一个字节,对端就直接丢弃消息。 - S1F14的Session ID要回显设备发来的值,而不是生硬地用配置的Device ID。虽然在单设备场景下两者相等,但做个好习惯,回显更稳妥。
- W位处理是消息方向的关键,用来解析请求/响应关系。回复时W位一定要置0,否则对端会认为你又发起了一个新的Primary消息,导致无法成功配对。
4.3 设备端需要做的事
设备端的逻辑和Host端是对称的:设备先主动连上Host,发送Select Request,然后发送S1F13,等待Host回复S1F14。设备端代码这里不展开,因为核心的字节拼装逻辑和Host端几乎一致,区别在于:
- 设备端Socket角色的目标是Host的监听端口,所以写的是
connect()而不是accept()。 - 设备端在发送Select Request时,Message ID可以自己从1开始递增。
- 设备端在收到S1F14后,需要检查W位是否为0、Message ID是否和S1F13一致,确认无误才算握手成功。
4.4 抓包验证:用Wireshark看清消息长什么样
跑通Demo之后,强烈建议你用Wireshark抓一次包。过滤条件直接写tcp.port == 5000,然后逐条看TCP payload,对照上面代码里的字节结构,一目了然。
正常情况下你会看到四条消息:
| 方向 | 内容 | 说明 |
|---|---|---|
| Equipment → Host | Select Request | 10字节头,SType=1 |
| Host → Equipment | Select Response | 10字节头 + Body(0x00) |
| Equipment → Host | S1F13 | 10字节头 + 两个ASCII数据项(MDLN、SOFTREV) |
| Host → Equipment | S1F14 | 10字节头,无Body |
如果抓包时发现S1F13的Body里数据项顺序或编码不对,用前面讲的格式码规则逐一对照,通常能很快定位问题。我见过不少同事在联调时靠grep打印日志排查,其实把Wireshark报文和代码日志结合起来看,效率会高出一大截。
5. 开发中常见的坑与排查技巧
5.1 连接失败类的坑
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备连接不上Host端口 | Host端口没监听,或防火墙拦截 | 确认监听状态,临时关防火墙验证 |
| 连接建立后立刻断开 | Select流程没有完成,或T7超时 | 抓包看是否收到Select Response和S1F13 |
| Select Response发出去设备不认 | Header里SType字节拼错 | 用Wireshark对照标准,确认SType=2 |
| 设备反复重连 | T5(连接失败重试间隔)设置太短 | 查看设备端日志,适当调大连接重试间隔 |
5.2 消息配对和超时:T3和T7最让人头疼
SECS协议定义了一组定时器,这里重点讲两个:
- T3:设备发出一条Primary消息后,等待Secondary响应的超时时间,典型值是45秒。如果Host业务处理太慢,超过T3还没回复,设备会认为消息丢失并主动断开连接或重发。EAP端如果发现设备频繁断连,先看是不是T3超时了。
- T7:在HSMS连接上,如果双方在一定时间内(典型值120秒)没有发送任何消息,连接会被认为失效。所以Host和设备之间往往需要心跳消息来保活。常见的做法是周期发送S1F3(设备状态请求)或者S2F17(日期时间请求),把连接维持在活跃状态。
我踩过一个真实的坑:在一台设备对接项目中,设备的T3配置特别短(只有15秒),而我们的EAP在收到S6F11事件后,因为上游系统响应慢,处理时间超过了T3,结果设备端不断重发事件,数据库里出现大量重复报警记录。后来在EAP侧优化了事件处理链路的响应速度,并把设备端T3适当调大,问题才消除。排查这类问题,关键是看设备端日志里是否有“Wait Reply Timeout”之类的关键词。
5.3 消息格式和业务语义的坑
- Format Code不匹配:设备发的数据项类型和SML里定义不一致,例如SML写的是ASCII“123”,设备实际发来的是INT4数值123。解析时如果直接强转,轻则拿到乱码,重则触发异常。所以解析消息体时要做类型检查,不能假设对端一定按文档来。
- List嵌套层级不对:有些消息的数据项是嵌套的List,比如S6F11里每个事件项都是一个List,里面又有多个数据项。层级少套一层或多套一层,解析结果完全不对。写解析代码时建议先打印原始数据项的层级结构,再逐层处理。
- 设备ID没对上:Device ID配置错是最常见的问题。Host配置的设备ID和设备侧发来的不一致,消息会被直接丢弃。联调时第一个要核对的就是这个东西。
5.4 找不到问题时的兜底办法
如果前面所有排查手段都用了还是找不到问题,我建议你做两件事:
- 用模拟器复现同样的交互流程。把真实设备替换成模拟器,跑相同的消息序列,看模拟器表现是否和真实设备一致。如果模拟器能正常交互而真实设备不行,问题大概率在设备侧实现。
- 抓包对比标准消息结构。把抓包得到的十六进制字节手工解码,逐字节对照SGEM标准文档或SML。很多时候,问题就出在一个毫不起眼的字节上,比如长度字段用了几字节、某个保留位没有置0、Header里的Message ID没有回显。
写在最后
第一次跑通S1F13/S1F14的时候,可能感觉也就那样,不就是几条TCP消息嘛。但实际上,这套“裸socket”走一遍的价值在于:你真实地看见了消息头的每一个字节、W位的置位和清零、Message ID的回显规则,这些细节是你在用开源库时被隐藏掉的。后续不管你是转向secsgem这类封装好的库,还是直接基于厂商SDK开发,对消息结构的理解都会成为你排查问题时最硬的本钱。
我个人在实际项目中还有一个习惯:每接触一种新设备,第一件事就是把设备的SML文件里所有消息过一遍,把常用的S1、S2、S5、S6、S7系列消息整理成一个速查表,标注好消息方向、数据项格式和业务含义。对接的设备多了以后,你会发现大部分问题不是协议本身有多难,而是细节的差异。下一期我会挑一条业务消息(比如S6F11事件上报)来完整讲一讲从设备上报到Host解析、入库的全过程,顺便聊聊EAP侧消息处理线程模型怎么设计才不容易被协议细节堵死。