
几年前我第一次把一台C#写的上位机接到半导体前道设备的SECS/GEM端口时心里其实没底。对方是日本厂商的薄膜设备EAP系统要求必须通过SECS/GEM联机才能跑Recipe而我手里只有几百页英文SEMI规范PDF和一台连IP都没配好的旧PC。后来调试了三个通宵才把S1F13/S1F14这条链路跑通期间踩过的坑够写好几篇博客。这篇文章就是把那段从“会用C#做TCP收发”到“能独立交付一个SECS/GEM通信模块”的完整过程沉淀下来包含协议族拆解、C#代码示例、消息编码细节、GEM状态机和调试手段。适合刚接触半导体设备自动化、准备用C#开发上位机/EAP连接的工程师也适合那些手头有设备但不知道怎么下手联机的朋友。1. SECS/GEM到底是什么——先搞懂你要通信的对象1.1 协议族里的E4/E5/E30/E37分别管什么SECS/GEM不是一个协议而是一族标准的统称。去SEMI官网查资料你会看到E4、E5、E30、E37这些编号不少初学者一看到就头大。我建议你按这个顺序理解SEMI E4也就是SECS-I是基于RS-232串口的物理层通信标准。早期设备用它速度慢、距离短现在新设备基本不看了但有些老机台翻新还是会遇到。SEMI E37也就是HSMS是基于TCP/IP的高速消息传输标准。现在绝大多数设备的网络联机都走HSMS这是你需要投入最多精力的部分。SEMI E5也就是SECS-II负责定义消息内容和数据项的编码规则。你会在里面看到List、Binary、ASCII、I4、U4这些Item类型理解这些是做消息解析的基础。SEMI E30也就是GEM定义了通用设备模型设备状态机、主机和设备如何交互、事件上报、报警、配方管理、远程命令等。它让不同厂商的设备在行为上有一致的“规矩”。打个比方E37是高速公路E5是货车的装箱标准E30是交通规则E4是乡镇土路。你开发和联调时大部分时间跑在E37这条高速上但车里装的东西必须按E5的箱子来放开车的行为又必须符合E30的规则。三者缺一不可。1.2 设备端和主机端是怎样分工的在SECS/GEM通信里两端角色很明确一端是Equipment设备另一端是Host主机。Host通常指工厂的MES、EAP或者你开发的上位机软件。设备端是实际生产的机台内部一般由嵌入式控制器、PLC或者工控机程序实现协议栈。这两端并不是完全对等的。Host可以下发远程命令要求设备执行动作设备则要根据当前GEM状态决定是否接受、是否执行。比如设备处于Offline状态时Host让它启动作业它可能直接拒绝。反过来说设备也会主动上报报警和事件Host必须及时回确认。理解这种“请求-回复”和“主动上报”的混合模式比单纯收发字节重要得多。1.3 为什么C#开发者在半导体行业也有一席之地半导体工厂上位机尤其是设备端的Windows联机软件、EAP模块、数据采集程序很多都用C#。C#写异步TCP、处理超时、做日志、画UI都很顺手开发生态也成熟。再加上有像Secs4Net这样可用的开源实现C#工程师完全能在短时间内搭出一个能用的SECS/GEM通信模块。我看过一些项目底层协议栈用C写但业务联调层用C#封装两边通过接口对接原因也很实在C#在业务逻辑、配置管理、报表输出、数据库集成这些层面的效率确实高。所以如果你已经会C#再补齐SECS/GEM这块知识在半导体设备自动化领域会非常有竞争力。2. 开工前的准备协议栈选型、模拟器和连接参数2.1 开源C#协议栈怎么选我的建议分三种情况。如果你是学习和做原型验证直接用Secs4Net这类开源库。它用纯C#实现了HSMS连接、SECS-II消息编解码还支持类似SML的文本描述方式能让你把精力放在业务逻辑而不是底层字节拼装上。GitHub上能找到源码跟着调试几遍对协议的理解会深很多。如果公司已经有自研或者采购的协议栈优先在现有框架上做封装不要轻易换。SECS/GEM联调最怕两边标准不一致一套跑通的协议栈比你花时间重写要值钱得多。只有现有库实在没法用、或者要长期定制才考虑自己从头写。如果你想彻底搞懂协议我建议自己用Socket写一个最小实现至少把消息头拼装、Select.req发送、S1F13收发这三角色跑通。这个练习不是生产方案而是让你以后遇到问题时不发懵能直接看Hex定位。资料方面SEMI标准的原始文档需要从SEMI官网获取部分设备厂商的售后文档里也会有精简版。搜索的时候用“SEMI E37 HSMS”“SEMI E5 SECS-II”“SEMI E30 GEM”这些关键词比直接搜“SECS/GEM”更能找到对口的规范。另外开源协议栈的源码和README也是很好的学习材料比规范文档更贴近实际代码。2.2 没有真实设备时用什么模拟设备端很多项目在拿到真机之前就要把通信程序写好这时候必须有个设备端模拟器。我吃过没模拟器的亏程序写完了现场联调才发现一堆问题而现场机台的时间都是按小时计费的试错成本很高。模拟器有两个选择。一是找厂商提供的仿真模式部分设备软件自带SECS/GEM模拟但这个要看运气。二是装一个独立的HSMS模拟器程序它扮演设备端监听端口或者主动连接你的Host程序然后你可以向它发送S1F13、S1F1、S6F11这些消息验证你的编解码和业务逻辑。我个人的经验是至少准备两台机器或者两个虚拟机一台跑你的Host代码一台跑模拟器。中间用Wireshark或者自写抓包工具记录所有TCP包。这样才能有效模拟现场的网络环境并且在出问题时能立刻说清楚是哪一端发的数据、另一端是否回了。2.3 HSMS连接参数到底该填什么连接参数看似简单实际上是联调第一天最容易出问题的地方。我们需要确认四类东西设备IDDevice ID/Session ID通常是0但也可能按厂商要求配置成其他值。这个值必须与设备端一致否则Select阶段就会失败。IP和端口如果是Active模式Host主动连接设备的IP和端口如果是Passive模式Host监听端口等设备连过来。端口常见的有5000、5001、9999但设备厂商不一定用默认值一定要问清楚。连接模式Active还是Passive必须两端匹配。很多“TCP通了但消息发不出去”的案例最后查出来是一端在等连接、另一端也在等连接。HSMS定时器T3是回复超时T6是控制事务超时T7是未选择状态超时T8是网络字符间隔超时。具体值需要按设备要求配置配得太短容易误报超时配得太长又会在断连时迟迟不发现。提示不要想当然地用默认值。联调前把设备ID、端口、连接模式、定时器列成一张参数表发给设备方和EAP方一起确认能避免90%的连接问题。3. 第一次跑通C#通信从HSMS帧到S1F13/S1F143.1 HSMS消息帧的结构10字节协议头HSMS在TCP上跑的消息帧很简单分为三部分长度字段、消息头、消息体。一个完整帧是4字节长度字段表示后面消息头消息体的字节数。10字节消息头。消息体也就是SECS-II编码的数据可以为空。10字节消息头是关键我拆给你看第0-1字节Session ID也就是设备ID大端序。第2字节高第7位是W位W1表示这是一条需要回复的Primary消息低7位有的是Stream编号不同协议栈对Stream所占bit的掩码处理会稍有差别但本质含义不变。第3字节Function编号和Stream一起组成SxFy。第4字节PType通常为0表示SECS-II消息。第5字节SType0表示数据消息非0表示HSMS控制消息。第6-9字节System Bytes事务标识后续靠它来匹配哪条回复对应哪条请求。举个例子如果一台设备往Host发S1F13建立通信请求消息体的数据项部分可能长这样具体长度视内容而定这里只看格式01 01 04 06 53 45 4D 49 2D 41 ...第一个01是List项第二个01表示这个List包含1个元素后面的04表示ASCII06表示这个ASCII字符串有6个字节。不理解没关系下一节会详细讲。3.2 会话建立的四个步骤很多人以为TCP连接建立就算通信完成了这是大误区。HSMS会话建立有明确的步骤TCP层先连上。Active模式下Host发起connect到设备端口Passive模式下设备主动连过来。Host发送Select.req控制消息。这个消息没有数据体只有10字节消息头且SType1SessionID固定为0xFFFF。设备回复Select.rspSType2携带选择结果状态码。收到该回复且结果正确才表示HSMS会话进入Selected状态。进入Selected状态后才能发送真正的SECS消息比如S1F13。TCP连通只代表线路通Select成功才代表SECS/GEM通信层就绪。我见过有同事直接跳过Select去发S1F13设备端根本不应答。3.3 用C#代码发一条S1F13/S1F14先给一个自写Socket方式拼装消息头的代码方便你理解本质byte[] BuildHsmsHeader(ushort sessionId, byte stream, byte function, bool waitBit, byte sType, uint systemBytes) { var header new byte[10]; header[0] (byte)(sessionId 8); header[1] (byte)sessionId; header[2] (byte)((waitBit ? 0x80 : 0x00) | (stream 0xff)); header[3] function; header[4] 0; // PType 0 header[5] sType; // SType, 0 表示 SECS 数据消息 header[6] (byte)(systemBytes 24); header[7] (byte)(systemBytes 16); header[8] (byte)(systemBytes 8); header[9] (byte)systemBytes; return header; }再看用开源库Secs4Net封装的写法思路会清晰很多// 以下为示意写法具体API以你使用的库版本为准 var connection new HsmsConnection(new HsmsConnectionConfig { DeviceId 0, IpAddress 192.168.10.20, Port 5000, IsActive true // Host主动连设备 }); await connection.OpenAsync(); var reply await connection.SendAsync(new SecsMessage(stream: 1, function: 13) { // S1F13 携带设备型号和软件版本 SecsItem Item.L( Item.A(MDLN-001), Item.A(1.2.0)) }); // 解析S1F14中的COMMACK0表示通信建立成功 var commAck reply.SecsItem.Items[0];使用开源库时不用手动拼消息头但你要理解底层发生了什么OpenAsync包含TCP连接和SelectSendAsync会分配SystemBytes、设置W位、等待对应的Secondary返回。3.4 如何验证通信真的建立了发完S1F13收不到S1F14或者收到S1F14但COMMACK不是0都说明通信建立有问题。验证时我习惯做三件事把收发日志完整打出来至少包含时间戳、方向、S/F、W位、SystemBytes、消息体Hex。不要只看“连接成功”。用S1F1/S1F2做一次简单的“Are You Online”往返确认消息链路稳定。停一段时间再发看看连接是否还在。如果连接会自己断开通常和T3/T6超时、Linktest心跳有关。注意HSMS会话建立后闲置连接需要维稳。很多设备会要求Host周期性发送Linktest.reqSType5否则设备会在一段时间后断开连接。如果你发现“连上一会儿就断”优先看心跳间隔是否配置正确。4. SECS-II消息体编码Item类型、事务匹配与C#建模4.1 SECS-II数据项的位级结构SECS-II消息体里面不是随手放的字节而是由一个个数据项Item组成的。每个Item由三部分组成格式字节、长度字段、数据体。格式字节的高2位表示长度字段占用的字节数低6位是格式码。常用的格式码我给一张表建议存着类型格式码说明ListL0x01列表用来嵌套其他项BinaryB0x02二进制字节数组Boolean0x03布尔值ASCIIA0x04字符串I20x092字节有符号整数U20x0A2字节无符号整数I40x0B4字节有符号整数U40x0C4字节无符号整数I80x0D8字节有符号整数U80x0E8字节无符号整数F40x0F4字节浮点数F80x108字节浮点数举个例子每条SECS消息都可以看成一个大List外层List包含若干子项每个子项可以是简单的整型、字符串也可以是嵌套的List。解析消息时递归遍历这个List树就行。4.2 用Item类型描述复杂消息以S6F11事件上报为例它用来把设备端产生的事件发给Host。结构大致是这样的L DATAID, CEID, L L RPTID, L L 变量ID1, 值1 L 变量ID2, 值2 用C#的Item来构造看起来就像这样var message new SecsMessage(stream: 6, function: 11) { SecsItem Item.L( Item.U4(0), // DATAID Item.U4(1001), // CEID事件ID Item.L( Item.L( Item.U4(7001), // RPTID Item.L( Item.L(Item.U4(100), Item.A(value-1)), Item.L(Item.U4(101), Item.A(value-2)) ) ) ) ) };这段代码的价值在于它把消息体的嵌套结构直接映射到了C#类型上。你以后看到S6F11抓包脑子里就能浮现这样一棵Item树而不是一堆十六进制。这也是C#开发SECS/GEM时最舒服的地方——用类型系统表达嵌套结构比用C裸指针舒服得多。4.3 事务匹配W位与SystemBytesSECS/GEM的消息分两类Primary消息和Secondary消息。Host发出Primary消息时通常把W位置1表示要求对方回复一条Secondary消息。那么对方回复的Secondary必须携带和Primary相同的SystemBytes这样发送方才能知道“这条回复对应我之前发的那条请求”。如果你在C#里用并发方式发送多条消息SystemBytes的唯一性就特别重要。不要直接用普通int自增而不加锁并发情况下很容易重复。更稳妥的做法是Interlocked.Increment生成或者用原子计数器包装一下private int _systemBytes; private uint NextSystemBytes() { return unchecked((uint)Interlocked.Increment(ref _systemBytes)); }回复消息到达时用一个ConcurrentDictionaryuint, TaskCompletionSourceSecsMessage按照SystemBytes找到对应的等待任务。这套机制基本就是所有SECS/GEM协议栈底层在做的事。自己写的时候犯过一次SystemBytes重复的错导致两台设备的消息互相错配排查了很久所以这块值得认真对待。5. GEM状态机不是一个开关COMM、Control与业务状态5.1 COMM和Control两层状态模型GEM不是让你连上就完事它对设备状态有明确建模。最核心的是两层Communication状态模型描述设备通信能力是否可用粗粒度上就是DISABLED和ENABLED两种状态。Control状态模型描述设备处于谁的控制之下通常有Offline、Online-Local、Online-Remote。设备要在Host控制下自动化运行通常得让Control状态切到Online-Remote。如果设备一直在Offline或者只是Online-Local现场本地面板控制Host发的远程命令往往会被拒绝。状态切换不是随意跳变GEM规定了触发状态切换的消息。比如Host发S1F17Online请求后设备会回复状态并切换到对应在线状态发S1F15请求离线则往离线方向走。具体要看设备是否具备远程模式能力以及当前是否有作业在执行。联调时最容易忽略的是设备处于Recipe执行中你发Offline请求设备可能返回“当前不可离线”这不是通信坏了而是状态机不允许。5.2 状态机在C#中的落地我建议在C#程序里把状态机做成显式的枚举和转移字典而不是散落的if/else。比如enum ControlState { Offline, OnlineLocal, OnlineRemote }再定义好每个状态下允许的消息集合。比如Offline状态下设备可能不执行某些Host命令OnlineLocal状态下即使远程命令能收到执行策略也可能与OnlineRemote不同。实际设计时把状态转移和消息处理分离先根据当前状态和收到的新消息判断是否允许转移如果允许再执行状态变化后的业务动作。这样逻辑清晰后面接手的人也好维护。我用过最差的做法是把状态判断散落在几十个消息处理函数里结果想改一个转移规则要翻遍整个项目。5.3 事件上报与远程命令的最小闭环SECS/GEM联调里S6F11/S6F12事件上报和S2F41/S2F42远程命令是两大核心闭环。S6F11是设备向Host主动上报事件Host必须回S6F12确认。上报事务的生命周期并不长但前提是设备端已经通过S2F33/S2F35等消息配置好了哪些Report对应哪些Collection Event。很多新手以为连上就能收到S6F11实际不是不配置的话设备不会发。S2F41是Host向设备下发远程命令比如启动流程、暂停、停止。消息体里要有命令字符串COMMAND和参数列表。设备执行完后回S2F42里面带命令完成确认码。要注意S2F42只是告诉Host“命令我收到了/执行情况怎么样”不代表生产动作已经彻底完成。有时候命令执行是异步的还需要通过S6F11上报最终结果。我建议新手做技术验证时把这三个闭环跑通S1F13/S1F14建立通信、S6F11/S6F12事件上报、S2F41/S2F42远程命令。这三条链路通了你的模块已经具备最核心的联机能力剩下的都是扩展。6. 现场调试记录连接断开、SystemBytes错配与日志沟通6.1 连接时通时断先看定时器和心跳现场调试最磨人的问题是“连接刚开始是好的过一阵子就断”。TCP层面看起来还活着但SECS/GEM事务已经挂了。这种问题首先怀疑T3/T6超时和Linktest心跳。T3超时是指Primary消息发出后等待Secondary回复的时间。如果业务逻辑处理太慢或者消息解析报错导致没有及时回复就可能在T3时间内收不到回复协议栈底层会当作事务超时处理。T6是控制事务超时比如Select、Linktest这类控制消息的等待时间。心跳方面常用做法是周期发Linktest.req并等待Linktest.rsp。如果设备没有在空转时给你发心跳Host程序就要主动承担这个职责。我一般会把心跳间隔放在配置项里方便现场调整。6.2 消息发出去了对方不回应SystemBytes和W位排查有一种情况很典型你的程序明明发了S1F13设备端就是不回S1F14。抓包发现TCP数据到了设备但设备没有响应。除了状态机不允许最常见的原因是W位或者SystemBytes异常。W位没有置1的消息在语义上表示“这是Secondary回复”设备收到一条没有对应请求的Primary会很困惑不回复也正常。再者SystemBytes如果用无符号数溢出后回绕可能导致设备认为这是一条超时的旧消息。排查方法很简单日志里把W位和SystemBytes打出来和抓包对比一看便知。6.3 日志与抓包用数据说话而不是“我觉得”调试SECS/GEM最忌讳凭感觉。我建议程序里固定打一份可解析的收发日志格式类似[2025-01-06 10:23:45.123] [TX] S1F13 W1 SYS0x00010001 BODY0101040653454D49... [2025-01-06 10:23:45.456] [RX] S1F14 W0 SYS0x00010001 BODY0101...有了这样的日志你和设备方、EAP方沟通时直接把Hex贴过去对方基本能立刻定位问题。我见过太多“我们这边没报错”“我们这边超时了”的争论最后都是靠日志才对齐的。如果要在项目里上抓包工具Wireshark能看TCP层但HSMS的解析不一定完整必要时直接看TCP payload的十六进制。自己写一个简单的抓包脚本或者程序把所有TCP字节记录下来配上前面的业务日志排查效率会高很多。6.4 与EAP对接时最容易出现的沟通问题最后说一点非技术因素。很多“SECS/GEM连不上”的问题最后查出来是两端参数没对齐设备ID不一致、端口配错、是否启用T3心跳有分歧、GEM版本不兼容、事件ID和变量ID映射对不上。我现在的习惯是项目启动时先发一页纸的参数表把通信模式、IP、端口、设备ID、定时器、所需消息列表全部列出来等双方签字确认再开发。参数表还会在每次联调前重新确认一遍避免有人中间改了配置忘了同步。这比出了事故再互相推责任要有效得多。调试阶段尽量先用模拟器把S1F13/S1F14、S6F11/S6F12、S2F41/S2F42这些闭环在本地跑通再约真机时间。真机窗口都很紧张现场只适合做参数微调和流程验证不适合从零开始Debug。模拟器多花的一两天现场能帮你省下好几天的通宵。最后说点我个人这几年的体会。SECS/GEM这个领域真正的门槛不在“用C#收发字节”而在“理解状态机和业务模型”。代码层面造消息头、发TCP、解析Item都是死功夫多做几遍就能熟练但GEM状态怎么迁移、事件怎么配置、远程命令怎么和设备动作对齐这些才是联调项目的灵魂。我见过太多项目抓包S1F1/S1F2能通但一走到S6F11事件上报就卡壳原因就是只学了“通信”没学“协议”。建议新手先别急着写业务界面老老实实把建立通信、事件上报、远程命令这三个最小闭环跑通再考虑上层功能。自己搭一个模拟器加Host的Demo环境比整天抱着一堆规范硬啃效率高得多。少熬几个通宵想想都是赚的。