简介:面向工业自动化与上位机开发人员的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”,有些版本里叫“被动模式”。
- 本地IP地址设为触摸屏的IP,比如
- 在父设备下面添加子设备,比如“通用TCP/IP子设备”或“ModbusTCP子设备”等,这个取决于你MCGS下面挂的PLC类型。上位机通信时访问的是这个子设备的数据对象。
- 编译工程,下载到触摸屏。
这里有一个容易忽略的点:MCGS的TCP服务是否连上,在触摸屏上不一定有明显提示。建议在组态画面的某个角落做一个指示灯控件,关联一个“TCP连接状态”变量(有些版本提供的系统变量),方便现场排查。
配置完触摸屏后,先用一个小工具(比如TCP调试助手)测试一下端口是否通,能连上再继续。不要一上来就调C#代码,基础链路都不通,后面全是白费工夫。
2.2 C#侧工程搭建与基础Socket骨架
C#侧我用的是.NET Framework 4.7.2,WinForms工程。网络层直接用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 Task<bool> 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 Action<byte[], 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#对应类型 | 字节长度 | 备注 |
|---|---|---|---|
| 开关型(Bit) | bool | 1 | 值为0或1 |
| 数值型(32位浮点) | float | 4 | 按IEEE 754存储 |
| 数值型(16位整数) | short | 2 | 有些版本叫“整数” |
| 数值型(32位整数) | int | 4 | 较少见 |
| 字符型 | string(byte[]) | 不定长 | 编码默认ASCII/GBK |
这个映射关系建议在程序里做成一个枚举或者配置表,方便后续扩展。我看到过不少项目把变量类型写死在代码里,一旦MCGS侧改了变量类型,上位机就出乱码,排查半天找不到原因。
4.2 命令帧格式:以太网帧 + 命令头 + 数据
MCGS的TCP帧大体上可以分成三层:以太网帧头、命令区、数据区。不同版本协议略有差异,以下是我在Tpc嵌入版上验证过的格式,照抄基本能通。
通用请求帧结构如下(长度单位是字节):
| 偏移 | 长度 | 说明 | 示例值 |
|---|---|---|---|
| 0 | 2 | 帧头标志,固定为0x11 0x17 | 11 17 |
| 2 | 1 | 功能码:0x01读,0x02写 | 01 |
| 3 | 2 | 数据区长度(从偏移5开始算) | 0x00 0x06 |
| 5 | 可变 | 数据区内容 | 见下文 |
这只是基础结构,实际MCGS不同的固件版本,帧头可能是0x11 0x17,也可能是别的,比如0x11 0x01。拿到设备后先用调试助手抓一下包,确定帧头再写死。别一上来就抄网上别人写的帧头,版本对不上你会怀疑人生。
读变量的数据区一般包含:
- 变量名长度(1字节或2字节)
- 变量名字节(ASCII编码)
- 读取长度/数据类型(1字节)
写变量的数据区则还需要额外加上:
- 要写入的值(按类型转换好的字节数组)
4.3 数值怎么解析、字符串怎么不乱码
关于数值解析,我做几个关键提醒:
字节序要确认。MCGS走TCP时,多字节数值的字节序(大小端)不同版本有差异。比如写一个16位整数100(0x0064),有的版本发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 List<byte> _recvBuffer = new List<byte>(); 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台以上),建议用一个ConcurrentDictionary<string, 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纠缠下来的一点回报。
本文还有配套的精品资源,点击获取