C#上位机与MCGS触摸屏TCP通信实战指南

发布时间:2026/9/8 13:11:06
C#上位机与MCGS触摸屏TCP通信实战指南 简介面向工业自动化与上位机开发人员的C#与MCGS昆仑通态通信范例代码解决在Visual Studio 2013环境下通过TCP协议读取MCGS触摸屏或组态软件实时数据的需求。适合具备一定C#基础、正在做设备数据采集或人机界面联调的工程师参考。压缩包共36个文件核心为9个C#源码文件.cs配合窗体布局资源、项目配置文件及可执行程序清晰展示从连接建立到数据读取的完整流程另有少量缓存与调试文件。包体仅92KB轻量易用。已有2498人学习下载受到同类项目开发者关注。通过源码可快速掌握TCP客户端编写、报文解析、与MCGS变量交互的编程思路并能基于示例工程直接修改扩展缩短项目开发周期。 做C#上位机这些年和组态屏打交道的次数两只手数不过来。MCGS昆仑通态在中小型自动化项目里出现频率相当高很多设备厂用它做人机界面上位机再通过TCP把数据接回来做MES对接或者数据追溯。这套组合看似简单真上手写通信的时候坑不少——协议文档偏老派、网上Demo要么太玩具要么抄来抄去直接拿到现场根本跑不稳。这篇就把我实际用C#和MCGS走TCP通信的完整方案理一遍从组态屏端的参数配置到C#里面的通信类封装再到帧结构解析和常见问题一次性说清楚给要接MCGS的朋友做个参考。1. 先搞明白MCGS的TCP机制再动手写代码1.1 MCGS在TCP通信里到底扮演什么角色很多人第一次接MCGS第一反应是“它是不是跟西门子S7那样我拿着协议去读就行了”。实际上MCGS跟西门子、三菱这类PLC的通信模型差别很大。MCGS本身的组态软件支持多种设备驱动但它的网络通信默认是MCGS自己作为TCP客户端去连接远端服务器或者反过来作为TCP服务器等待别人连进来。两种模式在工程配置里都能设置。以我常用的方式为例让MCGS触摸屏作为TCP服务器上位机C#程序作为客户端主动连接。这样做有个实际好处上位机主动发起连接断线之后重连逻辑完全由我这边控制不用去触摸屏上按任何按钮现场维护的人基本不用学操作。如果反过来让MCGS当客户端去连上位机一旦上位机软件重启触摸屏侧就一直在“连接中”状态还得去组态里折腾麻烦得多。MCGS的TCP通信底层其实封装了它自己的协议对外通过“设备窗口”里的“网络TCP/IP父设备”来管理。具体到工程配置在MCGS组态环境的设备窗口里添加一个“网络TCP/IP父设备”然后在这个父设备下面挂“通用TCP/IP子设备”或者直接挂你用的PLC驱动子设备。触摸屏的IP地址和端口号就在父设备属性里设置。默认端口一般用5555但实际项目里我建议改成不常用的端口避免现场多台设备冲突。这里有一个关键认知C#和MCGS通信本质上不是直接去读触摸屏的屏幕变量而是去读写MCGS里连接的“子设备”所对应的数据对象。所以你在MCGS里操作的那些组态变量比如“温度1”“电机速度”如果想让上位机能访问到必须保证这些变量在MCGS实时数据库里的名称和类型是明确的。这一点在后面讲协议帧的时候会体现得很清楚。1.2 直接变量表绕开繁琐协议的关键设计MCGS的TCP协议里读写数据的核心是“设备只读变量”和“设备只写变量”或者说“直接变量表”。这个设计思路跟OPC DA很像——服务器暴露一个扁平化的变量集合客户端按变量名去读或者写。实际配置时在MCGS的实时数据库里建立变量比如Temp_1数值型用于存放1号温区的温度Motor_Speed数值型电机转速Alarm_Flag开关型报警标志然后在上位机这边所有通信动作都围绕这些变量名展开。C#程序发一个“读变量”的请求帧MCGS收到后把Temp_1的当前值返回发一个“写变量”的请求帧MCGS把值写进对应的数据对象进而驱动画面上的控件变化或者下发到PLC。这种模式比直接映射Modbus寄存器地址更灵活但代价是字节序、数据类型长度这些细节很容易出错。后面章节我给出具体代码和帧格式照着写就能通。2. 通信前的两手准备MCGS工程配置与C#开发环境2.1 MCGS触摸屏端启用TCP服务在写第一行C#代码之前先把MCGS侧的配置搞定。以我常用的MCGS嵌入版Tpc嵌入式一体化触摸屏为例步骤是这样的在MCGS组态环境中进入“设备窗口”在右侧工具箱里找到“网络TCP/IP父设备”双击添加到设备窗口。双击该设备在弹出的属性设置中本地IP地址设为触摸屏的IP比如192.168.1.100本地端口号填一个自己定义的端口建议用四位数以上比如5000避免跟常用服务冲突。工作方式选“TCP服务”或者“TCP Server”有些版本里叫“被动模式”。在父设备下面添加子设备比如“通用TCP/IP子设备”或“ModbusTCP子设备”等这个取决于你MCGS下面挂的PLC类型。上位机通信时访问的是这个子设备的数据对象。编译工程下载到触摸屏。这里有一个容易忽略的点MCGS的TCP服务是否连上在触摸屏上不一定有明显提示。建议在组态画面的某个角落做一个指示灯控件关联一个“TCP连接状态”变量有些版本提供的系统变量方便现场排查。配置完触摸屏后先用一个小工具比如TCP调试助手测试一下端口是否通能连上再继续。不要一上来就调C#代码基础链路都不通后面全是白费工夫。2.2 C#侧工程搭建与基础Socket骨架C#侧我用的是.NET Framework 4.7.2WinForms工程。网络层直接用System.Net.Sockets不需要引入第三方库。MCGS的协议简单到完全不需要插件或组件自己写Socket收发就行。新建工程后规划一下类结构McgsTcpClient负责连接、断开、收发数据的核心类McgsFrame负责组帧和解帧的协议类MainForm界面逻辑定时刷新数据为什么拆成三个因为通信逻辑和协议解析是两回事。Socket只负责把字节流可靠地发出去和收回来组帧解帧是纯算法问题拆开之后哪块出问题都好排查。这也是我写上位机通信一贯的思路通信层和业务层必须解耦否则后期加需求会想骂人。3. C#通信核心类从连接到稳定双向通信3.1 完整通信类代码与注释下面给出通信类的核心代码。这个类不是我随手写的玩具是经过现场蹂躏过的版本。逻辑很简单但边界情况都处理了。using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; public class McgsTcpClient : IDisposable { private TcpClient _tcpClient; private NetworkStream _stream; private readonly object _lockObj new object(); private CancellationTokenSource _cts; private Task _receiveTask; /// summary当前连接状态/summary public bool IsConnected _tcpClient ! null _tcpClient.Connected; /// summary连接MCGS服务器/summary public async Taskbool ConnectAsync(string ip, int port, int timeoutMs 3000) { try { Disconnect(); _tcpClient new TcpClient(); // 异步连接加超时控制避免界面卡死 var connectTask _tcpClient.ConnectAsync(IPAddress.Parse(ip), port); if (await Task.WhenAny(connectTask, Task.Delay(timeoutMs)) ! connectTask) { throw new TimeoutException(连接超时); } _stream _tcpClient.GetStream(); _stream.ReadTimeout 5000; // 读超时5秒 _stream.WriteTimeout 5000; // 写超时5秒 _cts new CancellationTokenSource(); _receiveTask Task.Run(() ReceiveLoop(_cts.Token)); return true; } catch (Exception ex) { Console.WriteLine($[McgsTcpClient] 连接失败: {ex.Message}); Disconnect(); return false; } } /// summary断开连接/summary public void Disconnect() { try { _cts?.Cancel(); _stream?.Close(); _tcpClient?.Close(); } catch { /* 关闭时异常忽略 */ } finally { _stream null; _tcpClient null; } } /// summary发送原始字节/summary public async Task SendAsync(byte[] data) { if (!IsConnected || _stream null) throw new InvalidOperationException(未连接到MCGS服务器); // 加锁防止多个线程同时写导致数据交错 lock (_lockObj) { _stream.Write(data, 0, data.Length); _stream.Flush(); } await Task.CompletedTask; } /// summary接收循环持续读取MCGS返回的数据/summary private async Task ReceiveLoop(CancellationToken token) { byte[] buffer new byte[4096]; while (!token.IsCancellationRequested) { try { int bytesRead await _stream.ReadAsync(buffer, 0, buffer.Length, token); if (bytesRead 0) { // 对方关闭连接 Console.WriteLine([McgsTcpClient] 连接被远端关闭); break; } // 这里拿到原始字节流交给上层协议解析 OnDataReceived?.Invoke(buffer, bytesRead); } catch (OperationCanceledException) { break; } catch (Exception ex) { Console.WriteLine($[McgsTcpClient] 接收异常: {ex.Message}); break; } } // 循环退出说明连接不可用触发断开事件 OnDisconnected?.Invoke(); } public event Actionbyte[], int OnDataReceived; public event Action OnDisconnected; public void Dispose() { Disconnect(); _cts?.Dispose(); } }这段代码有几点我想专门说一下连接超时控制。ConnectAsync搭配Task.WhenAny看起来多花了三行代码但实际体验天差地别。没有超时控制的话当现场IP配错或者网线掉了ConnectAsync可能卡很久界面假死用户会直接砸键盘。收发分离。接收数据放在独立线程循环里跑发送和接收不互相阻塞。这个设计在工业通信里几乎是个约定俗成因为很多设备会不定时主动上报数据你不能等到自己发请求才去读。_lockObj锁。发送时加锁是为了防止多个业务线程同时写NetworkStream导致字节交错。比如界面线程在写“读温度”的同时后台线程在写“写参数”不加锁的话两条命令的字节会混在一起MCGS那边解析直接乱套。3.2 收发线程、心跳与断线重连的设计逻辑单独一个连接类还不够实际跑现场必须有心跳机制和自动重连否则半夜设备断电、网线被老鼠咬断第二天早上过来一看上位机还是“连接已断开”的状态那就要被现场工程师骂了。心跳逻辑我一般这样设计每隔5秒发一个特定的“读变量”请求帧下一篇会讲帧结构只要能收到MCGS的响应就认为链路正常连续3次没有响应判定连接失效进入重连流程。C#里用一个System.Windows.Forms.Timer或者System.Threading.Timer触发心跳具体代码private int _missedHeartbeatCount 0; private const int MaxMissedHeartbeat 3; private void HeartbeatTimer_Tick(object sender, EventArgs e) { if (!_tcpClient.IsConnected) { TryReconnect(); return; } // 发送心跳请求读取一个固定变量 var heartbeatFrame McgsFrame.BuildReadFrame(Heartbeat_Var, McgsDataType.Int16); _tcpClient.SendAsync(heartbeatFrame); _missedHeartbeatCount; if (_missedHeartbeatCount MaxMissedHeartbeat) { Console.WriteLine([Heartbeat] 连续未收到响应判定断线); _tcpClient.Disconnect(); } } private void TryReconnect() { // 记录当前时间避免太频繁的重连请求打爆日志 if (DateTime.Now - _lastReconnectTime TimeSpan.FromSeconds(3)) return; _lastReconnectTime DateTime.Now; bool ok _tcpClient.ConnectAsync(_config.Ip, _config.Port).Result; if (ok) { _missedHeartbeatCount 0; Console.WriteLine([Heartbeat] 重连成功); } }这里有个坑MCGS触摸屏作为服务端时对长时间不通信的客户端连接可能会主动断开不同版本的闲置超时时间不一样有的可能是30秒有的更长。所以心跳间隔不宜太长5秒到10秒是比较稳妥的选择。另外重连逻辑里做了时间间隔限制避免网络抖动时疯狂重连导致CPU占用飙升。4. 读写变量帧结构、字节序与字符串编码4.1 设备属性里的变量名与类型映射MCGS的TCP协议帧核心就是“读/写某个变量”。搞清楚它的数据格式通信就完成了一大半。先看类型映射。MCGS里的变量类型跟C#类型不是一一对应需要做一个映射表MCGS变量类型C#对应类型字节长度备注开关型Bitbool1值为0或1数值型32位浮点float4按IEEE 754存储数值型16位整数short2有些版本叫“整数”数值型32位整数int4较少见字符型stringbyte[]不定长编码默认ASCII/GBK这个映射关系建议在程序里做成一个枚举或者配置表方便后续扩展。我看到过不少项目把变量类型写死在代码里一旦MCGS侧改了变量类型上位机就出乱码排查半天找不到原因。4.2 命令帧格式以太网帧 命令头 数据MCGS的TCP帧大体上可以分成三层以太网帧头、命令区、数据区。不同版本协议略有差异以下是我在Tpc嵌入版上验证过的格式照抄基本能通。通用请求帧结构如下长度单位是字节偏移长度说明示例值02帧头标志固定为0x11 0x1711 1721功能码0x01读0x02写0132数据区长度从偏移5开始算0x00 0x065可变数据区内容见下文这只是基础结构实际MCGS不同的固件版本帧头可能是0x11 0x17也可能是别的比如0x11 0x01。拿到设备后先用调试助手抓一下包确定帧头再写死。别一上来就抄网上别人写的帧头版本对不上你会怀疑人生。读变量的数据区一般包含变量名长度1字节或2字节变量名字节ASCII编码读取长度/数据类型1字节写变量的数据区则还需要额外加上要写入的值按类型转换好的字节数组4.3 数值怎么解析、字符串怎么不乱码关于数值解析我做几个关键提醒字节序要确认。MCGS走TCP时多字节数值的字节序大小端不同版本有差异。比如写一个16位整数1000x0064有的版本发00 64大端有的发64 00小端。用调试助手先发一次写指令观察触摸屏上变量值是否对得上不对就交换字节序。不要凭直觉假设是大端我在现场吃过一次亏写进去20000结果显示是负数排查了半天才发现是字节序反了。浮点数解析。MCGS的浮点默认是IEEE 754单精度C#里用BitConverter.ToSingle就能解析。注意有些老设备的固件浮点字节序是反的需要先反转字节再转换float ParseMcgsFloat(byte[] data, int offset) { // 如果MCGS是小端浮点而C#的BitConverter在小端机器上直接读不需要反转 // 如果MCGS是大端浮点则需要反转 byte[] reversed new byte[4]; reversed[0] data[offset 3]; reversed[1] data[offset 2]; reversed[2] data[offset 1]; reversed[3] data[offset]; return BitConverter.ToSingle(reversed, 0); }字符串乱码问题。MCGS的字符型变量在TCP传输时默认编码一般是ASCII或者GBK。如果上位机用UTF-8去解码中文就会变成“锟斤拷”这类经典乱码。正确的解码方式是string DecodeMcgsString(byte[] data, int offset, int length) { // MCGS中文环境多用GBK编码 return Encoding.GetEncoding(GBK).GetString(data, offset, length); }如果是英文/数字用ASCII就行。如果项目里有中文变量名注意变量名本身在请求帧里也要用GBK编码不能拍脑袋用UTF-8否则MCGS匹配不到变量名会一直报“变量不存在”的错误。5. 现场最容易翻车的几个地方5.1 乱码、字节序、粘包与半包粘包与半包是Socket开发里最经典的问题MCGS的TCP也不例外。所谓粘包就是MCGS连续发送多个响应帧时TCP层可能把他们合并成一个包推给上位机半包则相反一个完整的响应帧可能被拆成两半分两次送到。C#里如果在ReceiveLoop里直接对buffer做解析不做帧边界处理数据量一多就会出现解析错位读出来的变量值全是乱的。解决思路是维护一个接收缓冲区把每次收到的字节追加进去然后循环尝试解析出完整帧解析出一条就从缓冲区里移除一条。伪代码如下private readonly Listbyte _recvBuffer new Listbyte(); private void HandleReceivedData(byte[] data, int length) { _recvBuffer.AddRange(data.Take(length)); while (true) { int frameLength TryParseFrameLength(_recvBuffer); if (frameLength 0 || _recvBuffer.Count frameLength) break; // 数据还不够等下一个包 // 取出一条完整帧 byte[] frame _recvBuffer.Take(frameLength).ToArray(); _recvBuffer.RemoveRange(0, frameLength); ProcessFrame(frame); // 交给业务解析 } }这个循环看起来简单但它是整个通信稳定性的基石。没有这一步一旦数据量上来就会出莫名其妙的问题。另外记得在ReceiveLoop之外加一个“若缓冲区持续不完整超过N毫秒则清空”的保护逻辑防止异常数据导致缓冲区永久卡死。5.2 MCGS变量名不匹配导致的静默失败另一个常见坑是请求帧的变量名和MCGS里实际的变量名不一致时MCGS不会返回错误帧而是直接不给响应或者返回一个空数据。这就导致上位机那边超时等待用户看到的就是“数据刷不出来”。排查方式很简单先用TCP调试助手手动发一帧读命令把MCGS里确定存在的变量名比如一个自己新建的测试变量发过去看有没有响应。如果有响应那说明代码组帧没问题是实际业务变量名写错了如果没响应检查帧头和功能码。5.3 MCGS侧脚本与触摸屏性能的相互影响还有一个容易被忽视的问题MCGS触摸屏本身性能不强如果你在上位机里用100ms的定时器去狂读几百个变量触摸屏的响应速度会肉眼可见地变卡画面切换迟钝甚至触摸都没反应。我实际项目里一般把刷新周期控制在500ms到1s并且按变量分组并行读取而不是一次性把所有变量塞进一帧。比如温度类变量一组状态类变量一组分开请求每组之间加一个小间隔减轻屏端压力。6. 稳定运行经验怎么做到几个月不重启6.1 心跳、看门狗和数据刷新频率的平衡上位机软件一旦交付到现场不可能天天派人盯着。做到几个月不重启需要提前做几件事。第一件是心跳看门狗。前面讲的_missedHeartbeatCount逻辑就是看门狗连续几次心跳无响应后不要傻等直接断开重连。有些现场电磁环境差偶尔丢一两个包是正常的但连续丢包就需要干预。第二件是分级数据刷新。把变量分成“实时性要求高”和“实时性要求低”两类实时性要求高的比如报警信号、急停状态500ms刷新一次实时性要求低的比如温度曲线数据、累计产量2~5秒刷新一次这样既保证了关键数据的及时性又不会给触摸屏造成太大压力。6.2 日志记录与异常自愈通信程序最怕出了问题无法复现。我会在关键节点写上日志包括连接成功、发送了哪些帧、收到了哪些帧、解析出了哪些变量、发生了重连、重连成功。日志轮转按天切割保留最近30天方便出问题时回溯。另外做一个自愈机制当通信线程异常退出后定时器检测到连接断开自动重连重连成功后再做一次数据全量刷新把界面上所有变量拉一遍这样即使中间断了几秒恢复后数据也是准的不需要人工介入。6.3 多台MCGS设备的协调有些产线不止一台触摸屏上位机要同时连多台MCGS。最简单的做法是一台设备一个McgsTcpClient实例每个实例有自己的心跳和重连逻辑互不干扰。如果设备数量特别多比如10台以上建议用一个ConcurrentDictionarystring, McgsTcpClient来管理key用设备编号比如“Line1_Panel”界面层只跟这个字典交互。不要写一个全局客户端然后到处传引用后期改起来会想哭。轮询策略上多设备可以并行轮询但是要注意每个设备请求之间的间隔不要小于100ms否则触摸屏的网络栈处理不过来。实测下来200ms间隔是最稳妥的。7. 我踩过的坑和一些小建议最后聊几个实操细节。首先连接MCGS之前一定先ping通。听起来是废话但真有不少同事拿着笔记本到现场不开网卡本地连接就跑去调程序折腾一小时发现物理链路都不通。其次MCGS的固件版本影响协议帧头务必以实际设备为准调试助手的抓包是排障第一手段。然后是界面刷新。C#上位机里UI刷新千万不要在接收线程里直接操作控件用Invoke或者BeginInvoke回到界面线程或者用定时器去读取解析好的数据缓存避免跨线程异常。数据缓存推荐用ConcurrentDictionary或者加锁的Dictionary存最新的变量值UI定时器只管读缓存跟通信线程完全隔离。再有一个容易被忽略的防火墙。上位机装了安全软件或者系统防火墙要确保TCP端口对MCGS的IP放行否则客户端明明连得上因为很多防火墙只拦入站不拦出站但MCGS那边收不到数据或者回包被拦现象就是“连接成功但数据一直超时”。这类问题非常隐蔽排查的时候优先想到。如果你打算长期维护这套通信建议把帧的组包和解析单独封装成一个类写单元测试把帧结构测试case覆盖住比如不同变量名长度、不同数据类型、粘包半包模拟、超长数据截断等。上现场之前先把测试跑通能省掉很多现场Debug的时间。还有一个进阶玩法MCGS的SDK在某些版本里提供了.Net接口调用如果你对接的版本支持可以考虑直接用SDK代替裸写Socket。但就我接触的场景来看裸写Socket控制力更强依赖更少也更利于排查问题。SDK用起来虽然省事但厂家给出的官方库往往更新慢API设计也偏老遇到问题很难深入诊断。通信这块的代码做到“连得上、读得准、断得稳、恢复快”基本就合格了。剩下的事情就是根据现场的具体设备型号和工艺要求微调协议细节和刷新策略。希望这篇能帮你少走点弯路也算是我这些年跟MCGS纠缠下来的一点回报。本文还有配套的精品资源点击获取