资讯动态

基于secs4net的SECS/GEM设备联机实战:从HSMS握手到MES对接

发布时间:2026/9/17 1:08:21 来源:尧图企业网站定制
做半导体工厂自动化这几年我越来越觉得真正决定产线能不能跑得稳的不是展厅里的数字大屏而是每一台设备能不能把自己身上的数据稳定地“说”出来。设备“说话”用的标准语言就是SECS/GEM——半导体设备与MES系统之间的事实协议标准。不管设备是刻蚀机、薄膜沉积还是光刻机只要是前道设备基本都认这套规矩。这篇文章我会完整记录一次半导体设备联机项目的实战过程用.NET平台的secs4net协议栈把一台老型号刻蚀机的控制器接入MES系统。内容包括HSMS连接建立、S1F1/S1F13握手、S6F11事件上报、S2F17远程命令处理每段都有可以直接复刻的C#代码最后一部分是联调现场踩过的坑和排查经验。适合正在做半导体或泛半导体设备联网、MES对接的工程师参考也想帮想入门SECS/GEM的后端同学省点时间。1. 为什么设备联网是MES的地基1.1 从“一堆设备”到“会说话的设备”真实的晶圆厂里设备品牌杂得很同一道工序可能就有三四种机台。每一台都有自己的控制器、自己的数据格式、自己的操作习惯。MES系统要管工单、管物料、管配方、管质量追溯数据源头全在设备侧。设备不联网MES就只是个手工录入系统产线上干一天系统里啥都不知道。MES的核心业务其实不复杂工单执行、批号追溯、配方管控、质量缺陷记录、设备状态监控、统计报表分析。这些模块听着各管一摊但背后都在等同一类东西——设备上报的实时事件。一片Wafer进腔了没有、加工时温度曲线是否正常、有没有报警、这个lot对应的配方参数是什么这些必须是设备主动“讲”出来而不是靠人拿着扫描枪去补录。这几年数字孪生概念很火不少工厂上了大屏展示、三维建模展示效果确实漂亮。但真进到产线上你会发现数字孪生解决的是“看起来怎么样”而MES解决的是“账实是否一致、流程是否受控”。设备连不上MES连数据都是缺的数字孪生再逼真也只是个空壳。把设备和MES之间这条通信链路做扎实远比把大屏做炫重要得多。1.2 SECS/GEM半导体设备的“普通话”SECS/GEM不是某一个协议而是一族SEMI标准具体分这么几层SEMI E4SECS-I基于RS-232串口的物理层协议老设备用得多速度慢现在主流新项目基本不再用了。SEMI E37HSMS基于TCP/IP的传输协议是现在设备联机的主流方式通信建立在以太网上吞吐量和稳定性比串口好太多。SEMI E5SECS-II消息层标准规定了消息长什么样也就是下面要重点讲的Stream/Function模型。SEMI E30GEM设备行为标准规定了设备应该支持哪些SECS-II消息、状态机怎么切、事件怎么上报。MES和设备要做哪些“对话”都以GEM为准。把这几个标准串起来理解设备这边跑一个SECS/GEM协议栈负责把业务数据编码成SECS-II消息通过HSMS的TCP连接发给MESMES端也有一个对应的协议栈收到消息后解码、解析再回一个应答。整个过程就像两个人用同一部字典打电话字典就是E5电话线就是E37。1.3 为什么用secs4net而不是自己造轮子SECS/GEM协议栈要处理的事情并不少HSMS的报文拆包粘包、SECS-II消息的编解码、各种数据类型的序列化、还有T3/T5/T6这些定时器管理。自己从零写一套没有三个月根本稳不下来而且很容易在字节序、List嵌套这些细节上翻车。secs4net是.NET生态里一个开源协议栈实现代码结构清晰支持HSMS和SECS-I消息事件模型用起来顺手也支持异步发送和接收。最关键的是它把SECS-II的编解码封装得比较好业务代码不需要手动去拼字节直接操作Item对象就行。这次项目里我用的是2.x版本配合.NET 6以上的环境几行代码就能把HSMS连接拉起来后面再把消息路由和MES接口对上效率比从零写协议栈高出一大截。2. SECS/GEM核心概念速览2.1 Stream/FunctionSECS-II的消息坐标SECS-II里的每条消息都用“Stream.Function”来唯一标识格式就是SxFy。Stream可以理解成大的功能分类Function是分类下的具体动作。比如1字头的Stream都是设备状态相关的2字头是设备控制和诊断6字头是事件和追溯数据。项目里最常用的一批消息建议先背下来消息名称方向作用S1F1Are You Online主机→设备询问设备是否在线设备回S1F2S1F2On Line Data设备→主机应答S1F1带MDLN和SOFTREVS1F3Request Status主机→设备请求指定SVID的状态值设备回S1F4S1F13Establish Communications Request任意→任意建立通信的握手请求回S1F14S2F17Request Remote Command主机→设备请求远程执行命令比如START/STOP回S2F18S6F11Event Report Send任意→任意上报事件数据比如加工完成回S6F12S6F12Event Report Acknowledge任意→任意应答S6F11COMMACK0表示接受这里面有个概念要分清发出请求并期望回复的叫Primary消息对应的回复叫Secondary消息。在代码里Primary消息带不带WWait位很关键W位表示“我等你回复”如果不置位或者搞错了对方很可能就不回了然后本地会一直等到T3超时。2.2 数据项类型List套List的二进制结构SECS-II消息的数据部分由数据项Item组成。基本类型有二进制B、布尔BOOLEAN、ASCII字符串A、各种长度的有符号/无符号整数I1/I2/I4/I8、U1/U2/U4/U8、浮点F4/F8还有最特殊的列表类型L可以嵌套其他任意Item。这个设计很像JSON里“数组套对象”的感觉。比如S6F11的完整数据是L U4 DATAID U4 CEID L U4 RPTID L A LOT-001 U4 25 F8 23.45 最外层是一个List里面套了两个U4和一个List内层List里又是RPTID和具体的数据值。写代码的时候如果List少套了一层或者类型写成了I4而对方期望U4MES那边解析就会报错或者直接把这个消息当作无效消息丢掉。这是新手最容易踩的坑后面排障部分我会专门讲。2.3 HSMS连接、Device ID与三组关键定时器HSMS连接走TCP默认端口一般是5000或5001。连接模式分两种主动模式Active下设备侧主动向MES的IP和端口发起TCP连接被动模式Passive下设备侧监听端口等MES来连。实际项目中绝大多数是设备侧做ActiveMES侧做Passive这样设备一开机就能主动找到MES注册上线。这里要注意Device ID。每台设备在MES侧都有一个独立编号所有HSMS报文头里都会带这个字段MES根据它判断消息来自哪台设备。如果配错了MES可能直接把消息丢弃甚至链路都建立不起来。还有一个很实用的概念HSMS有三组定时器排查问题的时候天天用得到。T3Primary消息发出后等待Secondary回复的超时时间默认45秒超时说明对方没回或者网络有问题。T5主动方发起连接后等待连接确认的时间默认10秒。T6控制消息比如建立/断开连接的握手消息的事务超时时间默认5秒。联调时如果发现消息发出去后一直没回音先查T3日志如果链路一直Link不上查T5和防火墙。这个思路能省掉一大半的查错时间。3. 环境准备与secs4net接入3.1 开发环境清单这次项目我用的环境如下给你做个参考Windows 10/11 或 Windows Server设备侧工控机多数是Windows系统.NET 6 及以上SDKNuGet包Secs4Net2.x一个SECS/GEM模拟器用来在MES端模拟设备侧发消息和收消息WireShark抓HSMS的TCP包排障神器实际设备侧如果是在Linux工控机上.NET Standard 2.0的库也能跑但这次我们的设备控制器是Windows部署起来比较省事。3.2 NuGet引入与第一个最小工程在项目里执行dotnet add package Secs4Net然后建一个控制台项目先跑通连接再往上加业务逻辑。注意secs4net不同小版本的类名和构造函数参数名可能有细微差别下面代码以2.x版本的接口为准如果你的包版本不同照着改命名就行。using System.Net; using Secs4Net; var endpoint new IPEndPoint(IPAddress.Parse(10.20.30.45), 5000); await using var secs new SecsHsmsConnection( endpoint, deviceId: 0, connectPassive: false); secs.ConnectionChanged (_, e) Console.WriteLine($连接状态 {e.NewState}); secs.MessageReceived (_, e) Console.WriteLine($收到消息 {e.Message}); await secs.OpenAsync(); Console.WriteLine(HSMS连接已启动); Console.ReadLine();这段代码跑起来如果MES侧监听正常你会看到连接状态切到Selected或Communicating说明TCP链路已经建立。先不着急写业务把这一步跑通后面的所有消息处理才有基础。3.3 连接参数IP、端口、Device ID和主动/被动模式连接参数写在配置里方便现场改我习惯用appsettings.json维护{ Secs: { HostIp: 10.20.30.45, Port: 5000, DeviceId: 0, PassiveMode: false }, Mes: { BaseUrl: http://10.20.30.100:8080 } }这几个参数的意思是设备侧主动去连MES主机的10.20.30.45:5000Device ID是0对应MES里分配给这台刻蚀机的编号。PassiveMode千万要和MES侧约定一致如果这台设备是主动连MES侧就必须是被动监听两边都是主动或者都是被动连接永远建不起来。4. 核心代码实现从握手到事件上报4.1 建立连接并订阅消息路由实际项目里我不会把所有逻辑堆在Main方法里而是封装成一个服务类。先看连接和消息路由的部分private async Task OnMessageReceived(object? sender, SecsMessageEventArgs e) { var msg e.Message; _logger.LogInformation(收到消息: {Message}, msg); // 只处理PrimarySecondary会被自动匹配到对应的发送任务里 if (!msg.Header.IsPrimary) return; try { switch (msg.Header.Stream) { case 1: await HandleStream1Async(msg); break; case 2: await HandleStream2Async(msg); break; case 6: await HandleStream6Async(msg); break; } } catch (Exception ex) { _logger.LogError(ex, 处理消息异常: {Message}, msg); } }把Stream对应的处理方法拆开后面加消息类型的时候只需要在对应Stream里加一个case不会把一个大方法越写越乱。这里有个经验消息处理回调里别做慢操作宁可先回ACK再异步处理业务也别卡住协议栈的消息循环否则后面所有消息都会排队超时。4.2 设备在线握手S1F1/S1F13的正确回复姿势MES连上设备后第一件事就是确认设备在不在线。S1F1是“你在线吗”设备的回复是S1F2数据要带两个A类型字段MDLN设备型号和SOFTREV软件版本。代码里这样写case 1: // S1F1 Are You Online? { var reply msg.CreateReply(); reply.SecsItem Item.List( Item.AString(ETCH-AMAT-01), Item.AString(V1.0.0)); await _secs.SendAsync(reply); break; }注意别只回一个A字符串。S1F2的数据规定是一个List里面包含MDLN和SOFTREV两个字段。如果你贪省事只回一个字符串MES那边解析List下标时会直接越界报错。S1F13是建立通信的握手请求回复S1F14时比S1F2多了个COMMACK字段0表示接受通信case 13: // S1F13 Establish Communications Request { var reply msg.CreateReply(); reply.SecsItem Item.List( Item.B(0), Item.AString(ETCH-AMAT-01), Item.AString(V1.0.0)); await _secs.SendAsync(reply); break; }有些MES要求必须先走完S1F13/S1F14才允许其他事务如果漏掉这步后面发S2F17远程命令会被MES直接拒绝。联调的时候先确认MES侧的通信建立流程是走S1F1还是S1F13还是两个都要各家实现不太一样。4.3 事件上报S6F11的解析、接收和发送S6F11是设备主动把事件上报给MES也是整个联机系统里流量最大的一类消息。设备侧主动上报的典型场景加工结束、批次完成、报警产生、状态变化。收到MES发来的S6F11时比如远程配置变更或者主机请求上报需要解析DATAID、CEID、RPTID和值列表然后回S6F12case 11: // S6F11 Event Report Send { var dataId msg.SecsItem[0].GetU4(); var ceId msg.SecsItem[1].GetU4(); var report msg.SecsItem[2]; var rptId report[0].GetU4(); var values report[1]; _logger.LogInformation(事件上报: DATAID{DataId}, CEID{CeId}, RPTID{RptId}, dataId, ceId, rptId); // 这里把解析好的数据交给MES接入层 await _mes.HandleEquipmentEventAsync(ceId, rptId, values); // COMMACK0表示接受 var reply msg.CreateReply(); reply.SecsItem Item.B(0); await _secs.SendAsync(reply); break; }更常见的需求是设备侧主动上报加工结束。假设设备加工完一批要把lot号、片数、配方名、平均温度发给MES代码就是构造一个S6F11发出去。注意S6F11的W位要置true因为必须等MES回S6F12public async Taskuint ReportProcessEndAsync(ProcessResult result, CancellationToken ct default) { var dataId _nextDataId; var msg new SecsMessage(6, 11, waitBit: true, Item.List( Item.U4(dataId), Item.U4(5001), // CEID5001是我们和MES约定的“加工完成”事件ID Item.List( Item.U4(1), // RPTID报表ID Item.List( Item.AString(result.LotId), Item.U4(result.WaferCount), Item.AString(result.RecipeId), Item.F8(result.AverageTemp))))); var reply await _secs.SendAsync(msg, ct); var commack reply.SecsItem[0].GetByte(); if (commack ! 0) _logger.LogWarning(S6F12 COMMACK{Ack}事件未被MES接受, commack); return commack; }用SendAsync发Primary的时候secs4net会一直等到对方的Secondary回来超时则抛T3异常。这个设计很方便等于把“发消息等回复”整个包成了一个异步调用业务代码写起来特别顺。4.4 远程命令S2F17的接收与回复MES远程控制设备就是走S2F17。比如MES下发的“START”命令设备收到后先判断当前状态允不允许启动然后回S2F18CMDA0表示接受非0表示拒绝case 17: // S2F17 Request Remote Command { var command msg.SecsItem[0].GetString(); _logger.LogInformation(收到远程命令: {Command}, command); var accepted command switch { START _state.CanStart, STOP _state.CanStop, _ false }; var reply msg.CreateReply(); reply.SecsItem Item.List(Item.B(accepted ? (byte)0 : (byte)1)); await _secs.SendAsync(reply); break; }这里的_state就是设备状态对象实际项目里要接上设备控制器的接口判断设备当前在什么状态、允不允许执行这个命令。注意别在消息处理回调里直接去调用设备运动控制应该把命令放进队列由设备控制线程去执行否则一个慢操作可能会堵死整个协议栈。4.5 完整服务类代码把上面的代码拼起来就是一个可以直接跑的完整服务类。这个类我在现场项目里的版本比这个复杂但骨架完全一致using System.Net; using Secs4Net; namespace EquipmentIntegration; public sealed class EquipmentSecsHost : IAsyncDisposable { private readonly SecsHsmsConnection _secs; private readonly ILogger _logger; private readonly IMesGateway _mes; private readonly EquipmentState _state; private uint _nextDataId 1; public EquipmentSecsHost(IConfiguration config, ILogger logger, IMesGateway mes, EquipmentState state) { _logger logger; _mes mes; _state state; var secsConfig config.GetSection(Secs); var endpoint new IPEndPoint( IPAddress.Parse(secsConfig[HostIp]!), secsConfig.GetValueint(Port)); _secs new SecsHsmsConnection( endpoint, deviceId: secsConfig.GetValueint(DeviceId), connectPassive: secsConfig.GetValuebool(PassiveMode)); _secs.ConnectionChanged OnConnectionChanged; _secs.MessageReceived OnMessageReceived; } public Task StartAsync(CancellationToken ct default) _secs.OpenAsync(ct); private void OnConnectionChanged(object? sender, ConnectionChangedEventArgs e) { _logger.LogInformation(连接状态变化 {State}, e.NewState); if (e.NewState ConnectionState.Closed) _ ReconnectWithBackoffAsync(CancellationToken.None); } private async Task ReconnectWithBackoffAsync(CancellationToken ct) { var delay TimeSpan.FromSeconds(1); while (!ct.IsCancellationRequested _secs.ConnectionState ! ConnectionState.Open) { try { await _secs.OpenAsync(ct); } catch { await Task.Delay(delay, ct); delay TimeSpan.FromSeconds(Math.Min(30, delay.TotalSeconds * 2)); } } } private async Task OnMessageReceived(object? sender, SecsMessageEventArgs e) { var msg e.Message; _logger.LogInformation(收到消息: {Message}, msg); if (!msg.Header.IsPrimary) return; try { switch (msg.Header.Stream) { case 1: await HandleStream1Async(msg); break; case 2: await HandleStream2Async(msg); break; case 6: await HandleStream6Async(msg); break; } } catch (Exception ex) { _logger.LogError(ex, 处理消息异常: {Message}, msg); } } private async Task HandleStream1Async(SecsMessage msg) { switch (msg.Header.Function) { case 1: // S1F1 Are You Online? { var reply msg.CreateReply(); reply.SecsItem Item.List( Item.AString(_state.MachineModel), Item.AString(_state.SoftwareRev)); await _secs.SendAsync(reply); break; } case 13: // S1F13 Establish Communications Request { var reply msg.CreateReply(); reply.SecsItem Item.List( Item.B(0), Item.AString(_state.MachineModel), Item.AString(_state.SoftwareRev)); await _secs.SendAsync(reply); break; } case 3: // S1F3 Request Status { var svids msg.SecsItem ?? Item.List(); var items new ListItem(); foreach (var item in svids) { var svid item.GetU4(); items.Add(Item.U4(svid)); items.Add(_state.GetSvidValue(svid)); } var reply msg.CreateReply(); reply.SecsItem Item.List(items.ToArray()); await _secs.SendAsync(reply); break; } } } private async Task HandleStream2Async(SecsMessage msg) { switch (msg.Header.Function) { case 17: // S2F17 Request Remote Command { var command msg.SecsItem[0].GetString(); _logger.LogInformation(收到远程命令: {Command}, command); var accepted command switch { START _state.CanStart, STOP _state.CanStop, _ false }; var reply msg.CreateReply(); reply.SecsItem Item.List(Item.B(accepted ? (byte)0 : (byte)1)); await _secs.SendAsync(reply); break; } } } private async Task HandleStream6Async(SecsMessage msg) { switch (msg.Header.Function) { case 11: // S6F11 Event Report Send { var dataId msg.SecsItem[0].GetU4(); var ceId msg.SecsItem[1].GetU4(); var report msg.SecsItem[2]; var rptId report[0].GetU4(); var values report[1]; _logger.LogInformation(事件上报: DATAID{DataId}, CEID{CeId}, RPTID{RptId}, dataId, ceId, rptId); await _mes.HandleEquipmentEventAsync(ceId, rptId, values); var reply msg.CreateReply(); reply.SecsItem Item.B(0); await _secs.SendAsync(reply); break; } } } public async Taskuint ReportProcessEndAsync(ProcessResult result, CancellationToken ct default) { var dataId _nextDataId; var msg new SecsMessage(6, 11, waitBit: true, Item.List( Item.U4(dataId), Item.U4(5001), Item.List( Item.U4(1), Item.List( Item.AString(result.LotId), Item.U4(result.WaferCount), Item.AString(result.RecipeId), Item.F8(result.AverageTemp))))); var reply await _secs.SendAsync(msg, ct); var commack reply.SecsItem[0].GetByte(); if (commack ! 0) _logger.LogWarning(S6F12 COMMACK{Ack}事件未被MES接受, commack); return commack; } public async ValueTask DisposeAsync() { await _secs.DisposeAsync(); } }对应的Program.cs启动代码using EquipmentIntegration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; var config new ConfigurationBuilder() .AddJsonFile(appsettings.json) .Build(); using var loggerFactory LoggerFactory.Create(b b.AddConsole()); var logger loggerFactory.CreateLogger(SecsHost); var mes new MesApiClient(new HttpClient { BaseAddress new Uri(config[Mes:BaseUrl]!) }); var state new EquipmentState(); await using var host new EquipmentSecsHost(config, logger, mes, state); await host.StartAsync(); Console.WriteLine(设备联机服务已启动CtrlC 退出); await Task.Delay(Timeout.Infinite);5. 与MES系统联调的完整流程5.1 本地联调环境模拟器 开源MES和设备联调之前我强烈建议先搭一套本地环境不要一上来就找MES团队占人家环境。我这次用了两个工具一个是SECS/GEM模拟器secs4net仓库的示例代码里就有一个简单的模拟客户端改改就能用来模拟MES侧收发消息另一个是社区里的开源MES系统比如Carbon做本地部署能跑起来完整的工单、批次、事件逻辑。本地联调的目的不是代替正式MES而是把设备侧的编解码、消息时序、异常处理全部验证一遍。等设备侧代码稳了再去跟正式MES做集成测试问题会少很多也不会因为乱发消息把生产环境的MES日志刷爆。模拟器的好处是可以自由注入各种“坏消息”类型不匹配的、List少一层的、空字符串的、超大数值的。设备侧代码如果能在模拟器的狂轰滥炸下不崩、不卡死、还能正确回ACK那上线后基本就稳了。5.2 MES数据模型映射CEID、RPTID、SVID怎么对联调第一步不是写代码而是对编码表。CEID事件ID、RPTID报表ID、SVID状态变量ID这些数字设备侧和MES侧必须用同一份约定否则设备上报了5001MES那边却当成5002处理数据全乱。我经历过一次印象特别深的教训设备手册里写的CEID和MES系统配置表里的编号差了1联调时所有上报数据都进了错误的队列排查了两个小时才发现是一处映射配置不一致。后来我们做了个统一配置表设备侧和MES侧共用一份JSON谁都不许在代码里硬编码事件编号。映射表大概长这样类别编号含义对应MES模块CEID5001批次加工完成工单执行CEID5002设备报警发生设备管理CEID5003部件寿命预警设备保养RPTID1加工结果报表质量追溯RPTID2报警明细报表异常管理SVID1001当前配方名配方管理SVID1002腔体温度工艺监控这个表最好让MES产品经理、设备工程师、开发三方一起过一遍。MES产品经理对业务编号最熟设备工程师知道设备的每个变量对应什么物理含义开发负责实现三方对完再写代码后面少走很多弯路。5.3 断线重连与异常恢复策略产线上的网络不像办公室那么稳定交换机重启、网线松动、MES主备切换都会导致HSMS连接断开。设备侧必须能自动重连而且不能把重连搞成“死循环风暴”。我在完整代码里已经加了指数退避重连的逻辑第一次重连等1秒失败后等2秒、4秒、8秒最多30秒封顶。这样即使MES持续宕机设备侧也不会像发疯一样每秒发起几十个TCP连接把对端打挂。还有一个关键点重连成功后设备要重新做S1F1/S1F13握手还要考虑断线期间有没有事件没上报。如果断线期间设备把一批Wafer加工完了S6F11还没发出去重连后要先把这段时间的补报事件排队发出去。这个补报机制MES侧必须支持否则数据就丢了。5.4 上线前检查清单连接参数和设备手册、MES配置表三方核对一致。S1F1/S1F13、S1F3、S2F17、S6F11这四组消息全部在模拟器里跑通。事件补报流程验证过断线期间的数据能完整补上。日志里能清晰看到每条消息的原始内容方便问题回溯。防火墙放行了TCP端口如果是UDP的SECS-I老设备确认串口线、波特率都正常。设备侧和控制器的状态接口联调过START/STOP命令能正确传递。6. 常见问题与排查技巧实录6.1 连接建立失败排障HSMS连接一直Link不上是最常见的开场问题。排查思路按这个顺序来抓包看TCP三次握手有没有完成。如果SYN包都看不到大概率是网络不通或者端口不对。确认主动/被动模式是否和MES侧约定一致。我见过最典型的错误设备侧配了主动MES也配了主动两边都在等对方来连。确认端口号。HSMS默认5000和5001都有用必须确定MES监听的是哪个。确认Device ID。有些MES实现里Device ID不匹配时链路建立了也不发任何事务消息。抓包工具建议用WireShark过滤器直接写tcp.port 5000看一眼连接和报文问题马上清楚。6.2 消息解析异常与T3超时消息发出去后一直收不到回复日志里报T3超时。这种情况先别急着怀疑网络把日志里发的报文拿出来仔细看。最常见的两个原因一是W位没置位对方认为你不需要回复自然就不回二是消息格式不符合对方解析器的预期MES侧解析报错直接把这个消息丢弃了。S6F11这种消息尤其典型List的嵌套层级、字段类型、字段顺序任何一个不对都会导致解析失败。我提供一个实战技巧联调时把secs4net的日志级别开到Debug每条消息的编码结果都会打印出来。我和MES团队对问题的时候直接把两边的日志贴上谁的消息长什么样一目了然不用猜来猜去。6.3 数据上报丢失与重复问题S6F11上报之后MES没记录到数据或者记录了两遍这类问题在产线上最恼火。排查时看三样东西DATAID是否重复。有些MES用DATAID做幂等键如果设备侧每次用同一个IDMES就把后到的消息当成重复消息丢弃。S6F12有没有收到。如果连S6F12都没等到说明消息根本没到MES走网络排查方向。MES侧事件处理有没有做幂等。如果MES内部处理S6F11时是“先落库再回ACK”处理到一半挂了设备侧重发就会重复。解决办法设备侧维护一个单调递增的DATAIDMES侧用DATAIDCEID做唯一索引两端都做好幂等这个问题就能彻底避免。6.4 坏消息注入演示要想设备侧代码稳光测正常流程不够。我建议你在模拟器里做一轮“协议攻击”测试把以下几种坏消息都发给设备类型不匹配S1F3里的SVID本应是U4你发个A字符串过去。List结构变形S6F11的报表部分少了RPTID这一层。超大字符串:发一个几百KB的ASCII串看消息循环会不会卡死。非法长度整数字段发一个超出范围的数。设备侧对任何一条坏消息都必须做到三件事不崩溃、不阻塞、能回一个合理的错误应答或至少把错误记到日志里。能把这一轮扛过去这套代码才敢上产线。6.5 避坑清单个人血泪总结别在消息处理回调里做耗时操作先回ACK再做业务这是T3超时的头号元凶。CEID、RPTID、SVID这种编号永远从统一配置里读别在代码里写死。每个消息都要放到日志里最好是结构化日志方便后续按条件搜索。补报机制从第一天就设计进去别等上线了再补断线丢数据是必然发生的事。S6F11的List嵌套一定要在模拟器里反复验证这是出错率最高的地方。上线前把设备和MES两端的日志时间同步好NTP对时否则排障时时间线对不上会非常痛苦。这套思路不只是半导体前道设备能用。我后来看SMT行业的MES方案发现逻辑一脉相承贴片机、回流焊、AOI这些设备同样要解决设备数据上报和远程控制的问题很多设备也支持SECS-II或者接口协议同源。理解了SECS/GEM这套设备通信的底层逻辑再去看任何行业的设备联网需求心里都有底得多。最后再分享一点个人体会做设备联机最考验人的不是写代码而是现场调试时的耐心。协议本身不复杂难的是各种设备千奇百怪的实现细节有些老设备连文档都和实际行为对不上。遇到诡异问题别急着改代码先把报文抓出来对照标准慢慢看。设备会骗你日志和报文不会。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价