简介:半导体制造中,设备与MES系统的稳定通信是产线连续运行的关键,而SECS/GEM协议正是解决设备接口碎片化的行业标准。它通过E37(HSMS)建立基于TCP/IP的高速传输通道,配合E5(SECS-II)完成消息的编解码,同时以E30(GEM)定义设备状态与行为模型。理解这套协议栈的原理,有助于工程师掌握设备与Host之间连接建立、心跳保活、消息帧封装及数据交互的完整链路。在实际应用中,无论是配方下发、过程数据采集还是报警上报,都依赖这套标准的可靠性。本文基于JngHightSpeedSecs开源实现,拆解协议分层结构、建连流程、消息处理机制和高性能设计要点,并结合常见故障排查经验,为半导体设备通信二次开发提供可落地的工程参考。 做半导体设备软件的朋友,对SECS/GEM这几个字母肯定不陌生。设备要跟MES(制造执行系统)通信,跑配方、报数据、传履历,绕来绕去都绕不开这套标准协议栈。我最近在梳理一套名为JngHightSpeedSecs的SECS/GEM源代码,把E4、E5、E30、E37那套东西用一套高性能内核重新实现了一遍,既有完整的SECS-II消息编解码,也有GEM状态模型。这篇文章就把这套源码的协议栈结构、建连流程、消息处理机制和常见坑位拆开讲清楚,适合正在做设备端通信模块二次开发的工程师,也适合想弄懂设备与Host之间到底在传什么的新手。
1. 这套源码解决的核心问题:为什么SECS/GEM绕不开
1.1 半导体制造里设备通信的真实痛点
半导体前道、后道设备,包括刻蚀、薄膜沉积、光刻、清洗、测试机、分选机,几乎都要与工厂的MES或Host系统通信。通信的内容不只是“设备开没开机”,而是要把完整的工艺配方下发给设备,把每片晶圆的过程数据、测量结果回传给Host,还要上报报警、跟踪设备状态。这个通信链条一旦出问题,产线上就是大批量停线,所以工厂对这个通道的稳定性要求极其苛刻。
问题在于,设备来自不同厂商,每个厂商的软件风格都不一样。如果没有一个统一标准,MES厂商就得为一台刻蚀机写一套接口,再为一台测试机写另一套接口,维护成本完全失控。SECS/GEM协议就是为了解决这个“接口碎片化”而生的行业标准。它定义了设备与Host之间怎么建立连接、消息怎么封装、数据用什么类型表示、设备状态怎么切换,让来自不同厂商的设备能用一个标准接口接入同一个工厂系统。
JngHightSpeedSecs这套源码,本质上就是把这套标准协议栈从零实现了一遍。它不是给业务逻辑用的框架,而是通信底座,负责把“底层TCP/IP的数据流”翻译成“上层业务看得懂的SECS消息”,同时负责心跳、重连、超时、消息分片这些脏活累活。
1.2 JngHightSpeedSecs与标准协议栈的对应关系
SECS/GEM标准体系由SEMI组织发布,涉及的核心标准主要有四个:
- SEMI E4:SECS-I,基于RS-232的半双工通信协议,早期老设备用得比较多。
- SEMI E5:SECS-II,定义了消息内容的数据格式和语义,也就是消息体怎么编码。
- SEMI E30:GEM,定义了设备行为模型,包括状态模型、报警管理、配方管理、事件管理、数据收集等。
- SEMI E37:HSMS,基于TCP/IP的高速通信协议,现代设备基本都走这套,替代了老的RS-232方案。
JngHightSpeedSecs这个名字里的HighSpeed,指的就是它直接以HSMS(SEMI E37)为传输层,而不是老旧的SECS-I。整套源码在处理消息时,把E5的SECS-II格式编解码和E37的HSMS传输层都做了模块化封装,同时向上层提供了E30 GEM级别的语义支持。
从代码层面看,这套源码大致可以分为三层。最底层是TCP通信层,负责socket管理、数据收发、断线重连;中间层是HSMS协议层,负责处理Select、Linktest、Separate这些控制消息,以及把上层数据消息封装成HSMS帧;最上层是SECS-II消息层,负责Stream/Function消息的解析和构造,同时提供GEM状态的维护入口。我把这套源码从头到尾理了一遍之后,发现它的分层思路和商用SDK非常接近,适合拿来当底层库集成到自己的设备软件里,也可以做协议学习参考。
2. 源码层面的核心机制拆解
2.1 消息帧格式与编解码链路
要读懂SECS/GEM源码,第一关就是搞明白消息在网络上到底长什么样。以HSMS为例,一条完整的消息由三部分组成:消息头、消息体、消息长度。
HSMS消息头固定10字节,里面包含Session ID、Stream与Function编号、Reply Expected标志、PType、SType和System Bytes。其中System Bytes是一个四字节的自增序号,用于把请求和回复配对,这个在源码里通常有一个专门的计数器来管理。PType固定为0,表示SECS-II消息;SType为0时是数据消息,为1到9时是控制消息,比如Select.req、Linktest.req这些。
消息体就是SECS-II格式编码的数据。SECS-II的数据格式是一棵带类型的树,可以嵌套,包括List(列表)、ASCII(字符串)、Binary(二进制字节)、Boolean(布尔)、四种整数类型(U1/I1/U2/I2/U4/I4/U8/I8)、两种浮点类型(F4/F8)。源码里的编解码器会递归这棵树,把内存里的对象转换成字节流,接收时再反向解析回来。
我在源码里比较关注的是格式头(Format Header)的处理逻辑。SECS-II规定,每个数据项的格式字节由高四位表示数据类型,低四位表示长度,长度超过255字节时要用长度扩展。如果实现不对,解析中文ASCII字符串或者超过长度限制的配方数据时很容易出错。JngHightSpeedSecs这块处理得比较干净,它把格式编码单独抽成了工具类,读写分开,从源头上避免了编解码状态互相污染的问题。
2.2 传输层状态机与建连流程
HSMS通信不是TCP一连上就能传数据的,它有自己的会话状态机。我刚看这套源码的时候,花了不少时间才把状态迁移理清楚,因为代码里用枚举定义了至少四五个状态,并且通过事件驱动在那几个状态之间跳转。
HSMS的状态大致包括:Disabled(未启用)、Init(初始化)、Not Connected(未连接)、Connected(TCP已连接但未选择)、Selected(会话已选择,可以传数据)。设备端和Host端在建立TCP连接后,必须先完成Select流程,也就是主机发Select.req,从机回Select.rsp,双方进入Selected状态,之后才允许传输数据消息。这套源码里还有一个Communication Retry状态,用于连接中断后按策略自动重连的场景。
这里要重点说一下Select.rsp里的结果码。Header Byte 2和Byte 3的二进制位分别表示是否支持该Session ID和是否已建立连接,源码里对应的校验逻辑如果写得太粗糙,会吞掉合法的Select请求。我在联调时就踩过一次,因为对方Host发的消息里携带了附加的Session ID,而代码里只比较了低16位,导致多会话场景下选不上会话。后面改成按标准注释逐位解析,问题才解决。
除了Select流程,Linktest心跳机制也是源码里必须看明白的部分。Host端通常会按照T7计时器周期性地发送Linktest.req,设备端必须在T3时间内回复Linktest.rsp。如果设备端迟迟不响应,Host会判定链路异常并断开。源码里Linktest的响应逻辑通常在接收线程里同步处理,如果这个线程被阻塞,比如业务回调里做了耗时的文件写操作,心跳就会超时,导致断链。
2.3 高性能设计:为什么它敢叫HighSpeed
既然叫HighSpeed,性能上必然有些设计考量。我看了这套源码之后,归纳出三点比较关键的设计。
第一点是收发分离的双缓冲队列。接收线程只做一件事,从socket读取字节流,按HSMS帧格式拆包,把完整的消息塞进接收队列;业务处理线程从队列里取消息做解析和回调。发送侧同样有独立的发送队列,业务线程调用发送接口后立即返回,不阻塞在socket写操作上。这种设计在批量上传数据、并发请求多的场景下特别有用,能避免业务代码里频繁的锁竞争。
第二点是内存池复用。SECS消息在频繁通信时会产生大量临时对象,如果每次收发都走一次系统内存分配,GC压力和堆碎片都会上来。这套源码在数据缓冲区层面做了预分配和复用,我在代码里看到了默认缓冲区池的实现,长度在几KB范围内的消息基本都能从池里拿内存,用完归还。实测下来,在高频采集场景下内存分配次数明显减少。
第三点是System Bytes的递增管理。每个请求发出时要分配一个同步号,源码里用原子变量来递增,避免多线程环境下的重复分配。这个号一旦重了,请求和回复的匹配关系就乱了,业务层会拿到错误的结果。用原子变量比加锁效率高,也比随机数可靠,因为随机数在极端情况下可能重复,自增号的碰撞窗口基本不存在。
3. 实操:用JngHightSpeedSecs搭一个设备端通信服务
3.1 从源码构建与初始化配置
先把JngHightSpeedSecs的源码工程打开,我拿到的版本是以C/C++为主实现,对外暴露了类似SDK风格的接口,同时提供了一套C#调用示例。如果你要在Windows上跑,直接打开工程文件,依赖项主要是标准网络库,没有额外的第三方组件,编译起来很省心。Linux环境下的CMakeLists文件也是配好的,只需要把编译器切换到C++17标准就行。
编译完成之后,第一个要配置的是设备通信参数。以设备端为例,下面这段初始化配置是这类SDK最常见的用法,我以示意代码说明:
SecsGemDevice device; device.deviceId = 0; // 设备ID,跟Host端约定 device.ipAddress = "192.168.10.20"; // 设备端IP device.port = 5000; // HSMS默认端口 device.connectMode = Passive; // 设备端处于被动监听模式 device.t3Timeout = 45; // 回复超时,单位秒 device.t6Timeout = 5; // 控制消息超时 device.t7Timeout = 10; // 链路空闲心跳间隔 device.linktestEnabled = true; // 开启心跳保持 device.initialize();设备ID不用多说,是设备在工厂里的唯一编号,Host端用它来区分链路上的多台设备。connectMode这里要重点说,SECS/GEM通信里有Active和Passive两种角色,设备软件里通常做成可配置项。主动模式下,设备去连接Host的端口;被动模式下,设备监听自己的端口,等Host来连。产线调试时两种模式各有坑,后面我会单独讲。
配置完成后调用start方法启动服务。源码里这个调用会同时启动接收线程和业务处理线程池,服务就绪后会打一条日志,类似“HSMS service started, waiting for host connection”之类的信息,看到这条日志基本说明底层通信模块已经活了。
3.2 实现设备端S7F1/S7F5通信
跑通基础连接之后,下一步就是让设备能响应Host的请求。我最常用的是配方相关的两类消息:S7F1(Process Program Load Request)和S7F5(Process Program Request)。S7F1是Host通知设备“有一个配方要下发”,设备收到后回复S7F2表示是否接受;S7F5是Host请求设备返回某个配方内容,设备回复S7F6携带配方数据。
这套源码里注册消息处理的模式是典型的观察者风格,伪代码大概是这个样子:
device.registerHandler(7, 1, [](SecsMessage& msg, ReplyContext& ctx) { // 解析请求数据,比如配方ID auto recipeId = msg.body().getList().get(0).asString(); // 检查配方是否存在于设备 bool accept = checkRecipeExists(recipeId); // 构造S7F2回复 SecsMessage reply(7, 2); reply.body() .addBinary(accept ? 0 : 1); // 0接受,非0拒绝 .addList() .addU4(recipeId); ctx.reply(reply); }); device.registerHandler(7, 5, [](SecsMessage& msg, ReplyContext& ctx) { auto recipeId = msg.body().getList().get(0).asString(); RecipeData data = loadRecipe(recipeId); SecsMessage reply(7, 6); reply.body() .addList() .addU4(data.id) .addA(recipeId) .addF8(data.param1) .addA(data.params); ctx.reply(reply); });需要注意,消息处理器运行在工作线程池里,不是在socket接收线程里。这个设计是合理的,因为业务回调经常要做文件读取、数据库查询之类的耗时操作,不能阻塞接收线程。但对应的代价是,如果你的处理器内部访问了共享变量,一样得加锁,别以为回调里就安全了。
另外一个关键点是,业务回调里判断是否“需要回复”。Host发来的请求消息带有Reply Expected标志,源码解析消息头时会把标记存下来,业务回调里通过msg.isReplyExpected()判断。如果Host不期望回复,你还硬回一条,Host会当成意外消息处理,甚至断开连接。我见过有人在S6F11这种事件上报消息上加了回复逻辑,结果Host端日志刷了一堆异常,排查了很久才发现是这里的问题。
3.3 与Host端联调验证的正确姿势
设备端代码写完,必须找一套Host端模拟器来联调。网上常见的SECS/GEM模拟器工具有好几款,比如SECS Simulator、E4/E5/E37协议模拟器之类的。我习惯用一个轻量级Host模拟器来做主测工具,它支持基础的Select、Linktest,能手工构造任意Stream/Function消息,还能记录所有收发消息的内容。
联调第一步,先用模拟器以Active模式连接设备端的Passive监听端口。连接成功后,看模拟器是否自动完成Select交换。正常情况下,日志里会依次出现TCP连接建立、Select.req发出、Select.rsp返回、进入Selected状态这几条记录。如果Select没通过,先检查设备ID是否一致,再检查模拟器和设备端的端口、IP配置是否匹配。
第二步,手工发送一条S1F1(Are You There)消息,设备端应当回复S1F2,携带设备状态和厂商信息。这是最基础的消息,也是判断协议栈通不通的试金石。S1F1通了,再测S7F5和S7F1这些业务消息。每测一个消息,都打开模拟器的原始帧日志,对照SECS-II报文字节逐项看一遍,确认数据类型和长度都符合标准。
联调阶段我建议做一次“断开重连”测试,就是让Host端强制断开TCP连接,观察设备端能否在T5超时后检测到断连,并按重试策略重新监听。JngHightSpeedSecs源码里对断线重连做了状态重置,如果没重置好,会出现连接恢复了但状态还停留在Selected的错误,导致后续消息被静默丢弃。我的经验是,每次断线测试后都主动看一遍设备端的运行日志,确认状态机重置到了Not Connected,再进行下一轮。
3.4 配置与调优参数速查
不同工厂的Host系统对通信参数要求不太一样,我整理了实际项目里经常需要调整的参数和推荐值,你可以直接拿去做参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| T3 | 45s | SECS消息回复超时,设太短容易误判慢速设备 |
| T5 | 10s | 建立TCP连接后,等待Select的超时时间 |
| T6 | 5s | 控制消息(Select/Linktest等)的响应超时 |
| T7 | 10s | 链路空闲时间,超过后主动发心跳,一般10到20秒 |
| T8 | 5s | 连续接收到不完整或异常帧的容忍时间 |
| System Bytes起点 | 随机初始化 | 避免设备重启后与Host端已有缓存的值冲突 |
| 接收缓冲区大小 | 4KB起步 | 配方数据大的话建议调到64KB以上 |
这里特别提醒一下T3和T7的关系。T3管的是数据消息的回复等待时间,T7管的是链路空闲心跳。如果设备在收到数据消息后,业务处理耗时超过了T3,Host那边会判定超时并报错。解决方向有两个,一是把T3调大,二是把耗时操作改成异步处理,先立即回复“收到”,再做实际处理。JngHightSpeedSecs源码里没有限制你必须在回调里同步回复,所以异步回复是完全可以实现的,关键在于ReplyContext的生命周期管理,回调结束后它还活着,你把上下文缓存到别的地方,后续再调用reply方法即可。
4. 常见问题与排查技巧实录
4.1 连接建不上、链路反复断
连接建不上是国产化设备接入SECS/GEM时最常见的故障。我排查这类问题有一个固定套路:先确认物理链路,再抓包看协议交互,最后定位代码逻辑。
第一步,telnet一下端口确认TCP层通不通。如果telnet能通但Select失败,把抓包目标锁定在Select.req/Select.rsp上。最常见的两个原因:一是设备ID不匹配,设备端配置的Device ID和Host端请求的不一致;二是Select.rsp的结果码没有按标准填,尤其是“Connection Not Ready”这个状态,表示接收方认为当前会话还没有准备好,这多半是状态机没有正确进入Connected状态。
链路反复断的情况,大概率是Linktest响应不及时导致的。设备端接收线程如果被业务回调阻塞,在T7间隔内收不到Linktest.req,Host会判断链路死了。我在源码里看到,Linktest处理应该放在接收线程里快速完成,不要在回调里做任何耗时操作。如果已经这么写了还是断,检查一下设备的防火墙策略,有些网络环境会静默丢弃长时间空闲的TCP连接,这时需要把T7心跳调小到10秒以下。
4.2 消息收发超时的排查
消息超时在联调阶段太常见了。现象是Host发S7F5,设备端不回,或者回了但Host在T3时间内没收到。抓包之后通常能看到请求已经到了设备端,回复也发出了,但客户端收不到,或者设备端压根没收到请求。
如果请求到了设备端但没回复,重点检查消息注册表有没有绑定对应的Stream/Function。JngHightSpeedSecs源码里的注册表是按Stream和Function两维索引的,注册错了比如把7F5注册成了7F3,收到请求后就找不到处理器,直接丢弃,Host就会超时。这个坑可以通过日志排查,收到未注册消息时,源码会打印一条warning级别的日志。
如果设备端已经调用了reply,但Host始终收不到,那要看回复消息构造是否正确。比如回复的Stream/Function填反了,S7F6写成了S7F2,Host那边虽然有消息进来,但校验时会当成异常消息丢掉。最有效的方法是直接用模拟器或者Wireshark看原始帧,把消息头10个字节一项项对比标准,很快能找到问题。
4.3 大块数据(比如配方、Trace数据)传输慢
配方数据动辄上百KB,Trace数据在高频采集时更是持续不断,这类大块数据传输容易触发两个问题。第一个是消息长度字段溢出的处理。HSMS消息长度字段占4字节,对普通配方来说完全够用,但如果你把整个Trace数据一次性塞进一条S6F11消息,有些实现会出现长度解析异常。JngHightSpeedSecs在这一点上处理得不错,它会按实际长度动态扩展缓冲区,不需要提前设上限。
第二个是TCP粘包拆包。HSMS协议本身不保证一个TCP包就是一条完整消息,所以接收端必须有按帧长度拆包的逻辑。如果拆包逻辑写错了,小消息看不出问题,大消息一传就乱。源码里拆包的核心是先读4字节长度,再按这个长度读对应的消息体,代码是标准的“读长度-读数据”循环,但前提是接收缓冲区要设置得足够大,特别是多个消息连续到达时。我在实际场景里把接收缓冲区调到了128KB,大块消息传输才稳定。
对大流量Trace数据,我还要提醒一点:避免在SECS消息里传过大的单个数据项。如果一条S6F11里塞了几万个数据点,发射端一次malloc大块内存,接收端也要解析大List,整体效率都会下降。合理的拆分策略是把数据按时间段分成多条消息上报,每次不超过2KB到4KB,既保证了实时性,又降低了单条消息的失败风险。
4.4 与标准Host软件兼容性的几个细节
很多工厂会用美国、韩国厂商的Host软件,这些软件对协议实现的要求非常严格,经常会在别人看不出来细节上卡住。我自己碰到过三个典型的兼容性问题。
第一个,消息头的Device ID字段。虽然大多数场景只用一个设备ID,但Host可能会在消息头里携带Session ID信息,有些实现忽略高字节,有些则要求必须全量匹配。JngHightSpeedSecs源码里对Device ID的比较是全量16位比较,如果遇到只比较低7位的旧Host软件,需要按对方的实现调整。
第二个,ASCII字符串的编码规范。SECS-II标准规定ASCII使用可打印字符,但设备返回的版本号、设备名称里经常包含不可见字符或者UTF-8编码的中文。虽然很多Host软件能容忍,但也有严格的Host会直接拒绝这类消息。我的做法是设备端所有对外ASCII字段统一走英文和数字,如果一定要传中文,优先用Binary类型并附带编码说明,而不是硬塞进ASCII里。
第三个,S6F11等事件消息上报格式。GEM标准对事件上报有一套严格结构,包括DATA_ID、CEID和事件数据列表。有些设备软件把自定义字段直接往上报,Host解析不到对应项目就报错。我建议在上报前用标准的GEM验证工具跑一遍消息格式校验,确认结构树符合E30规范再联调,能省下大量时间。
4.5 常见故障快速定位表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| TCP能通但Select失败 | 设备ID不匹配 | 对比握手报文中的Device ID |
| Select.rsp状态异常 | 状态机未正确进入Connected | 检查连接状态重置逻辑 |
| 心跳超时断链 | 接收线程被阻塞 | 确认回调里没有耗时操作 |
| Host发请求设备端不回 | 消息处理器未注册 | 查看warning级日志 |
| 回复发出但Host收不到 | Stream/Function填错 | 用原始帧对比消息头 |
| 大消息丢帧乱序 | 接收缓冲区过小 | 调大缓冲区并复测 |
| 中文ASCII字段乱码 | 编码不符合标准 | 改用Binary或英文内容 |
| 事件消息Host解析失败 | 数据结构不符合GEM | 用GEM校验工具验证报文 |
5. 从源码二次开发角度补几句话
如果你不是只跑通示例就完事,而是想把这套源码集成到自己的设备软件里,有几个设计上的取舍很重要。
第一,线程模型要和现有软件匹配。JngHightSpeedSecs内部默认起了接收线程和处理线程池,如果你的设备软件本身是单线程事件循环,可以改造它的消息分发,把SECS消息转换成自己的事件投递,而不是让源码自己开线程。这样能避免两套线程模型互相干扰,尤其在Qt或者MFC这类有自己的消息循环的框架里,直接嵌入多线程SDK很容易出现界面卡顿和回调乱序。
第二,日志接口要接好。源码里的日志输出默认打到stdout,但设备软件通常有自己的日志系统。我建议把日志回调接口接上,把SECS通信层的日志统一写到设备日志文件里。遇到产线问题,第一件事就是翻通信日志,查Select、Linktest、消息收发记录,一份完整的通信日志能把问题定位时间压缩到几分钟量级。
第三,做好配置持久化。设备ID、IP、端口、T3超时这些参数,建议做成配置文件或者写入设备注册表,不要写死在代码里。工厂机房里的设备移动、网络改段是常有的事,如果每次改配置都要重新编译烧录,现场工程师会非常崩溃。我在实际项目里把这些参数都做成可以远程修改的配置项,配合通知机制动态生效,省了很多现场运维的麻烦。
收个尾:这套源码的价值在我看来说三点
最后说点个人心得。做SECS/GEM通信开发,最难的不是写出能收发消息的代码,而是写出能在产线上稳定跑几个月的代码。JngHightSpeedSecs这套源码给我的最大收获,不是它帮我省了多少开发工作量,而是它让我看到了一个工业级通信库应该有的样子:状态机边界清楚、超时处理完整、线程模型清晰、大块数据有预案。对照标准协议文档逐行读它,基本上能把SECS/GEM协议的很多隐藏细节吃透,比如Session ID的位域规则、System Bytes的复用策略、心跳与数据消息的优先级关系,这些都是标准文档里不会花大篇幅讲、但实际调试时一定会碰到的知识点。
如果你也是刚开始接触SECS/GEM,我建议手里备三样东西:一份SEMI E37和E5的标准文档,一台可以用Wireshark抓包的测试环境,以及一套能手工构造消息的Host模拟器。有了这三样,再配合源码,基本就具备了自己动手排查问题的能力了。希望这篇拆解能帮你少走点弯路。
本文还有配套的精品资源,点击获取