ARTICLE DETAIL

资讯详情

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

C#实现SECS/GEM通信:设备端与EAP主机端完整实战与踩坑记录

C#实现SECS/GEM通信:设备端与EAP主机端完整实战与踩坑记录 简介本资源是一套基于C#实现SECS/GEM通信协议的完整工程实践方案面向半导体设备开发工程师、工厂自动化系统集成人员及工业通信协议学习者解决设备端Equip与主机端EAP Host间标准SECS消息交互的落地难题。压缩包含1317个文件总大小41.58MB主体为280个C#源码文件cs、545个运行依赖DLL、16个可执行程序exe及配套配置文件config、SML协议定义文件sml和调试符号文件pdb支撑从编译构建到联调验证的全流程。已有76人下载学习资源结构清晰包含DevDeploy.bat部署脚本、Secs4Net核心类库项目csproj、SML协议解析模块及典型会话流程实现——如Socket连接后主动端发送Selected.rsp、被动端响应Select.rsp进入Selected状态并完成S1F13/S1F14握手建立标准SECS/GEM连接。读者可直接编译运行双端程序深入理解SECS消息封装、状态机管理与TCP层通信机制。 手里的SecsGem项目终于跑通了整套代码包含Equip设备端和EAP Host主机端两个程序压缩好放在一个zip里。这个项目不是简单的TCP长连接而是把半导体制造场景里常用的SECS/GEM标准通信链路用C#重新实现了一遍。如果你在做上位机、设备自动化对接MES或者准备接入EAP这套代码的思路和踩坑记录应该能帮你少走不少弯路。我会把开发过程中最关键的设计决策、消息编解码方式、设备端与主机端的交互逻辑以及联调时遇到的那些怪问题全部拆开讲。不是贴一大堆完整源码完事而是让你看懂每一层在干什么为什么这么写后面遇到问题能自己排查。1. SecsGem到底在解决什么问题1.1 半导体设备通信的链路组成SecsGem不是单一协议它是一整套由SEMI标准定义的通规则最核心的几层包括HSMS负责底层TCP/IP传输SECS-II定义消息格式和内容GEM则站在设备行为高度规定设备在线状态、报警上报、事件收集、数据采集这些功能应该如何实现。很多新人一开始搞不清楚这三者的关系我刚接触时也一样花了很长时间才理清楚。从实际开发角度来看Equip设备端做的就是暴露一个TCP服务端等待Host连接然后按照SECS规定回复各种Primary/Secondary消息。EAP Host端则更像主动方要向设备发起会话、订阅事件、下发参数、读取数据。这两端都要自己处理连接生命周期、消息ID对应、超时重传还有SECS-II消息体的序列化和反序列化。这套协议本身不涉及上层业务逻辑它更像通用的工业级通信管道。设备端在这条管道上报告“我上电了”“我在生产”“有个报警发生了”Host则通过标准命令查询设备状态、请求变量值、设置系统时间。不管设备是炉管、光刻机还是清洗机只要按这套标准做上层的EAP、MES就能无缝接入。可能有人会问为什么不用简单的自定义TCP协议因为半导体制造系统对设备兼容性要求太高一台设备可能在多个工厂流转不同工厂会装不同MES/EAP。如果不遵循SEMI标准每次对接都要定制开发成本高而且很难维护。SecsGem的意义恰恰是标准化的设备自动化接口这也是这个项目最值得复刻的地方。1.2 为什么用C#而不是C/Python写这套通信半导体设备商传统的SECS/GEM实现大多用C/C因为设备端底层直接跑在嵌入式环境或者工控机硬实时系统上。但纯上位机层面的S/W尤其是EAP Host端C#反而很合适。C#的async/await让长连接处理非常顺手TcpListener、TcpClient封装得足够好用再配合内置的BinaryPrimitives做字节序转换写HSMS解析器比C省事太多。用Python也能做但落地到Windows工控机上部署、配置服务、对接数据库Python环境相对啰嗦。C#可以编译成单文件exe装个.NET Runtime就能跑后续做可视化配置界面也方便。当然也有商业库比如SECSComm之类的可以直接调用但版权和授权费用不便宜而且封装太黑盒出了问题不好定位。自己从协议层开始实现虽然前期工作量多些但后面排查问题会轻松很多也更可控。我并不是说商业库不好实际上如果项目周期很紧、又需要严格符合GEM认证商业库是更稳妥的选择。但若你是想深入理解这套协议或者需要定制一些特殊消息自己实现无疑是最好的方式。这个项目本质上也是出于这个动机把协议吃透再做一套可复用的两端代码。2. 动手写之前先把协议细节盘清楚2.1 HSMS消息封装与四条关键字节序HSMS是跑在TCP之上的一层封装全称是High-Speed SECS Message Services。每条HSMS消息由Header和Data两部分组成其中Header固定10字节最后4字节如果是数据消息表示SECS-II消息体的长度如果是控制消息则固定是0。前6个字节包含Session ID、Stream/Function、PType、SType以及用于消息应答配对的System Bytes。最关键的是这四个字节大小端Header中的数据长度和系统字节都是大端序即高字节在前。C#的BitConverter默认是系统小端序直接转换会出问题必须用BinaryPrimitives.ReadUInt32BigEndian或者手动移位。说实话我见过太多人在这里栽跟头现象就是S1F13回复超时或者控制消息完全无法解析最后发现系统字节高低位反了。除了数据消息HSMS还定义了Select Request/Response、Linktest Request/Response、Separate Response等控制消息。TCP连接建立成功后Host不会立刻发SECS消息而是先发Select RequestSType1建立会话设备端应答Select ResponseSType2之后才允许传递数据消息。LinktestSType5是双方互发的心跳用来检测链路是否还活着。这里要重点提一下T5、T6、T7、T8这几个定时器。T5是连接未建立时的重试时间T6是控制消息响应超时T7是连接建立超时T8是接收数据间隔超时。项目中我会把T6设成5秒T8设为10秒通过配置文件可调。如果T6设太短高负载下容易误判超时设太长链路故障后状态恢复又慢。这几个参数直接影响联调体验建议根据现场网络状况做微调。2.2 SECS-II消息体的编解码SECS-II规定消息体格式核心概念是Item。一条SECS-II消息体可以看作一棵ITEM树根节点往往是一个List下面可以套子List也可以包含单个数据项。常用的格式码有List0x01、ASCII0x40或0x41、Binary0x20、Boolean0x29、U1/U2/U4/U80x25/0x26/0x27/0x28、I1/I2/I4/I80x31/0x32/0x33/0x34、F4/F80x44/0x48。每个Item由格式码、长度、数据组成其中长度还可能使用两字节格式。因为Item是嵌套结构解码必须用递归。从根List开始读取一个Item头根据格式码判断是否要继续向下解析如果是List就递归否则读取具体长度和数据。编码时同理递归构建字节序列。这个过程中最容易犯的错是List里元素个数与后续实际Item数不匹配在解析时抛异常。我在编码器里加了一个Debug断言以及错误消息打印联调阶段非常实用。还有一种常见问题就是ASCII编码SECS-II协议里字符串没有强制统一编码但实际中绝大多数设备用ASCII。C#里如果写成Encoding.UTF8.GetBytes遇到中文或者特殊符号就可能导致字节数对不上设备端解析时直接报错。项目里统一使用Encoding.ASCII并规定所有字符串处理逻辑都走同一套工具方法避免混用。2.3 设备端和Host端各自的职责划分在动手写代码前必须把两端角色理清楚。Equip设备端是TCP服务端监听固定端口维护与Host的连接。它要处理的业务消息包括S1F1Are You There、S1F13Establish Communications Request、S2F17Date and Time Request、S6F11Event Report Send、S5F1Alarm Report Send等。简单说设备端核心逻辑是“响应请求、上报状态、上报报警、上报事件”。注意设备端也有主动发起消息的场景例如上电后主动向Host发送S1F13不等Host先问以及发生报警时主动发送S5F1。这种消息发送出去后理论上Host要回S5F2确认。如果长期收不到确认设备端是否重发GEM标准里有具体规则我们项目中简化成回调一个超时事件由上层业务决定是否重发。EAP Host端则要主动发起TCP连接连接成功后发送Select Request建立会话随后发送S1F13与设备确认通信状态发送S2F17设置时间发送S1F15/S1F16请求设备列表发送S2F33读取变量通过S2F41/S2F42管理事件报表。Host端要有一个消息分发器收到设备端主动上报的消息后路由到事件处理器例如收到S6F11就解析事件ID并触发业务回调。还有一点容易被忽略Host端同时管理多台设备时每条连接都要单独维护会话状态。这个项目的Host端虽然只连一台设备但代码里把设备连接抽象成了DeviceSession类后续扩展多设备时只需要维护一个DeviceSession列表即可不需要大改核心逻辑。3. Equip设备端和EAP Host端的具体实现3.1 开发环境与项目结构开发环境方面我用的是.NET 8.0IDE用Visual Studio 2022。项目结构分两个独立工程一个叫EquipServer一个叫EapHost另外还有一个类库SecsGem.Core用于共享协议编解码、HSMS消息模型、日志工具。这样拆的好处很明显协议层代码只需写一次设备端和Host端都引用同一个类库避免两端解析逻辑不一致。SecsGem.sln ├── SecsGem.Core │ ├── Hsms │ │ ├── HsmsMessage.cs │ │ ├── HsmsHeader.cs │ │ └── HsmsConnection.cs │ ├── Secs2 │ │ ├── SecsItem.cs │ │ ├── Secs2Encoder.cs │ │ └── Secs2Decoder.cs │ └── Common │ └── ByteHelper.cs ├── EquipServer │ ├── Program.cs │ └── DeviceService.cs └── EapHost ├── Program.cs └── HostService.cs这个项目结构参考了常见的服务端与客户端分离思路如果只是临时调试也可以把两端放在同一个控制台程序里用不同线程模拟但可读性会差很多。特别是后续要添加GEM功能、数据库记录、界面展示拆开工程会轻松得多。3.2 设备端程序从收连接到上报事件设备端第一步是启动TcpListener监听9529端口。端口不是协议强制的但我习惯用9529因为SECS/GEM默认端口一般就是5000/9529很多设备厂商使用9529。监听成功后进入AcceptTcpClientAsync循环每个客户端连接使用独立Task处理保证并发安全。private readonly TcpListener _listener; private readonly ConcurrentDictionaryint, HsmsConnection _sessions new(); public async Task StartAsync(int port) { _listener new TcpListener(IPAddress.Any, port); _listener.Start(); _logger.LogInformation(EquipServer listening on port {Port}, port); while (true) { TcpClient client await _listener.AcceptTcpClientAsync(); _ Task.Run(() HandleClientAsync(client)); } }对于每个新连接要经历一次Select会话建立过程。整个流程是循环读取HSMS消息头判断SType值。如果是SType1回复SType2 Select Response并将会话状态置为Selected如果是SType0说明是数据消息进入SECS-II消息分发器如果是SType5回复Linktest Response如果是SType9则关闭连接并清理会话。private async Task HandleClientAsync(TcpClient client) { using var stream client.GetStream(); while (client.Connected) { HsmsMessage msg await HsmsReader.ReadMessageAsync(stream); switch (msg.SType) { case 0: // Data Message await ProcessSecsMessageAsync(msg); break; case 1: // Select Request await SendControlMessageAsync(stream, 2, msg); break; case 5: // Linktest Request await SendControlMessageAsync(stream, 6, msg); break; case 9: // Separate return; } } }数据消息分发要按Stream和Function来区分。我封装了一个字典key为(SFN, SFN的组合)value是处理方法。例如收到(S1,F13)就调用HandleS1F13组装S1F14回复。回复时必须把请求消息的SystemBytes原样返回这样Host才能关联哪条回复对应哪个请求这是整个协议最基础的对应关系。S1F13回复内容按照GEM要求必须包含MDLN设备型号、SOFTREV软件版本。代码中我定义了一个DeviceInfo类用来表示设备ID、型号、软件版本。这些信息在S1F14中按设备信息列表格式返回如果你省略了某个必填项Host端解析S1F14就可能失败这也是联调时经常遇到的问题。事件上报相对独立。当设备内部EVENT发生变化时例如一个批次完成业务代码调用EventReporter上报S6F11。S6F11的Body结构是List包含的是DataID、EventID然后是多个Report集合。这里的EventID必须是Host之前通过S2F41/S2F42配置过的否则设备上报的CEID Host端没有登记会被当作未知事件丢弃。3.3 主机端程序连接管理、参数下发与报表收集Host端的实现入口是主动连接设备IP和端口。连接建立成功后不能立刻发数据消息先发起Select Request等待Select Response。这个步骤成功之后才代表HSMS会话建立。随后项目里做了三件核心事情发送S1F13建立通信发送S2F17同步时间发送S1F15/S1F16查询设备列表。public async Task ConnectAndEstablishAsync(string ip, int port) { await _tcp.ConnectAsync(ip, port); _stream _tcp.GetStream(); await SendHsmsControl(1); // Select Request HsmsMessage response await ReceiveAsync(); if (response.SType ! 2) throw new Exception(Select Response failed); var s1f13Reply await SendAndReceiveAsync( BuildS1F13Request() ); // 解析S1F14取出MDLN和SOFTREV }这里SendAndReceiveAsync必须做一个请求消息的字典登记等接收线程拿到消息后通过SystemBytes找到对应的TaskCompletionSource并返回结果。这是实现异步请求应答模型最清爽的方式。我第一次实现时没有这么做而是用一个全局变量存最近一次SystemBytes结果并发请求一多立刻乱套。后来改成ConcurrentDictionaryint, TaskCompletionSource 才解决。Host端接收线程是独立的它负责解析所有收到的HSMS消息。如果是Primary消息比如设备主动上报的S6F11、S5F1就直接进入事件回调如果是Secondary消息则说明是对之前Primary请求的回复通过SystemBytes匹配到对应的TaskCompletionSource把结果交还给那个等待方。我在项目里用一个简单的回调事件让上层业务感知设备状态变化。例如DeviceOnline、DeviceEventReceived、DeviceAlarmReceived。这样把通信层和业务层解耦后续做WPF界面或者接数据库记录都只需要挂新事件即可不需要改动通信逻辑。再多提一句配置管理。EAP Host连接哪台设备、端口多少、心跳间隔多久、T6超时多少我全放进appsettings.json。千万不要硬编码在代码里否则现场联调时换台设备就要改代码重新编译太痛苦了。设备端也要支持配置DeviceID、MDLN、SOFTREV最好做成启动参数或者配置文件读取。4. 联调实录现象、排查与规避技巧4.1 一次S1F13超时的定位过程联调第一天两边程序都能启动TCP也连上了但Host发送S1F13后迟迟等不到S1F14。抓包看到设备端其实已经回了包但Host端一直抛超时。最开始怀疑是防火墙问题后来看连接状态完全正常。于是我在解析消息处加了日志把收到的HSMS消息前10字节全部打印成Hex对比发现设备端回复的SystemBytes竟然不是Host请求里带的那个数。问题出在设备端构造Secondary消息时直接new了一个新的SystemBytes而不是拷贝请求中的值。这个错误在代码走查时很难发现因为两边SystemBytes刚好都是4字节整数打印出来看着都类似只有精确比对才知道不一致。修复方法很简单所有回复消息的SystemBytes直接从原请求对象赋值。但为了以后防止再犯我在SendAndReceiveAsync里加了一个校验如果收到的系统字节与期望值不一致记录一条Error日志并丢弃这条回复。这类问题的定位思路值得记录一下先确认TCP链路通不通再确认HSMS控制消息是否正常然后确认SECS-II消息体能不能正确解码最后才去排查业务逻辑。一层一层剥洋葱不然直接看Application层日志可能会被误导。4.2 常见问题速查表下面这张表是我在这个项目里实际遇到以及行业内朋友经常碰到的问题整理成速查表按现象分类列出。排查的时候可以先对号入座。故障现象可能原因解决方案TCP能连上但Select Request后无响应设备端没有处理SType1控制消息检查控制消息分支必须在数据消息之前处理SelectS1F13回复超时SystemBytes未原样返回回复消息必须携带请求消息的SystemBytes收到S1F14但解析失败List节点元素个数与实际数量不符用解码器的Debug校验函数检查Item树设备上报S6F11Host端不触发事件EventID未在Host端注册检查S2F33/S2F35配置流程以及事件报表定义通信一段时间后自动断开T8超时即数据间隔超过设备容忍范围加大T8存活时间或定期互相发送Linktest消息能收能发但设备显示数据乱码字符串编码不一致统一使用ASCII编码禁止混用UTF8多设备并发时回复串线用单一变量存储请求ID未按连接隔离使用ConcurrentDictionary按连接或Session维护任务字典消息发送延迟高没有做消息队列频繁创建Socket发送复用连接用Channel处理消息优先级其中T8超时这个问题在仿真环境中最容易忽略调试时由于停断点多消息间隔时常超过设备容忍时间。最直观的表现就是程序停在断点继续运行时连接已经被设备断开。这个不是代码Bug但会让你误以为通信逻辑有问题建议在调试模式下把设备端的T8设长一些比如60秒。4.3 几个新手最容易忽略的实现细节第一个是字节读取的完整性。TCP是流式传输一次Read调用不一定能拿到完整消息。比如你读取10字节Header可能只读到了4字节剩下的6字节要等下一次Read。代码里必须设置累计读取逻辑直到填满Header长度再根据长度字段读取Data部分。我封装了ReadExactlyAsync方法来循环读取固定字节数避免因为拆包导致消息解析错乱。第二个是日志的等级设计。Debug级记录每个收发消息的Hex和解析后的Item树Info级记录会话建立、断开、关键流程Error级记录异常堆栈。这个设计在联调时价值巨大。当别人拿着抓包工具问你“为什么我的消息没收到”你直接打开Debug日志就能看到是在等哪个数据对比一下就知道是对方没发还是自己解析错了。第三个是状态机的实现。设备端至少要有Disconnected、Connected、Selected、Communicating四种状态。Host下发消息前判断当前是否处于Communicating状态如果不是就报错提示“设备未就绪”。不要把所有状态都揉到if else里最好是定义状态枚举再通过状态转换方法统一控制。这套逻辑如果前期不做好联调时各种脏数据会让人崩溃。还有一个容易漏的是消息优先级。设备端可能同时要回S1F14的Secondary消息又要主动上报S6F11事件。如果发送时不做队列两个线程同时往同一个Socket写数据会造成消息交错接收方解析出来就是一坨乱码。我实现了一个简单的Channel 作为发送队列单线程消费按FIFO顺序发送从根上避免并发写问题。GEM标准里还有定时器处理、设备常量、数据变量、配方管理等功能这些在这个项目中有些是简化实现的有些是预留了接口。如果你要严格过GEM认证不能这么简化但作为一套可跑通的参考框架目前的设计足以支持实际项目的二次开发。最后再聊一点项目经验这套代码写下来前后花了两周多期间推翻过一次整体设计。第一次图省事设备端和Host端各写各的解析器结果联调时同一个消息两边的解析结果不一样查了半天发现是长度字段的读取方式不同。后来把编解码逻辑统一放到SecsGem.Core里这个问题才彻底解决。经验就是协议解析这类通用逻辑永远只写一遍用共享库引用。如果你准备基于这套代码做自己的项目我的建议是先跑通Demo再逐步加功能。第一件事是让设备端和Host端在自己电脑上完成一次S1F13通信确认消息能通然后再去接真实的模拟器或者设备。另外调试时不要直接用生产参数先把T6、T8调大一些等到链路稳定了再收敛到标准值这样能避免很多因网络波动造成的假故障。最后补充一个小技巧联调时最好用一段独立脚本模拟对端只发送固定字节的消息这样可以快速验证本端的解析逻辑。我调试的时候就是用Python写了个伪设备每次触发S6F11上报时打印并保存报文再拿这段报文去验证Host端的解析结果。这种“报文回放”的方式比每次人工构造消息高效得多。本文还有配套的精品资源点击获取
返回列表