ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#实战:secs4net解析半导体SECS/GEM协议S6F11事件报告

C#实战:secs4net解析半导体SECS/GEM协议S6F11事件报告 1. 半导体设备通信的普通话SECS/GEM协议到底在解决什么问题第一次接触半导体设备通信的人大概率会被一堆缩写砸晕SECS、GEM、HSMS、SML、S6F11、S2F42……我当年从普通上位机开发转到半导体设备端开发时也经历过这个阶段。说白了SECS/GEM就是半导体制造车间里设备和上层系统之间约定的一套普通话——设备厂商几百家每家的硬件接口、内部逻辑都不一样如果没有一套统一标准工厂的MES系统根本没法管理这些设备。SECSSEMI Equipment Communications Standard定义了报文格式和通信机制GEMGeneric Equipment Model定义了设备应该具备哪些行为、哪些状态、哪些数据。两者合起来就是一套完整的设备通信规范。你可以在里面找到设备状态上报、配方管理、报警管理、事件通知、远程命令等一整套标准动作。那S6F11是什么它是SECS报文家族里最活跃的成员之一。S6代表事件通知这一类消息F11是其中具体的一条Event Report Send事件报告发送。设备侧一旦发生某个被订阅的事件比如一批晶圆加工完成、某个传感器触发、配方切换完成就会主动往主机Host方向发一条S6F11把事件ID和相关的数据变量Report一起带过去。为什么S6F11这么重要因为半导体产线是高度自动化的主机不可能靠轮询去问设备你现在怎么样了。设备主动上报才是效率最高的方式。一条S6F11报文里通常包含事件IDCEID、报告IDRPTID列表、每个报告里的变量VID和对应的值。主机收到后根据CEID判断发生了什么再根据VID去查数据字典把原始值翻译成有意义的工程数据。这篇文章面向的是已经有一定C#基础、正在做半导体设备上位机或EAPEquipment Automation Program开发的工程师。我会用C#配合secs4net这个开源库把S6F11从收到一串字节到解析成业务对象的完整链路讲清楚。中间会涉及报文结构、库的选型理由、代码实现、踩坑经验以及一些实际项目里才会遇到的边界情况。如果你正在做设备端或主机端的SECS通信这篇应该能帮你少走不少弯路。2. 动手之前S6F11报文结构与secs4net选型分析2.1 S6F11的报文骨架长什么样在写代码之前必须先把S6F11的数据结构吃透。SECS-II报文本质上是项Item的树形结构每个Item有一个格式码Format Code和长度可以嵌套。S6F11的标准结构大致如下S6F11 (W) L [3] U4 DATAID // 数据ID通常为0 U4 CEID // 事件ID核心字段 L [n] // 报告列表 L [2] U4 RPTID // 报告ID L [m] // 变量列表 V // 变量值类型不定 ... ... 几个关键点需要提前明确DATAID一般填0表示不关心数据ID主机侧通常忽略。CEID事件ID这是主机判断发生了什么的唯一依据。比如CEID1001可能代表晶圆加工开始CEID1002代表晶圆加工结束。RPTID报告ID一个CEID可以关联多个报告每个报告是一组变量的集合。VID变量ID实际的数据点。VID和RPTID的对应关系在设备侧和主机侧必须通过S2F33/S2F35等报文提前协商好否则解析出来就是一堆数字。这里有个新手常犯的错误以为S6F11里直接带VID。实际上S6F11里带的是RPTID和变量值VID是通过报告定义S2F33提前绑定好的。主机收到S6F11后需要先查自己维护的报告-变量映射表才能知道每个值对应哪个数据点。这个映射关系如果没同步好解析出来的数据就是错位的。2.2 为什么选secs4net而不是自己撸字节市面上做SECS/GEM通信的C#方案大致分三类方案优点缺点适用场景自己解析字节流完全可控无依赖工作量大容易出错SECS-II嵌套结构处理复杂学习原理、极简场景商业库如Cimetrix功能全有技术支持贵授权复杂大型量产项目开源库secs4net免费社区活跃API清晰文档偏少部分高级功能需自己扩展中小项目、学习、快速原型我选secs4net的理由很直接它把SECS-II的Item模型抽象得很干净Item.L、Item.U4、Item.A这些静态方法构造报文非常直观解析时也能递归遍历。而且它同时支持HSMS和SECS-I连接管理、超时重传这些底层逻辑都封装好了你只需要关注业务层的报文处理。安装很简单NuGet直接搜secs4netdotnet add package secs4net如果你用的是老版本的.NET Framework项目注意选对应版本secs4net对.NET Standard 2.0及以上支持比较好。我实测在.NET 6/8上跑得最顺.NET Framework 4.7.2也能用但异步API的体验会差一些。2.3 连接层HSMS还是SECS-I现在新设备基本都用HSMSHigh-Speed SECS Message Services走TCP/IP默认端口5000。SECS-I是老的串口方案速率低现在很少见了。secs4net里用HsmsConnection类来管理连接var options new HsmsConnectionOptions { IsActive true, // 主动连接模式 IpAddress 192.168.1.100, Port 5000, DeviceId 0, SocketReceiveBufferSize 65535, SocketSendBufferSize 65535 }; var connection new HsmsConnection(options);IsActive true表示你的程序作为主动方去连接设备或主机。如果对方是主动方你就设false做被动监听。这个方向搞反了连接永远建不起来这是最常见的入门坑之一。3. 核心实现用C#接收并解析S6F11的完整链路3.1 建立连接与消息订阅secs4net的连接对象暴露了一个Start()方法返回一个IObservablePrimaryMessageWrapper你可以用Rx的方式订阅也可以用await foreach。我习惯用后者代码更直观await connection.StartAsync(); _ Task.Run(async () { await foreach (var wrapper in connection) { try { await HandleMessageAsync(wrapper); } catch (Exception ex) { _logger.LogError(ex, 处理消息失败); } } });这里有个细节wrapper是PrimaryMessageWrapper类型它包含了完整的报文信息包括Message属性SECS-II内容和ReplyAsync方法用于回复。S6F11是Primary Message按照SECS规范接收方必须回复一条S6F12Event Report Acknowledge。如果你不回复设备侧会一直等超时后可能重发甚至断开连接。3.2 判断消息类型并提取CEID收到消息后第一件事是判断它是不是S6F11private async Task HandleMessageAsync(PrimaryMessageWrapper wrapper) { var msg wrapper.Message; if (msg.S ! 6 || msg.F ! 11) { // 不是S6F11交给其他处理器 await DispatchOtherAsync(wrapper); return; } // 解析S6F11 var root msg.SecsItem; // 根Item类型是L var dataId root[0].FirstValueuint(); // DATAID var ceid root[1].FirstValueuint(); // CEID var reportList root[2]; // 报告列表 // 处理业务 ProcessEvent(ceid, reportList); // 必须回复S6F12 await wrapper.ReplyAsync( new SecsMessage(6, 12, replyExpected: false) { SecsItem Item.B(0) // ACKC60表示接受 }); }FirstValueuint()是secs4net提供的便捷方法直接从Item里取第一个值并转成指定类型。如果类型不匹配会抛异常所以生产环境里最好用TryGetFirstValue或者先判断格式码。3.3 遍历报告列表与变量值报告列表的结构是嵌套的L需要递归或双层循环遍历private void ProcessEvent(uint ceid, Item reportList) { var eventDef _eventRepository.Get(ceid); if (eventDef null) { _logger.LogWarning(未定义的事件ID: {Ceid}, ceid); return; } foreach (var reportItem in reportList.Items) { var rptId reportItem[0].FirstValueuint(); var values reportItem[1]; var reportDef _reportRepository.Get(rptId); if (reportDef null) { _logger.LogWarning(未定义的报告ID: {RptId}, rptId); continue; } for (int i 0; i values.Items.Count; i) { var vid reportDef.VidList[i]; var rawValue values.Items[i]; var value ConvertValue(rawValue, reportDef.VidTypes[i]); _dataService.Update(vid, value); } } }这段代码的核心逻辑是CEID → 事件定义 → RPTID列表 → 报告定义 → VID列表 → 变量值。每一层都需要查表所以项目里必须维护好事件定义、报告定义、变量定义这三张表。这些表通常来自设备的S2F33/S2F35报文或者从设备的GEM手册里手工录入。3.4 变量值的类型转换SECS-II的变量值类型很多ASCIIA、BinaryB、BooleanBOOLEAN、U1/U2/U4/U8、I1/I2/I4/I8、F4/F8等。secs4net的Item对象保留了原始格式码转换时要根据VID定义的类型来处理private object ConvertValue(Item item, Type targetType) { return item.Format switch { SecsFormat.ASCII item.FirstValuestring(), SecsFormat.U4 item.FirstValueuint(), SecsFormat.U2 item.FirstValueushort(), SecsFormat.U1 item.FirstValuebyte(), SecsFormat.I4 item.FirstValueint(), SecsFormat.F4 item.FirstValuefloat(), SecsFormat.F8 item.FirstValuedouble(), SecsFormat.Boolean item.FirstValuebool(), SecsFormat.Binary item.FirstValuebyte[](), _ throw new NotSupportedException($不支持的格式: {item.Format}) }; }实际项目里我建议把VID的类型定义也维护在配置表里而不是靠运行时猜。因为有些设备会把U4的值用U2发过来值没超过65535如果你按U4去取secs4net会抛异常。这种类型降级的情况在老旧设备上很常见。4. 实战中踩过的坑与排查技巧4.1 报告定义不同步导致的数据错位这是最隐蔽的问题。设备侧改了报告定义比如在RPTID1001里加了一个VID但主机侧的表没更新结果就是变量值数量对不上或者值全部错位。表现是数据看起来能解析但数值明显不对。排查方法在解析时加一个校验比较values.Items.Count和reportDef.VidList.Count不一致就告警if (values.Items.Count ! reportDef.VidList.Count) { _logger.LogError(报告{0}变量数量不匹配收到{1}定义{2}, rptId, values.Items.Count, reportDef.VidList.Count); // 可以选择跳过或按最小数量解析 }更稳妥的做法是定期用S2F33/S2F35去同步报告定义或者监听设备的S2F37事件报告链接变化。4.2 回复超时与消息积压S6F11是高频消息一条产线上可能每秒好几条。如果你在await foreach里做耗时操作比如写数据库、调外部API消息会积压最终导致HSMS超时。我的做法是接收和业务处理解耦。接收线程只负责解析和入队业务处理放到独立的消费者线程private readonly ChannelEventMessage _channel Channel.CreateBoundedEventMessage(10000); // 接收侧 _channel.Writer.TryWrite(new EventMessage(ceid, parsedData)); // 消费侧 await foreach (var evt in _channel.Reader.ReadAllAsync()) { await _businessService.HandleAsync(evt); }用BoundedChannel还能防止内存无限增长满了就丢弃或阻塞根据业务重要性决定。4.3 常见问题速查表现象可能原因排查方向连接建不起来IsActive方向反了确认哪边主动连接收到S6F11但解析报错Item索引越界打印原始报文结构确认层级变量值类型转换异常设备类型降级用TryGetFirstValue加类型兼容数据错位报告定义不同步校验变量数量同步S2F33消息积压超时业务处理太慢接收与处理解耦用队列回复后设备仍重发ACKC6值不对确认回复的是S6F12且ACKC604.4 日志与调试技巧SECS调试最痛苦的是报文是二进制或SML格式肉眼难读。secs4net支持把Item转成SML字符串调试时非常有用_logger.LogDebug(收到S6F11: {Sml}, msg.SecsItem.ToSml());输出大概长这样L [3] U4 0 U4 1002 L [1] L [2] U4 1001 L [3] A LOT123 U4 25 F4 3.14 一眼就能看出结构对不对。生产环境记得把日志级别调高不然高频S6F11会把磁盘写爆。5. 从能跑到好用工程化建议与扩展思路5.1 配置化而非硬编码事件定义、报告定义、变量定义这三张表千万不要硬编码在代码里。我见过太多项目把CEID和VID写死在switch-case里设备一升级就全废。正确做法是放到数据库或JSON配置文件支持热更新。表结构大致如下-- 事件定义 CREATE TABLE EventDef ( Ceid INT PRIMARY KEY, EventName NVARCHAR(100), Description NVARCHAR(500) ); -- 报告定义 CREATE TABLE ReportDef ( RptId INT PRIMARY KEY, Ceid INT, VidList NVARCHAR(MAX) -- JSON数组 ); -- 变量定义 CREATE TABLE VariableDef ( Vid INT PRIMARY KEY, VarName NVARCHAR(100), DataType NVARCHAR(20), Unit NVARCHAR(20) );这样设备换型或升级时只需要更新配置不用重新编译部署。5.2 用反射或表达式树做动态映射如果你想把解析出来的数据直接映射到C#对象可以用反射或表达式树。比如定义一个WaferProcessCompletedEvent类字段用[Vid(1001)]标注解析时自动填充。这样业务代码就很干净[SecsEvent(Ceid 1002)] public class WaferProcessCompletedEvent { [Vid(1001)] public string LotId { get; set; } [Vid(1002)] public uint WaferCount { get; set; } [Vid(1003)] public float Temperature { get; set; } }表达式树比反射快但实现复杂。中小项目用反射足够了性能瓶颈通常不在这一层。5.3 异常处理与断线重连HSMS连接不是永远稳定的网络抖动、设备重启都会断。secs4net的HsmsConnection有ConnectionChanged事件可以监听并做重连connection.ConnectionChanged async (sender, e) { if (e.NewState ConnectionState.Selected) { _logger.LogInformation(HSMS连接已建立); await SyncReportDefinitionsAsync(); // 重连后同步定义 } else if (e.NewState ConnectionState.Retry) { _logger.LogWarning(连接断开等待重连...); } };重连后一定要重新同步报告定义因为设备可能在这期间重启过定义可能变了。5.4 性能考量高频S6F11场景下几个优化点避免在接收线程做IO入队即可写库交给后台。批量写库用Dapper的批量插入或SqlBulkCopy别一条一条insert。对象池如果解析量大考虑复用Item解析的中间对象减少GC压力。日志采样高频消息不要每条都打Debug日志按比例采样。我实测过单连接每秒处理500条S6F11用Channel解耦Dapper批量写CPU占用不到10%。如果超过这个量级就要考虑多连接或分布式处理了。5.5 测试策略SECS通信的测试是个难点因为真实设备不好随时用。我的做法是写一个模拟设备端用secs4net起一个被动连接按脚本发送S6F11// 模拟设备发送S6F11 var s6f11 new SecsMessage(6, 11, replyExpected: true) { SecsItem Item.L( Item.U4(0), Item.U4(1002), Item.L( Item.L( Item.U4(1001), Item.L( Item.A(LOT123), Item.U4(25), Item.F4(3.14f) ) ) ) ) }; await connection.SendAsync(s6f11);这样可以在没有真实设备的情况下把解析逻辑、异常处理、性能都测一遍。模拟端还能故意发错误报文测试你的容错能力。6. 一些个人体会做SECS/GEM开发这几年最大的感受是协议本身不难难的是对设备和业务的理解。S6F11的解析代码熟练的C#工程师半天就能写出来但真正让系统稳定运行的是那些看不见的东西——报告定义的同步机制、异常报文的容错、高频消息的背压处理、断线重连后的状态恢复。我踩过最深的坑是一个老旧设备在发送S6F11时偶尔会把某个U4变量用U2格式发出来。因为值没超过65535设备侧觉得没问题但主机侧按U4解析就抛异常导致整条消息被丢弃。后来我在类型转换层加了一层兼容先尝试按定义类型取失败就按实际格式码取再失败才告警。这个改动之后那条产线的数据完整率从92%提到了99.9%。还有一个经验永远不要相信设备会按手册发报文。手册写的是标准实际设备发出来的可能是各种变体。所以解析代码要写得宽容一些能容错就容错实在容不了再告警。当然宽容不等于放任关键字段比如CEID错了还是要严格处理。最后分享一个小技巧如果你在排查S6F11解析问题先把原始报文用SML打印出来然后拿设备的GEM手册对照一层一层看结构。90%的问题都能通过这种方式定位。剩下的10%大概率是报告定义不同步去查S2F33的同步日志就行。
返回列表