
简介面向半导体设备自动化与主机通信开发者的C# SECS/HSMS通信实例源码包基于secs4net-master开源项目完整演示了通过TCP/IP实现SECS和HSMS消息交换的核心流程。资源共1293个文件以C#源码260个.cs、程序集655个.dll、配置与工程文件config、csproj、xml等为主兼容Visual Studio解决方案便于直接编译调试。压缩包大小约32.7MB已吸引8371人学习参考。从源码中可系统掌握套接字连接管理、SECS消息编解码、多线程异步收发、异常与日志处理等关键环节并参考其分层封装与API设计思路适合正在搭建半导体设备EAP通信模块或希望深入理解HSMS协议的开发者。1. 项目概述与场景分析1.1 为什么是C#和SECS这个组合半导体行业的朋友应该深有体会设备联网和数据采集这件事十家有八家绕不开SECS协议。这个被SEMI组织定义的标准协议族从1980年代诞生至今依然是晶圆制造、封测、光伏、LED等泛半导体领域设备通信的事实标准。无论是贴片机、固晶机、分选机还是PVD、刻蚀、清洗设备几乎都标配了SECS/GEM接口。而C#在上位机开发中的地位也不用多说尤其是配合WinForms或WPF做设备界面、数据展示、报表统计开发效率比C高一大截。更关键的是SECS通信本身是典型的TCP/IP加消息帧的交互模式这和C#的异步Socket模型、事件驱动机制配合得相当自然。我最早接触这个组合是在一个光伏组件产线项目里设备端是某国产分选机要求上位机通过SECS-II消息实时获取良率数据和Bin分布那时候市面上可参考的C#实现少得可怜大部分资料都是C或者C语言的DLL封装硬啃了一个多月才把整个通信链路跑通。这篇内容就是把我踩过的坑、最终沉淀下来的通信框架和关键代码逻辑整理出来适合需要对接SECS设备的软件工程师、设备工程师或者正在评估用什么方案做设备联网的自动化团队参考。内容从协议基础开始逐步深入到可运行的C#通信实例核心代码可以直接拉下来改改IP和Port用。1.2 这个实例解决什么问题先说结论这套C# SECS通信实例解决的是从零搭建SECS/GEM通信链路的完整问题包括连接管理、消息收发、SECS-II数据解析、消息状态机和异常处理。不是给你一个Demo级别的Ping-Pong程序而是一个能直接嵌入到正式上位机工程里的通信模块。很多人在网上搜SECS C# 源代码下载下来发现要么是封装好的商业DLL只能调用不能看实现要么是半成品只能发一条S1F1收到回复就结束了。真正到了产线上你得面对几十种消息类型、设备主动上报、通信中断重连、数据超时这些实际问题。这个实例把这些生产环境常见的需求都考虑进去了消息层用配置文件定义收发用事件驱动重连有看门狗机制整套逻辑跑起来以后换设备、加消息基本上不用改通信层的代码。提示如果你完全是SECS新手建议先花半天时间理解SECS-I/HSMS和SECS-II的基本概念再来看代码否则直接对着代码看会有点懵。2. SECS通信协议核心机制拆解2.1 协议分层别被一堆缩写吓住SECS/GEM这一堆缩写看着吓人实际上协议分层非常清晰。底层的SECS-I是RS-232串口传输现在基本已经被HSMSHigh-Speed Message Service取代了HSMS走TCP/IP把SECS消息封装成标准帧格式传输。中间层是SECS-II定义了消息内容的数据结构和语义也就是消息体里的数据怎么组织。最上层是GEM它规定了设备必须具备的标准状态机、报警管理、配方管理、数据采集等功能集合。打个比方HSMS就像快递运输的货车负责把包裹从一个城市运到另一个城市SECS-II相当于包裹里填好的快递单规定了什么东西放在什么位置GEM则是整个快递系统的服务标准比如承诺几天送达、丢件怎么赔。你在C#里要做的就是实现货车HSMS通信层和快递单SECS-II消息构建与解析这两部分GEM的功能逻辑在此基础上用业务代码去实现。2.2 HSMS消息帧结构HSMS通信中链路上跑的不是裸数据而是按固定格式封装的帧。HSMS消息帧由消息头Message Header和正文Data Message组成消息头固定10个字节结构如下偏移长度字段说明02Message Length消息头消息体的总长度大端序21Session ID会话ID通常设备用0xFFFF31Header Byte 2流号Stream41Header Byte 3功能号Function51PType消息类型0表示SECS-II消息61SType会话类型0表示数据消息74System Bytes消息序列号用于请求响应对匹配C#中用字节数组通过BitConverter和移位操作来拼装。开发初期建议先实现一个MessageHeader类字段齐全方便调试时在日志里完整打印每个字节的含义排查问题会轻松很多。public class HsmsHeader { public int SessionId { get; set; } public byte Stream { get; set; } public byte Function { get; set; } public byte PType { get; set; } public byte SType { get; set; } public uint SystemBytes { get; set; } public byte[] ToBytes() { var data new byte[10]; data[0] 0; data[1] 0; data[2] (byte)((SessionId 8) 0xFF); data[3] (byte)(SessionId 0xFF); data[4] Stream; data[5] Function; data[6] PType; data[7] SType; data[8] (byte)((SystemBytes 24) 0xFF); data[9] (byte)((SystemBytes 16) 0xFF); return data; } }2.3 SECS-II消息格式与SML表示SECS-II消息由流号Stream和功能号Function唯一标识比如S1F1表示Are You There请求S1F2表示On Line Data响应。消息内容采用一种自描述的数据项格式每个数据项由格式代码Format Code、字节长度和实际数据三部分组成。SECS-II中常见的数据项格式包括ListL可嵌套的容器类似JSON的数组ASCIIA定长或变长字符串BinaryB二进制字节数组U4/I4/U2/I2/U8/I8无符号/有符号整数F4/F8浮点数BooleanBoolean布尔值理解这套结构是写代码的关键。我的做法是先把每个要用到的SECS-II消息在SMLSECS Message Language格式下写一遍比如S1F3的SML表达如下S1F3 W L U4 设备ID L L U4 1A MODULE_ID L U4 2A SOFTWARE_REV SML写清楚了C#侧的构建和解析逻辑就非常直观了所以强烈建议先手写SML再写代码。2.4 消息状态机的必要性SECS通信中每条消息的处理都遵循一定的状态流转。初学者最容易踩的坑是——把SECS当成普通的请求响应发一条消息等待回复结束了。但现实情况是设备在通信过程中随时可能主动上报事件比如S6F11也可能因为异常发送报警S5F1如果你的代码只处理同步请求响应这些异步消息一来就会让程序逻辑乱掉。这里推荐用状态机管理通信链路和消息处理。通信链路至少有四种状态未连接、连接中、已连接在线、连接断开。消息处理层面每条请求消息发出后要记录System Bytes和对应的事务状态收到响应时通过System Bytes匹配事务收到非预期的主动上报消息则走事件分发。注意System Bytes是消息匹配的关键绝对不能随便指定。请求消息发出时要用全局递增计数器生成System Bytes收到的响应消息必须包含请求消息相同的System Bytes这是SECS协议的基本约定。3. C#通信模块架构与代码实现3.1 整体架构通信层与业务层解耦在动手写代码之前先把架构想清楚。这一个多月踩坑的总结是通信模块如果和业务逻辑耦合在一起后面调试会让你痛不欲生。我最终采用的是四层结构层级职责关键类传输层TCP连接管理、断线重连HsmsConnection消息层HSMS帧编解码、SECS-II数据项构建解析HsmsMessage, SecsItem事务层请求响应对匹配、超时管理TransactionManager业务层具体设备逻辑、事件处理EquipmentService依赖关系是单向的业务层依赖事务层事务层依赖消息层消息层依赖传输层。每一层只关心自己职责范围内的事情这样后续换设备协议、增加新消息都只需要在业务层扩展不会动到底层通信代码。3.2 传输层基于TcpClient的异步收发传输层是整个通信模块的地基。我用TcpClient包装配合NetworkStream做异步读写。这里有个关键细节——HSMS消息是TCP流式传输的没有天然的边界所以必须从缓冲区完整解析出一个消息帧后才能处理不能每次Read到数据就直接当一条消息用。public class HsmsConnection { private TcpClient _client; private NetworkStream _stream; private readonly object _lockObj new object(); private readonly Listbyte _receiveBuffer new Listbyte(); public event ActionHsmsMessage MessageReceived; public event Action ConnectionClosed; public async Task ConnectAsync(string ip, int port) { _client new TcpClient(); await _client.ConnectAsync(ip, port); _stream _client.GetStream(); _ Task.Run(ReceiveLoopAsync); } private async Task ReceiveLoopAsync() { var buffer new byte[4096]; while (_client.Connected) { int read await _stream.ReadAsync(buffer, 0, buffer.Length); if (read 0) { ConnectionClosed?.Invoke(); break; } lock (_lockObj) { _receiveBuffer.AddRange(buffer.Take(read)); TryParseMessages(); } } } private void TryParseMessages() { while (_receiveBuffer.Count 4) { int messageLength (_receiveBuffer[2] 8) | _receiveBuffer[3]; int totalLength messageLength 4; if (_receiveBuffer.Count totalLength) return; var frame _receiveBuffer.Take(totalLength).ToArray(); _receiveBuffer.RemoveRange(0, totalLength); var message HsmsMessage.FromFrame(frame); MessageReceived?.Invoke(message); } } }注意TryParseMessages这个方法里先取4个字节判断完整消息长度缓冲区不够就先返回等下一次数据到达够了才从缓冲区剥离出一条完整消息。这是TCP粘包拆包的基本操作没经验的人容易在这里翻车。3.3 消息层SECS-II数据项的动态构建SECS-II数据项用树形结构组织List可以嵌套任意子项所以消息模型天然是递归的。我设计了一个SecsItem抽象基类子类包括SecsList、SecsAscii、SecsU4、SecsBinary等。public abstract class SecsItem { public string Name { get; set; } public abstract byte[] ToBytes(); public abstract int DataLength { get; } } public class SecsAscii : SecsItem { public string Value { get; set; } public override int DataLength Value.Length; public override byte[] ToBytes() { var data Encoding.ASCII.GetBytes(Value); var header new Listbyte { 0x40 | (byte)GetLengthBytes() }; header.AddRange(EncodeLength(DataLength)); return header.Concat(data).ToArray(); } }提示Format Code的高三位表示格式类型比如0x0是List0x4是Ascii0x5是Binary低两位表示长度字段占几个字节。长度小于256用1字节小于65536用2字节依次类推。编写EncodeLength方法时一定要处理长度字节数的计算很多解析错误都出在这个细节上。发送一条S1F1Are You There消息体是空List代码大概是这样的var message new HsmsMessage { Stream 1, Function 1, WBit true, Body new SecsList() }; await connection.SendAsync(message);3.4 事务层消息匹配和超时处理发送一条带W位等待回复的消息后接下来必须在规定时间内收到匹配的响应消息。匹配依据就是System Bytes。我设计了一个TransactionManager核心是维护一个Dictionaryuint, PendingTransaction发送时注册事务收到消息时匹配事务并触发回调超时由独立的定时器扫描。public class TransactionManager { private readonly Dictionaryuint, PendingTransaction _pending new Dictionaryuint, PendingTransaction(); private uint _systemBytesCounter 0x00000001; public uint SendRequest(HsmsConnection conn, HsmsMessage message, int timeoutMs, ActionHsmsMessage callback) { uint sysBytes _systemBytesCounter; message.SystemBytes sysBytes; _pending[sysBytes] new PendingTransaction { Callback callback, Timer new Timer(o OnTimeout(sysBytes), null, timeoutMs, Timeout.Infinite) }; conn.SendAsync(message); return sysBytes; } public void OnMessageReceived(HsmsMessage message) { if (_pending.TryGetValue(message.SystemBytes, out var transaction)) { transaction.Timer.Dispose(); _pending.Remove(message.SystemBytes); transaction.Callback?.Invoke(message); } else { // 设备主动上报的消息交给事件处理器 OnEquipmentEvent?.Invoke(message); } } }超时时间一般设备端要求60秒以内生产环境建议设30秒因为如果设备没有及时响应多半是出了问题等太久只会让产线一直在等。4. 消息处理与设备状态流转实现4.1 对接过程中的关键业务消息这里整理一下最常见的基础消息大部分SECS设备联调的第一步就是从这些消息开始的消息方向用途S1F1/S1F2Host→EQ询问设备是否在线返回在线状态S1F3/S1F4Host→EQ读取设备ID、软件版本等属性S1F13/S1F14双向建立通信确认协议的建立S1F15/S1F16Host→EQ请求设备离线/确认离线S2F17/S2F18Host→EQ请求设备在线/确认在线S5F1/S5F2EQ→Host报警上报/报警确认S6F11/S6F12EQ→Host事件数据上报/确认联调的时候一定按顺序来先把S1F1/S1F2跑通再做S1F13/S1F14建立通信最后才做S2F17/S2F18操纵设备在线状态。一上来就发S2F17很多设备直接拒绝。4.2 事件上报处理S6F11的实际解析设备在产线上会主动上报各种事件最典型的是S6F11。解析S6F11时需要按SECS-II数据项结构逐层拆解逻辑清晰很重要。private void HandleS6F11(HsmsMessage message) { var body (SecsList)message.Body; int dataId ((SecsU4)body.Items[0]).Value; var reportList (SecsList)body.Items[2]; foreach (SecsItem reportItem in reportList.Items) { var reportId ((SecsU4)((SecsList)reportItem).Items[0]).Value; var values (SecsList)((SecsList)reportItem).Items[1]; Console.WriteLine($Report ID: {reportId}); foreach (var value in values.Items) { if (value is SecsAscii ascii) Console.WriteLine($ Value: {ascii.Value}); else if (value is SecsU4 u4) Console.WriteLine($ Value: {u4.Value}); // 其他类型省略 } } }收到S6F11后需要回复S6F12确认消息而且System Bytes要使用收到的S6F11的System Bytes这样设备才知道你收到了。注意S6F11的第二个数据项可能没有表示当前时间第三个数据项才是报告数据的List。解析的时候要检查Items.Count不要直接按固定索引取不规范的数据会让程序直接崩掉。4.3 设备状态机管理设备状态管理也是GEM的核心要求之一。我用的状态集合包含通信未建立Not Communicating、正在建立通信Communicating、设备在线Online、设备离线Offline、设备尝试在线Online Local等。状态之间流转是有约束的比如设备必须完成S1F13/S1F14建立通信后才能做在线/离线切换设备在离线状态下不能上报生产数据只能处理控制类消息。这些约束我封装在一个EquipmentStateMachine类里业务层调用API时先检查当前状态是否允许该操作如果设备在离线状态收到S6F11就需要主动判断并忽略。5. 调试实战与常见问题排查5.1 开发环境搭建与模拟器选型没有真机的情况下做SECS开发模拟器是必需品。我用过两款一款是免费的开发调试利器Secs4Net模拟器另一款是商业软件Pegasus Simulator。Secs4Net模拟器配置灵活可以自定义消息序列适合自动化测试Pegasus模拟器仿真度高连界面都模仿真实设备适合演示和验证。调试时建议全程开包抓取工具比如WireShark加上HSMS解析插件或者直接用模拟器自带的日志面板。国产设备厂商的调试工具大多是C写的日志格式不统一但一般都能看到收发消息的十六进制数据。把这个十六进制数据和你的代码打印的帧数据对照检查是排查问题最快的方式。5.2 高频踩坑点与解决方案问题现象根本原因解决方案设备连接上收不到任何消息设备配置的PType或SType与代码不匹配抓包确认设备发送的PType是否等于0SType是否为0收到消息无法解析报长度错误消息长度字段计算错误检查Message Length是否等于10字节消息头加上消息体长度请求发出后无响应超时报错System Bytes配置错误或消息格式不合法对比模拟器发送的请求和响应的System Bytes是否一致解析S6F11报索引越界设备可能省略了可选数据项解析前检查Items.Count条件判断通信一段时间后断连设备端心跳超时HSMS有T5/T6/T8定时器要求实现心跳机制定期发送控制消息保持连接检查T6定时器事务超时触发后的处理逻辑消息收发乱序多个线程并发发送未加锁发送方法加锁或者用SemaphoreSlim控制并发量5.3 一个真实的联调案例有一次在客户现场联调设备是台湾某品牌固晶机。按常规流程先发S1F1设备正常回复了S1F2然后发S1F13建立通信设备回复了S1F14表示通信建立成功紧接着发S2F17请求设备在线结果设备直接没有任何响应等了30秒超时然后再发任何消息都是超时。排查过程很典型。先抓包从TCP层看到的是设备根本没有回包然后看设备那边日志设备工程师说他们设备只支持S2F41Host Command上线不支持S2F17/S2F18这种老式的上线方式。让设备主动上报S6F11事件设备也是支持的。这里暴露了一个问题——SECS设备的实现差异比想象中大得多。最后是让设备工程师在设备端把GEM配置改为支持S2F17或者按他们设备的特殊要求处理上线逻辑才算解决了问题。所以联调时一定先找设备厂商要来GEM功能清单看清楚支持哪些消息别拿标准规范硬套。提示设备联调前务必向设备厂商索要SECS/GEM功能清单搞清楚设备支持哪些Stream/Function、数据项的格式、超时时间要求比什么技巧都管用。6. 工程化落地与二次开发建议6.1 日志模块的重要性SECS通信调试中日志模块的价值绝对被低估。因为现场联调时设备工程师、工艺工程师、软件工程师三个角色常常同时盯着屏幕出问题了第一件事就是翻日志。如果没有完整记录收发消息的关键信息定位问题会非常痛苦。我的日志方案是分两层第一层是通信原始日志记录每条消息的十六进制帧数据、收发方向、时间戳用于底层排查第二层是业务层日志记录解析后的消息类型、关键数据内容、设备状态变化方便业务人员理解。用NLog或者Serilog都可以实现关键是日志格式要统一时间戳精确到毫秒。6.2 如何扩展到新设备和新消息这套架构最大的好处是扩展方便。接到新设备时只需要做两件事第一在业务层增加一个新的EquipmentService子类处理该设备特有的消息逻辑第二在消息配置文件中注册设备支持的消息类型。通信层、事务层完全不用动。以对接新设备的S6F11事件数据为例只需要在EquipmentService中增加一个处理方法订阅TransactionManager的OnEquipmentEvent事件在方法里解析S6F11的Report List把数据写入数据库。新增其他设备时整个消息层的SecsItem体系是通用的设备差异只是具体消息的布局不同而已。6.3 性能优化与稳定性保障一套正式上线的SECS通信模块稳定性一定比功能性优先级更高。我这里做过的几个关键优化值得分享第一消息解析对象采用对象池复用避免高频消息触发GC频繁回收第二写数据库采用异步批量提交不要在消息回调里直接执行耗时操作避免阻塞后续消息处理第三针对高频上报事件做合并去重比如设备的温度曲线每秒上报一次如果产线需要的是分钟级数据在应用层做聚合减轻数据库压力。另外长期运行的内存问题也要关注。TransactionManager里的_pending字典如果设备持续不响应事务一直不清理内存就会缓慢增长所以超时处理里除了触发回调还要把超时事务从字典中移除。根据我的实际经验这套结构跑个把月不重启基本没问题。最后再分享一个小技巧联调前先把模拟器的消息日志打开把设备端PC和时间校准一致否则排查问题时两边日志对不上时间线会特别头疼。本文还有配套的精品资源点击获取