
做半导体设备自动化的兄弟对SECS/GEM这组词应该都不陌生。它是半导体设备与工厂主机之间通信的事实标准从晶圆制造到光伏、面板几乎所有需要接入产线自动化的设备最后都会被问到同一个问题“你们支不支持SECS/GEM”这次要聊的是我平时调试设备通信自己写的一个SECS/GEM模拟器。这个模拟器能在没有真实设备、没有真实主机的情况下把整套SECS/GEM的消息交互在本地跑通无论你是调试设备端GEM功能、验证Host侧代码还是给新同事做入门培训都能派上用场。项目标题里写的是“SeceGem”这其实是SECS/GEM的常见笔误下面我统一用SECS/GEM来讲。1. 先把SECS/GEM这几个标准拆开讲明白1.1 四个E系列标准各管一段很多刚入行的朋友会把SECS/GEM当成一个协议其实它是SEMI组织制定的一整套E系列标准的统称至少包含四个部分SECS-ISEMI E4、SECS-IISEMI E5、GEMSEMI E30和HSMSSEMI E37。SECS-I最早基于RS-232串口的传输层协议定义了数据块怎么在物理线路上传输包括块号、重传机制。现在新设备基本不太用但存量老设备里还有不少。HSMS基于TCP/IP的传输层协议可以看成SECS-I的以太网替代版。它定义了数据帧格式、连接建立机制、心跳检测是目前绝大多数设备联网的首选。SECS-II消息层协议定义了消息的具体内容也就是Sx Fy这种格式比如S1F13是建立通信请求S6F11是事件报告。它有一套完整的数据类型系统List、ASCII、Binary、U1/U2/U4/U8等相当于规定了“话怎么说”。GEM设备行为标准定义了设备端在什么状态下应该干什么包括状态模型、事件采集、远程命令、报警管理、配方管理、数据变量等。它规定的是“设备在什么时机说什么话”。用一句不严谨但很好记的话总结SECS-I和HSMS管网络传输SECS-II管消息格式GEM管设备行为。四者合在一起才构成一个完整的设备自动化通信方案。1.2 一次标准的通信建立流程是怎么走的不管设备还是主机只要遵循这套标准通信建立的流程基本都是固定的。以HSMS为例大致是这么几步TCP连接建立后主机先发Select.req控制消息设备回Select.rsp表示同意建立会话。这相当于双方确定“咱俩可以开始聊了”。随后设备或者主机可以发起S1F13建立通信请求对端回S1F14携带设备型号、软件版本这类信息。这一步做完双方才知道对面是什么设备、协议版本是否兼容。之后再进入正常的业务交互设备上报事件、主机下发远程命令、双方互相传数据变量等。期间会定期发Linktest.req和Linktest.rsp来检测链路是否存活防止TCP连接断了但双方都不知道。这里要特别提醒一点S1F13和Select不是一回事。Select是HSMS传输层的会话建立S1F13是SECS-II消息层的通信建立。前者相当于先拨通电话后者相当于接通后验证一下“喂你是哪位”。很多新手第一次抓包会混淆以为Select之后就完事了结果发S1F13一直收不到回复就是这个顺序没理清。1.3 为什么这套标准如此难绕开半导体行业的产线高度自动化晶圆在设备之间流转完全靠MES、APC、FDC这类主机系统统一调度。设备必须把自己的状态、工艺结果、报警信息实时同步给主机同时接收主机的开始、停止、配方下发等指令。这些东西如果用各家私有协议实现每接一种设备就得做一次定制开发成本高到离谱。所以SEMI才搞出这么一套统一标准。客户和设备供应商约定好“照这个规范来写”对接效率才能上去。实际项目里设备供应商和客户往往处于不同开发节奏设备端功能开发完了客户的主机环境还没就绪或者主机侧的联调排期只能约到几周后。这时候如果手里没有一个趁手的模拟器所有验证只能干等。这也是我做这个模拟器的直接动机让自己在设备端开发阶段就能把GEM行为和消息格式都验证到位。2. 模拟器的设计与功能定位2.1 设备端模式没有Host也能把GEM功能跑起来这个模拟器的第一个核心定位就是模拟设备端。你在本地跑起一个设备端实例后它会监听一个TCP端口等待主机连接然后自动完成Select、S1F13这类标准握手流程。设备端模式下模拟器会实现一套完整的GEM行为。比如收到S2F41远程命令时根据命令名START、STOP、RESET等走对应的分支逻辑然后回S2F42告诉主机命令执行结果。再比如收到S1F1询问设备状态时能按照E30定义的状态模型返回当前状态而不只是回一堆固定字节。这一点很重要因为GEM的标准不是“消息能兑上就行”还需要设备在正确的状态下做正确的响应。这个模式最适合谁用如果你是设备端的开发工程师或者正在给客户演示设备通信能力这个模式能帮你把“缺失的对端”补出来。2.2 Host模式反过来验证设备端行为除了模拟设备这个模拟器还支持Host模式也就是反过来当主机用。在这个模式里你不需要写任何代码通过界面或命令行就能主动发起S1F13、S2F41、S1F1这些消息用来测试真实设备的回应是否正确。举个例子你开发了一台设备客户要求设备收到S2F41的START命令后要执行特定动作并在完成后上报一个ProcessStart事件。你可以在Host模式里手动发一条S2F41观察设备返回的S2F42是否在合理时间内到达、返回码是否正确再检查设备之后是否主动上报了S6F11事件事件里的数据项对不对。双向都能模拟是这个工具在实际项目里最大的价值。我在本地做消息流程验证时经常是开两个实例一个跑Host模式、一个跑设备端模式两边对发消息比在真实产线上反复试错效率高得多。2.3 为什么用C#来做技术选型上我选了C#主要是三个原因。第一个是生态。工厂自动化领域的上位机程序Windows C#的组合太常见了后续如果你要把这个模拟器集成进自己的工具链、包一层UI或者跟MES系统打通C#都顺手。第二个是开发效率。C#的异步编程模型TcpClient/TcpListener在处理网络协议栈时非常舒服配合async/await写收发逻辑不会像纯同步阻塞那样容易卡界面。第三个是部署方便。打出来的程序直接扔到Windows服务器上就能跑客户现场没有开发环境也能用。当然这不是说其他语言不行。Python写起来更快Go部署起来更简单但就“工业上位机生态”这个场景来说C#至今还是最稳的选择。3. 模拟器实操从配置到跑通一次完整通信3.1 环境准备与关键参数配置这个模拟器基于.NET 8开发所以你要先装好.NET 8 SDKWindows、Linux都行不过重点还是Windows环境。代码本身不依赖第三方库全部基于Socket原生实现。拿到程序后先看配置文件重点参数有这几项参数说明我的推荐值ModeEquipment设备端或Host主机端EquipmentListenIp监听IP0.0.0.0设备端模式ListenPort监听端口5000RemoteIp / RemotePort对端IP和端口主机模式使用127.0.0.1:5000DeviceId设备ID也就是HSMS头里的SessionID101ConnectModeActive主动连接还是Passive被动监听PassiveLinktestInterval心跳间隔秒30设备ID这里多说两句。HSMS帧头里有一个SessionID字段主机侧用它区分是哪个设备发来的消息。同一个Host下面往往挂着几十台设备每台都要有一个唯一ID。实际对接时客户会给你分配一个设备ID千万别所有设备都用同一个。连接模式也很关键。如果Host侧是主动连设备那设备端必须设成Passive模式并监听端口如果设备需要主动上报到Host那设备端要设成Active模式去连Host的端口。这两种模式在Select握手时行为略有不同但最终都会进到同一个通信状态。3.2 启动连接看完一次Select和S1F13/S1F14在设备端模式下启动模拟器后控制台会打印监听中的日志。比如[Info] Equipment simulator started. [Info] Listening on 0.0.0.0:5000, DeviceId101 [Info] Waiting for Host connection...这时候你需要一个TCP客户端去连接它。最简单的办法是写一个十几行的Python脚本但如果想直观看到每次交互的数据我建议配合Wireshark一起用过滤条件写tcp.port 5000能清楚看到连接建立和Select消息的时序。当Host连接上来后模拟器日志会显示[Info] TCP connection established from 127.0.0.1:51234 [Info] Received Select.req, sending Select.rsp接下来Host发S1F13模拟器回S1F14[Info] Received S1F13 W [Info] Send S1F14 W [Info] Body: L [A SimDevice A 1.2.0]从日志里能看到S1F14回复的是一个List里面有两个ASCII字符串分别对应设备型号和软件版本。客户主机侧对这两个字段往往有严格校验值必须写在GEM映射表里模拟器里的配置项可以手动改。3.3 核心代码HSMS消息解析与S1F14应答要说模拟器里最关键的部分就是HSMS消息头的解析与构造。HSMS数据消息的报头固定是10字节字节1-2SessionID大端高字节在前字节3Stream编号比如S1F13的Stream就是1字节4Function编号S1F13的Function是13字节5高7位是PType0表示SECS-II最低位是W位1表示需要回复字节6SType0表示数据消息控制消息如Select.req是1Select.rsp是2Linktest.req是5字节7-10SystemBytes用于关联请求与响应网络传输用大端字节序而C#和Windows默认是小端所以代码里不能直接BitConverter得手动拼。public static HsmsMessage ParseHeader(byte[] buffer, int offset) { var msg new HsmsMessage(); msg.SessionId (ushort)((buffer[offset] 8) | buffer[offset 1]); msg.Stream buffer[offset 2]; msg.Function buffer[offset 3]; msg.WBit (buffer[offset 4] 0x80) ! 0; msg.SType (byte)(buffer[offset 5] 0x7F); msg.SystemBytes ((uint)buffer[offset 6] 24) | ((uint)buffer[offset 7] 16) | ((uint)buffer[offset 8] 8) | buffer[offset 9]; return msg; }构造回复时要特别注意把请求里的SystemBytes原样带回来。这是SECS/GEM回复最基本的要求主机侧靠这个字段配对请求和响应对不上就乱套。public byte[] BuildS1F14(ushort deviceId, uint systemBytes) { byte[] body new byte[] { 0x01, 0x0C, 0x01, 0x02, 0x53, 0x69, 0x6D, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x01, 0x06, 0x31, 0x2E, 0x32, 0x2E, 0x30 }; byte[] header new byte[10]; header[0] (byte)(deviceId 8); header[1] (byte)(deviceId 0xFF); header[2] 0x01; header[3] 0x0E; header[4] 0x81; header[5] 0x00; header[6] (byte)(systemBytes 24); header[7] (byte)(systemBytes 16); header[8] (byte)(systemBytes 8); header[9] (byte)(systemBytes 0xFF); byte[] frame new byte[4 header.Length body.Length]; int bodyLen header.Length body.Length; frame[0] (byte)(bodyLen 24); frame[1] (byte)(bodyLen 16); frame[2] (byte)(bodyLen 8); frame[3] (byte)(bodyLen 0xFF); Buffer.BlockCopy(header, 0, frame, 4, header.Length); Buffer.BlockCopy(body, 0, frame, 4 header.Length, body.Length); return frame; }S1F14的枚举号是Stream 1 Function 14十六进制写0x01 0x0E。header[4] 0x81表示PType0、W位1需要对方回复。body是SECS-II编码的数据内容这里按L、A、A的格式手动构造了三个元素。实际项目里你大概率会自己写一套SECS-II编解码器来干这个活否则每种消息都手拼字节太容易出错。3.4 用Python快速写一个测试Host如果你只是想验证设备端模拟器的基本链路不想一上来就用C#写完整主机可以拿Python写个几十行的最小Host。不需要第三方库纯socket就行。import socket import struct def send_hsms(sock, device_id, stream, func, wbit, sysbytes, bodyb): header struct.pack(HBBBBL, device_id, stream, func, (0x80 if wbit else 0x00) | 0x00, 0x00, sysbytes) length len(header) len(body) sock.sendall(struct.pack(L, length) header body) def recv_hsms(sock): head b while len(head) 4: head sock.recv(4 - len(head)) (length,) struct.unpack(L, head) data b while len(data) length: data sock.recv(length - len(data)) sid (data[0] 8) | data[1] stream data[2] func data[3] sysbytes struct.unpack(L, data[6:10])[0] return sid, stream, func, sysbytes, data[10:] sock socket.create_connection((127.0.0.1, 5000), timeout5) _, _, _, _, _ recv_hsms(sock) # 应收到 Select.req这里先不细看 # 回复 Select.rsp sock.sendall(struct.pack(LHBBBBL, 10, 0xFFFF, 0, 0, 0, 2, 1)) send_hsms(sock, 101, 1, 13, True, 0x0000ABCD) sid, stream, func, sysbytes, body recv_hsms(sock) print(Response: S%dF%d SysBytes0x%08X Body%s % (stream, func, sysbytes, body.hex()))这个脚本干了两件事先回复模拟器的Select.req然后主动发一条S1F13最后打印设备的S1F14响应。跑通之后说明HSMS链路和消息层都没问题。3.5 验证GEM关键功能时的测试清单链路通了之后接下去不要急着测业务先把GEM几个核心功能过一遍。我实际测试的时候习惯按下面这个清单来测试项预期行为模拟器侧表现Select握手Select.req后收到Select.rsp日志显示握手成功S1F13 / S1F14设备返回型号和软件版本回复两个ASCII元素S1F1 / S1F2设备返回当前状态按内部状态机返回S2F41 / S2F42远程命令执行与结果返回按命令名分发返回对应ACKS6F11 / S6F12设备上报事件Host确认收到S6F12且SystemBytes匹配S5F1 / S5F2报警上报与确认报警ID、严重级别能对上Linktest周期触发心跳双方互发Linktest.req/rsp这七个测试项能覆盖绝大多数设备对接的日常场景。如果这些都能稳定通过基本可以判断这个设备端的GEM实现没有大问题。4. 模拟器使用中的高频问题与排查实录4.1 高频问题速查表用了一段时间后我把大家在群里问得最多的问题整理成了下面这张表问题现象可能原因排查方向一直连不上设备端口防火墙拦截或IP/端口配错先telnet试端口通不通Select发出后无人应答设备端未启动或连接模式不匹配检查Passive/Active设置S1F13发出去石沉大海消息解析异常或对端等到body时错位抓包看报文长度字段回复消息对不上请求SystemBytes没有原样复制检查回复构造代码通信一段时间后自动断开心跳超时或KeepAlive没配置看Linktest间隔配置收到消息内容解析乱码SECS-II数据类型长度解析错误检查数据项的格式字节设备不愿意响应命令状态模型不对不处于ONLINE状态查看当前状态和允许动作4.2 深度排查连不上和Select超时连不上首先要区分是TCP层问题还是HSMS层问题。在终端跑一下telnet 127.0.0.1 5000如果端口都不通说明还没走到协议层直接查防火墙、监听地址和端口占用。如果telnet通了但Select没有回应那就关注两个地方一是连接模式两边必须一个Active一个Passive二是设备ID控制消息的SessionID固定是0xFFFF如果代码里把控制消息也按业务消息处理设备绝对不会正确响应。我实际遇到过一个很隐蔽的问题Windows防火墙默认会拦截入站端口本地测试时走了回环地址没问题一放到局域网里就全连不上。后来发现是监听IP写成了127.0.0.1只监听本地回环了。改成0.0.0.0才正常。这个坑日志里不会有任何提示只能靠检查监听IP排查。4.3 深度排查半包黏包导致消息错位这是所有TCP协议实现里最容易踩的坑SECS/GEM也不例外。TCP是流协议它不保证你每次recv恰好收到一整个HSMS消息。可能一次recv收到半个消息也可能攒了好几个消息一起到达。如果代码里按recv返回的长度直接解析轻则解析错乱重则整个通信链路报废。正确姿势是维护一个接收缓冲区先把数据塞进去然后循环尝试按头部的前4字节长度字段切包直到缓冲区内数据不足一个完整的消息再等下轮。我见过不少项目因为这个原因表现为“时好时坏”本地压力小的时候正常一旦消息发得频繁就偶发解析错误。针对这个现象第一反应就应该是查半包黏包处理逻辑而不是去怀疑协议栈写错了。4.4 深度排查W位和SystemBytes错配W位的意思是这个消息需要对方回复。模拟器收到W1的消息后必须在合理时间内给出对应响应。但很多人不理解回复时要把请求的SystemBytes原样带回去这是SECS/GEM的硬性规则。如果回复时随便填一个SystemBytes对端会认为这不是本次请求的响应从而一直等待直到超时。在我们这个模拟器里对SystemBytes的校验做得很严格收到的每个请求都会缓存SystemBytes回复时自动带上一旦发现对端发来的响应SystemBytes没有对应关系就直接在日志里报警。开发时我也建议你用同样的校验逻辑写好这个习惯能帮你少排查很多诡异问题。4.5 深度排查GEM状态模型对命令的限制不少新手会把《模拟器能收到消息》等同于《GEM流程正常》这是个大误解。SECS/GEM里有个核心概念叫状态模型设备只有处于ONLINE状态时主机才能下发某些工艺相关的远程命令如果设备还处于OFFLINE或者本地模式即使消息链路完全正常设备也应该拒绝执行工艺命令并在S2F42的返回码里说明原因。模拟器里这一块被我特意做成严格模式设备初始状态是OFFLINE手动切到ONLINE之前发S2F41的START命令会收到带错误码的回复。这个设计不是为了给自己找麻烦而是为了尽可能贴近真实设备免得你在模拟器上验证通过、上到真设备后反而懵了。真实GEM设备里状态机管理往往比模拟器复杂得多但基本思想完全一致。5. 写了这个模拟器后的几点体会如果你问我这套东西最值钱的部分是什么我的答案不是那些消息解析代码而是“你自己亲手跑通过一次完整交互”的感觉。SECS/GEM这个协议光看规范文档非常枯燥E30那几百页足够把人劝退。但当你用模拟器亲眼看到Select.rsp从对端返回、看到S1F14里回来自定义的数据内容时很多抽象概念一下就落地了。我建议新接手设备对接项目的朋友都自己写一个最小可跑的模拟器哪怕功能很粗糙。这个过程会逼你把HSMS帧结构、SECS-II数据格式、GEM的消息时序全部摸清楚。等你把这些基础打牢再去调真实设备会发现很多问题看一眼日志就能定位效率完全不一样。实践下来这套理论配合模拟器是理解SECS/GEM最快的一条路。