简介:这份资源面向半导体设备通信方向的C#开发者与工控软件工程师,提供一套基于HSMS通信的SECS协议WinForm窗体程序及配套类库源码,可用于快速理解SECSII/HSMS协议在.NET平台上的落地方式,并作为设备端或主机端通信模块的开发起点。压缩包为zip格式,整体约3.3MB,内含窗体程序与类库工程源码,并附有连接使用文档,代码中带有中文注释,便于对照阅读与二次开发。目前已有2724人学习下载,说明该方向存在稳定的学习需求。读者可从中获取SECS消息收发、HSMS连接建立与状态管理、窗体交互与类库分层设计等完整实现思路,结合文档快速跑通通信流程,并在此基础上按自身设备协议扩展消息项与业务逻辑,减少从零搭建通信框架的时间成本。
1. 从一台半导体设备联调说起:这套 C# WinForm SECS/HSMS 源码到底能干什么
如果你在半导体、光伏、面板或者 SMT 行业做过上位机,大概率遇到过这样的场景:设备端的 PLC 和工控机都调通了,唯独跟 MES/EAP 那台服务器连不上,对方工程师甩过来一句“你们 SECS 没通,先把 HSMS 建起来再说”。这时候你打开 Visual Studio,发现手头只有一份设备厂商给的 PDF 协议文档,连个能跑的 Demo 都没有。这套C# WinForm 基于 HSMS 通信的 SECS 协议窗体程序及类库源码,解决的就是这个从零到一的问题——它把 SECS-II 消息编解码、HSMS 会话管理、WinForm 界面调试工具打包在一起,让你能直接看到报文收发过程,而不是对着日志猜。
它适合三类人:一是刚接触半导体设备通信、需要快速理解 SECS/GEM 报文结构的 C# 开发者;二是要给现有设备加 SECS 接口、但不想从字节流开始手搓的工控上位机工程师;三是拿它当教学案例,讲 TCP 长连接、状态机、二进制协议解析的培训场景。源码本身是 WinForm 窗体加类库的结构,类库负责协议逻辑,窗体负责调试展示,这个分层对后续集成到自己的项目里比较友好。
2. 拆开类库看结构:SECS-II 消息体与 HSMS 会话是怎么分层的
2.1 先分清 SECS-II 和 HSMS 各管什么
很多人第一次接触会把这俩混在一起说,其实它们是两层。SECS-II 定义的是“消息长什么样”——一个消息由 Stream 和 Function 组成,比如 S1F1 是 Are You There 请求,S1F2 是回复。消息体里是一串 Item,每个 Item 有格式码(List、Binary、Boolean、ASCII、I4、F8 等)和长度。HSMS 定义的是“消息怎么在 TCP 上跑”——包括连接建立、Select/Deselect 状态、心跳(Linktest)、以及把 SECS-II 消息包上 10 字节头再发出去。
这套源码的类库通常会把这两层分开:一个SecsMessage类管 Stream/Function/Item 的组装和解析,一个HsmsConnection类管 Socket 连接和会话状态。你拿到源码后,先看类库的命名空间划分,一般能看出作者的分层意图。如果所有逻辑都塞在一个SecsHelper里,那集成起来会痛苦一些;如果分得清楚,你就能只取 SECS-II 编解码部分,把 HSMS 换成自己写的连接管理。
2.2 消息头的 10 个字节,每个都有用
HSMS 消息头固定 10 字节,这是整个通信的基石。源码里一般会有一个HsmsHeader结构体或者类来映射这 10 个字节。字段含义如下:
| 字节偏移 | 字段 | 说明 |
|---|---|---|
| 0-1 | SessionID | 会话 ID,设备端通常为 0xFFFF 表示不指定 |
| 2 | Stream | 高 7 位,第 8 位是 W-bit(是否需要回复) |
| 3 | Function | 功能码 |
| 4 | PType | 协议类型,SECS-II 固定为 0 |
| 5 | SType | 会话类型,0 表示数据消息,1 是 Select.req,2 是 Select.rsp,9 是 Linktest |
| 6-9 | SystemBytes | 系统字节,用于匹配请求和回复 |
你在调试时如果发现对方不回复,先抓包看 SystemBytes 有没有原样带回。很多“玄学”问题其实是 SystemBytes 没对上,或者 W-bit 设了但对方没回。
2.3 用代码块看一个 S1F1 的组装过程
下面这段代码演示如何用类库组装一条 S1F1 消息并发送。注意不同源码的 API 命名可能不同,这里按常见设计写:
// 创建 SECS-II 消息,S1F1,W-bit 置 1 表示需要回复 SecsMessage msg = new SecsMessage(1, 1, true); // S1F1 通常不带 Item,但有些场景会带一个 ASCII 的 MDLN // msg.AddItem(new SecsItem(SecsFormat.ASCII, "EQP001")); // 通过 HSMS 连接发送,内部会封装 10 字节头 HsmsConnection conn = new HsmsConnection("192.168.1.100", 5000); conn.Connect(); conn.Send(msg); // 接收回复,通常需要异步或轮询 SecsMessage reply = conn.Receive(); Console.WriteLine($"收到 S{reply.Stream}F{reply.Function}");逻辑说明:SecsMessage构造函数接收 Stream、Function 和 W-bit。AddItem用于往消息体里追加 Item,S1F1 一般不带,但如果你要传设备型号可以加。HsmsConnection.Send内部会做三件事:把 SecsMessage 序列化成字节数组、拼上 10 字节头、通过 Socket 发出去。参数方面,IP 和端口是设备端或 Host 端监听的地址,SECS 常用端口是 5000,但实际项目里可能改。
提示:如果你拿到的源码里
Send是同步阻塞的,在高频消息场景下要改成异步,否则界面会卡死。
2.4 类库的依赖和编译环境
这套源码一般基于 .NET Framework 4.0 到 4.8 之间,用 VS2015 或更高版本打开。类库项目通常不依赖第三方 NuGet 包,纯 Socket 和字节操作,这对现场部署是好事——不用考虑客户机器上能不能装包。但要注意,如果源码里用了System.Threading.Tasks或者async/await,那目标框架至少是 4.5。打开解决方案后先看每个项目的目标框架,不一致的话统一一下,否则编译报错。
3. 把窗体程序跑起来:连接、发消息、看日志的三步操作
3.1 第一步:配置连接参数并建立 HSMS 会话
窗体程序一般会有一个区域让你填 IP、端口,选择主动连接还是被动监听。半导体设备端通常是 Passive 模式,等 Host 来连;Host 端是 Active 模式。你调试时如果只有一台电脑,可以开两个实例,一个 Active 一个 Passive,自己连自己。
操作步骤:
- 打开解决方案,找到 WinForm 项目设为启动项目。
- 编译运行,在界面上填入本机 IP 和端口 5000。
- 如果作为 Passive 端,点“监听”按钮;如果作为 Active 端,点“连接”按钮。
- 观察状态栏是否显示“Selected”或“Connected”。HSMS 连接建立后还有一个 Select 过程,只有 Select 成功才能发数据消息。
// 被动监听模式示例 HsmsConnection conn = new HsmsConnection(); conn.Port = 5000; conn.Mode = HsmsMode.Passive; conn.OnSelected += () => { // Select 完成后才能发 S1F1 this.Invoke(new Action(() => { txtLog.AppendText("HSMS Selected, 可以发消息了\r\n"); })); }; conn.Start();参数说明:Mode决定主动还是被动,OnSelected是 Select 成功后的回调。如果你发现连接上了但发消息没反应,先看有没有触发 Selected。
3.2 第二步:手动构造消息并观察收发日志
窗体上通常有 Stream、Function 输入框,以及一个 Item 编辑区。你可以手动填 S1F1,点发送,然后在日志区看到十六进制报文。这一步是理解协议最快的方式。
常见做法是:先发 S1F1,对方回 S1F2;再发 S1F13(Establish Communications Request),对方回 S1F14。S1F13 是 GEM 里建立通信的标准消息,很多 Host 要求先发这个。
// 发送 S1F13,带两个 Item:MDLN 和 SOFTREV SecsMessage s1f13 = new SecsMessage(1, 13, true); s1f13.AddItem(new SecsItem(SecsFormat.ASCII, "EQUIP01")); s1f13.AddItem(new SecsItem(SecsFormat.ASCII, "1.0.0")); conn.Send(s1f13);逻辑说明:S1F13 的回复 S1F14 里第一个 Item 是 COMMACK,0 表示成功。如果你收到非 0,说明对方不接受通信建立,要查对方的 GEM 配置。
3.3 第三步:用日志区排查字节级问题
窗体程序的日志区一般会显示发送和接收的原始字节。你要学会看几个关键点:前 4 个字节是不是FF FF 00 00(SessionID 和 Stream/Function 的常见值)、SType 是不是 0、SystemBytes 是不是递增。如果日志里只有发送没有接收,先确认对方 IP 和端口对不对,再用telnet测一下端口通不通。
注意:有些源码的日志区只显示解析后的消息,不显示原始字节。这种情况下你需要自己在
Send和Receive里加BitConverter.ToString打印,否则排查字节对齐问题会很痛苦。
3.4 把类库集成到自己项目的注意事项
如果你不想用它的窗体,只想用类库,那步骤是:把类库项目加到你的解决方案,引用它,然后自己写连接管理。注意几点:一是类库里的HsmsConnection可能直接依赖System.Windows.Forms做日志回调,集成到控制台或服务里会报错,需要把日志回调改成事件或接口;二是线程模型,如果类库在后台线程收消息,你更新 UI 要Invoke;三是异常处理,Socket 断线重连逻辑如果源码里没有,你得自己补。
4. 避坑与排查:HSMS 连不上、消息不回、界面卡死的常见原因
4.1 现象:TCP 连上了,但一直不 Selected
原因:HSMS 的 Select 过程需要双方交换 Select.req 和 Select.rsp。如果一方发了 Select.req 但另一方没回,或者回了但 SystemBytes 不对,就会卡在 Connecting 状态。有些源码把 Select 做成自动的,有些需要手动触发。
解决:先确认双方模式匹配——Active 端发 Select.req,Passive 端回 Select.rsp。抓包看 SType 是不是 1 和 2。如果源码里 Select 逻辑有问题,可以手动构造 SType=1 的消息发出去。
4.2 现象:S1F1 发出去了,对方不回 S1F2
原因:最常见的是 W-bit 没设。S1F1 的 W-bit 必须是 1,对方才会回复。其次是 SystemBytes 冲突,如果你连续发多条消息但 SystemBytes 没递增,对方可能丢弃。还有可能是对方要求先建立通信(S1F13)才能响应其他消息。
解决:检查SecsMessage构造时第三个参数是不是true。检查 SystemBytes 生成逻辑,确保每条消息唯一。先发 S1F13 再发 S1F1。
4.3 现象:窗体界面发完消息就卡死
原因:Send或Receive在 UI 线程上同步阻塞。SECS 通信是长连接,Receive如果写成同步等待,UI 线程就被占住了。
解决:把通信逻辑放到后台线程,用Task.Run或Thread。UI 更新通过Invoke回到主线程。如果源码里已经是异步的,检查是不是在async方法里用了.Result或.Wait(),这也会死锁。
4.4 现象:收到的消息解析出来是乱码或 Item 错位
原因:SECS-II 的 Item 长度字段有 1 字节、2 字节、3 字节三种格式,取决于长度值。如果解析时固定按 2 字节读,遇到短消息就会错位。另外 ASCII 和 Binary 的长度单位不同,ASCII 是字符数,Binary 是字节数。
解决:对照 SEMI E5 标准检查 Item 长度解析逻辑。常见做法是:先读第一个字节,如果最高位是 0,长度就是 1 字节;如果是 1,再看后续字节。源码里如果这部分写死了,需要改。
4.5 现象:长时间运行后连接断开,重连失败
原因:HSMS 有 Linktest 机制,如果一段时间没数据,一方会发 Linktest.req,另一方要回 Linktest.rsp。如果源码没实现心跳,中间的网络设备可能把长连接掐掉。重连失败往往是 Socket 没正确释放,端口被占用。
解决:在类库里加定时器,每隔 30 秒发一次 Linktest。重连时先Close再Dispose,确保 Socket 资源释放。如果源码里没有心跳,这是你必须补的第一个功能。
5. 进阶用法:把 SECS 类库改造成可复用的通信组件
5.1 用接口隔离日志和 UI 依赖
原始源码的类库如果直接调TextBox.AppendText,那它只能用在 WinForm 里。改造成可复用组件的第一步是定义日志接口:
public interface ISecsLogger { void Log(string direction, byte[] data); void Log(string message); } // 在 HsmsConnection 里注入 public class HsmsConnection { public ISecsLogger Logger { get; set; } private void OnReceive(byte[] data) { Logger?.Log("RX", data); // 后续解析... } }这样你在 WinForm 里传一个把日志写到 TextBox 的实现,在控制台里传一个写到 Console 的实现,类库本身不依赖任何 UI 框架。参数说明:direction用 "TX" 和 "RX" 区分收发,data是原始字节,方便你做十六进制展示或存文件。
5.2 用状态机管理 HSMS 会话生命周期
HSMS 会话有明确的状态:NotConnected、Connected、Selected。用状态机管理比一堆 bool 标志清晰得多。常见做法是定义一个枚举和转移表:
| 当前状态 | 事件 | 下一状态 | 动作 |
|---|---|---|---|
| NotConnected | TCP 连接成功 | Connected | 启动接收线程 |
| Connected | 收到 Select.req | Selected | 回 Select.rsp |
| Connected | 发出 Select.req 并收到 rsp | Selected | 触发 OnSelected |
| Selected | 收到 Linktest.req | Selected | 回 Linktest.rsp |
| Selected | TCP 断开 | NotConnected | 触发 OnDisconnected |
你可以在类库里加一个HsmsStateMachine类,把状态转移和动作分开。这样排查问题时,日志里打印状态变化,一眼就能看出卡在哪一步。
5.3 消息编解码的单元测试怎么写
SECS-II 的编解码是纯逻辑,最适合写单元测试。你可以构造已知字节数组,解析成 SecsMessage,再序列化回去,看是否一致。比如 S1F1 的字节是00 00 00 01 00 00 00 00 00 00(10 字节头,无 Item),你可以断言解析出的 Stream=1、Function=1、W-bit=true。
[TestMethod] public void ParseS1F1_ShouldReturnStream1Function1() { byte[] raw = new byte[] { 0xFF, 0xFF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; SecsMessage msg = SecsMessage.Parse(raw); Assert.AreEqual(1, msg.Stream); Assert.AreEqual(1, msg.Function); Assert.IsTrue(msg.WBit); }注意 SystemBytes 在测试里可以固定,实际运行时必须递增。单元测试覆盖边界情况:空 Item、超长 Item、多字节长度、嵌套 List。这些测过了,现场调试会少很多血泪经验。
5.4 我踩过的一个坑:SystemBytes 回绕
SystemBytes 是 4 字节无符号整数,从 0 到 4294967295。我见过一个项目连续运行几个月后,SystemBytes 回绕到 0,结果和之前某条消息的 SystemBytes 撞了,对方直接丢弃。后来我在生成逻辑里加了一个判断:如果当前值等于 0,就跳到 1,并且记录日志。从那以后我每次集成 SECS 类库,都强制走一遍 SystemBytes 递增和回绕测试,确认不会重复。希望帮到你。
本文还有配套的精品资源,点击获取