C#上位机与三菱CNC通讯实战:MC协议解析与WinForm开发

发布时间:2026/9/2 4:47:27
C#上位机与三菱CNC通讯实战:MC协议解析与WinForm开发 简介面向C#开发人员与工业自动化工程师的WinForm三菱CNC通讯Demo源码演示如何利用EZNCAUTLib通讯库封装底层交互逻辑适合正在开发上位机监控、设备数据采集或数控机床联网功能的读者。代码内置MitCom通讯类包含通讯库变量、系统类型参数、消息输出及AlarmData事件定义可快速掌握三菱CNC以太网通讯的初始化、数据读写与告警回调流程类结构简单便于按需扩展。压缩包共145个文件以24个C#源文件、22个DLL运行库、11个Config配置文件、10个Resources资源文件及PDB调试符号为主整体仅771KB目录结构清晰可按模块查阅核心代码与依赖关系。已有1118人学习下载适合需要对接三菱CNC设备的C#开发者参考直接沿用封装思路替换业务逻辑即可缩短项目调试周期。 这阵子在工控群和博客后台经常能看到有人问C#怎么跟三菱CNC通讯有没有现成的DEMO源码可以参考正好我去年给车间做过一套WinForm上位机用来实时读取三菱M70/M80系统的坐标、当前程序号还能下发M指令。今天就把这套DEMO的核心思路、协议细节、代码结构和踩坑记录整理出来希望能给正在做C#上位机开发、尤其是第一次碰三菱CNC通讯的朋友一个可以复用的起点。这个东西说白了就是一句话上位机通过以太网用三菱的MC协议和CNC交换数据。开源的现成轮子不多官方手册又是几百页的PDF网上能找到的资料往往只讲PLC不讲CNC。所以我当时决定自己写一套轻量的通讯类配合WinForm界面把最常用的“读坐标、读程序号、写M码”打通。下面进入正题。1. DEMO定位这台CNC上位机到底解决什么问题1.1 三菱CNC通讯的底层逻辑三菱CNC以常见的M70/M80系列为例通常内置以太网口也可以选配以太网模块。它对外提供的通讯能力本质上就是走MC协议MELSEC Communication Protocol现在也叫SLMP的一种实现。MC协议是一套成熟的工业通讯协议支持串口、以太网、CC-Link等多种物理介质对C#开发来说以太网TCP是最省事的方式。你可以把这个过程想象成和一个只认固定格式电报的“老式电报员”对话你发一段特定格式的字节流过去对方解析后执行操作再把结果按固定格式回传给你。协议本身不复杂难点在于你发的每一帧必须严格符合规范错一个字节顺序、错一个长度字段结果就是超时或者乱码。这套DEMO解决的核心问题就是把这些“电报格式”封装成C#方法。你只需要调用ReadWords(地址, 数量)就能拿到CNC里指定软元件的数据调用WriteWords(地址, 数据)就能把数据写进去底层组帧、校验、解析全部封装好。1.2 为什么选C# WinForm我在工控现场做过不少方案选C#和WinForm不是偶然。C#上手快Socket、异步编程、UI控件的生态比C友好太多串口、TCP、Modbus这些工控常用的通讯库也都有成熟方案。WinForm虽然看起来有点“老”但在工业环境下恰恰是优点轻量、启动快、占用资源少车间工控机配置普遍不高开个WinForm程序几乎无压力。有人会问为什么不用WPFWPF的界面表现力确实更强但学习成本高MVVM那套模式在小工具项目里反而显得繁琐。我这边做的上位机大多是单机工具功能就是连设备、读数据、显示、下发指令WinForm拖几个控件就搞定了。如果你只是做内部工具而不是商用产品WinForm绝对是最快路径。1.3 DEMO整体功能与代码结构这个DEMO实现了以下功能功能说明连接/断开通过IP和端口连接三菱CNC的以太网模块读取坐标读取当前机床坐标X/Y/Z值并显示读取程序号获取当前正在运行的程序编号写入M指令向指定软元件写入M代码或D寄存器数据通讯状态监控显示连接状态、最后通讯时间、错误日志代码结构上分成三层。UI层是MainForm负责所有界面交互协议层是McProtocolClient负责MC协议组帧、解析、收发底层是TcpClient封装的网络连接。这样的分层让后面换协议、加功能都方便不是把所有代码堆在一个事件里。2. 协议细节读懂MC协议通讯就成功了一半2.1 3E帧结构拆解三菱MC协议以太网通讯有几种帧格式最常用的是“3E帧”全称是“QnA兼容3E帧”。它支持二进制码对C#来说解析最方便。一个完整的读写请求帧长这样字段长度字节说明示例值帧头4固定为D0 00 00 00D0 00 00 00请求目标模块IO编号2PLC CPU固定为FF 03低字节在前FF 03请求目标模块站号2通常为00 0000 00请求数据长2从“请求数据长”字段之后到帧尾的字节数0B 00监视定时器2超时时间单位ms10 27代表10000ms10 27命令2读软元件04 01写软元件14 01低字节在前01 04子命令2字单位01 00位单位00 0001 00数据体不定地址、点数、写入数据等视命令而定注意这里很多字段都是“低字节在前”也就是Intel小端序。比如命令0401实际发送是01 04。我第一次调这个协议就栽在这里按习惯先发高位结果CNC一直回错误码。2.2 读软元件命令怎么拼读操作是MC协议里最常用的。假设我要读取D0开始的10个字16位寄存器命令结构如下D0 00 00 00 FF 03 00 00 0B 00 10 27 01 04 01 00 00 00 A8 0A 00逐段解释0B 00是请求数据长从监视定时器开始到帧尾共11个字节。10 27监视定时器10000ms超时。01 04命令0401表示读软元件。01 00子命令按字单位读取。00 00 A8是起始软元件编号。A8是D寄存器的软元件代码后面的00 00是地址偏移合起来表示D0。0A 00是读取点数十进制10。软元件代码表也是必备的。常用的有D数据寄存器是A8R文件寄存器是AFM中间继电器是90X输入是9CY输出是9D。三菱CNC里坐标、程序号、报警号这些信息通常都映射到特定的软元件或者系统变量区具体地址要查对应CNC型号的手册附录。2.3 写软元件与响应解析写操作和读操作结构类似区别在命令字和尾部数据。写入D0开始的2个字值分别为100和200帧格式是D0 00 00 00 FF 03 00 00 0F 00 10 27 01 14 01 00 00 00 A8 02 00 64 00 C8 00注意命令变成了01 14即1401写命令数据长变成15字节帧尾多了4个字节的写入数据64 00等于十进制100C8 00等于十进制200。响应帧解析比较简单。正常响应前8个字节和请求帧一样然后是响应数据长、结束代码和数据区。结束代码00 00表示成功其他值代表各种异常常见的有5F 01命令错误、5F 02软元件地址超范围等。收到响应后先判断结束代码再解析数据区才有意义。3. 代码实现从Socket到界面的一条龙3.1 通讯核心类McProtocolClient我把通讯逻辑封装成一个独立的类放在McProtocolClient.cs里。核心方法是连接、读、写三个。连接方法用TcpClient实现加了一个锁对象防止多线程同时收发导致数据交叉public class McProtocolClient { private TcpClient _client; private NetworkStream _stream; private readonly object _lock new object(); public bool Connect(string ip, int port) { try { _client new TcpClient(); _client.Connect(ip, port); _stream _client.GetStream(); return true; } catch (Exception ex) { MessageBox.Show(连接失败 ex.Message); return false; } } public void Disconnect() { _stream?.Close(); _client?.Close(); } }组帧我单独抽了方法避免读和写各写一遍逻辑。读取D寄存器的组帧代码如下private byte[] CreateReadFrame(int address, int count) { Listbyte frame new Listbyte(); frame.AddRange(new byte[] { 0xD0, 0x00, 0x00, 0x00 }); // 帧头 frame.AddRange(new byte[] { 0xFF, 0x03 }); // IO编号 frame.AddRange(new byte[] { 0x00, 0x00 }); // 站号 frame.AddRange(new byte[] { 0x00, 0x00 }); // 请求数据长先占位 frame.AddRange(new byte[] { 0x10, 0x27 }); // 监视定时器 frame.AddRange(new byte[] { 0x01, 0x04 }); // 命令 0401 frame.AddRange(new byte[] { 0x01, 0x00 }); // 子命令 字单位 frame.Add((byte)(address 0xFF)); // 地址低位 frame.Add((byte)((address 8) 0xFF)); // 地址高位 frame.Add(0xA8); // D寄存器代码 frame.AddRange(BitConverter.GetBytes((short)count)); // 点数 int dataLength frame.Count - 10; frame[8] (byte)(dataLength 0xFF); frame[9] (byte)((dataLength 8) 0xFF); return frame.ToArray(); }数据长为什么是frame.Count - 10因为请求数据长字段从监视定时器开始统计而当前索引8、9正好是数据长本身数据长之后的内容从索引10开始。这个计算我第一次也搞反了导致发的帧长度不对CNC直接不回包。发送和接收响应我放在同一个方法里用锁保证原子性public byte[] ReadWords(int address, int count) { lock (_lock) { byte[] frame CreateReadFrame(address, count); _stream.Write(frame, 0, frame.Length); byte[] buffer new byte[1024]; int n _stream.Read(buffer, 0, buffer.Length); byte[] response new byte[n]; Array.Copy(buffer, response, n); int endCode response[9] 8 | response[8]; if (endCode ! 0) throw new Exception(MC协议异常结束代码: 0x endCode.ToString(X4)); byte[] data new byte[n - 10]; Array.Copy(response, 10, data, 0, data.Length); return data; } }3.2 WinForm界面与后台线程这里要重点说一个新手常踩的坑不要在UI线程里直接做Socket通讯否则网络卡一下界面就假死给你看。我的做法是启动一个后台线程循环读取把结果通过Control.BeginInvoke回传到UI线程更新控件。private void BtnConnect_Click(object sender, EventArgs e) { _client new McProtocolClient(); if (_client.Connect(txtIp.Text, int.Parse(txtPort.Text))) { _cts new CancellationTokenSource(); _readTask Task.Run(() ReadLoop(_cts.Token)); UpdateStatus(已连接); } } private void ReadLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { // 只读D0~D2三个寄存器的示例 byte[] data _client.ReadWords(0, 3); BeginInvoke(new Action(() { txtD0.Text BitConverter.ToUInt16(data, 0).ToString(); txtD1.Text BitConverter.ToUInt16(data, 2).ToString(); txtD2.Text BitConverter.ToUInt16(data, 4).ToString(); })); } catch (Exception ex) { UpdateStatus(通讯异常 ex.Message); break; } Thread.Sleep(200); } }BeginInvoke和Invoke的区别在于Invoke会阻塞当前线程直到UI更新完成BeginInvoke异步提交不会阻塞。读取循环里我优先用BeginInvoke因为UI更新不应该拖慢数据采集节奏。停止读取时我使用CancellationTokenSource来通知线程退出而不是用已经废弃的Thread.Abort。这个习惯很重要Thread.Abort强行终止线程很容易造成资源未释放、Socket状态不一致的隐患。3.3 数据展示与简单联动界面布局我做了三个区域顶部是连接参数和连接按钮中间是数据监测区用几个输入框显示坐标和关键寄存器值底部是操作区和日志区。日志区用RichTextBox每次收发的帧数据都打印出来调试时能直接看到通讯过程。如果数据显示控件用PropertyGrid还涉及一个细节只想展示不让用户修改就把它的ReadOnly属性设为true。这个问题我被问过好几次有人到处找怎么禁止编辑其实就是一个属性的事。4. 调试、踩坑与常用排查技巧4.1 连接不上怎么办三个必查项这是最常见的故障。每次遇到连不上我都会按这个顺序排查IP网络通不通。在CMD里ping 目标IP如果ping不通检查网线、网卡、防火墙。CNC侧很多是没有防火墙的问题通常出在工控机这边Windows防火墙有时会拦掉非系统端口的TCP连接。端口对不对。三菱CNC以太网通讯常用的端口不是固定的跟PLC模块型号、系统参数有关。常见的默认端口是1024或5550附近但一定以你手上那台设备的参数为准。很多CNC的端口号是通过NC参数设定的需要进维护界面去确认。CNC侧以太网参数有没有配对。IP地址、子网掩码、站号这仨必须和上位机在同一网段且没有冲突。站号虽然是00 00居多但如果设备里设置了别的站号帧里的站号字段也得跟着改。4.2 返回乱码或超时字节序和帧长MC协议只要有一个字节不对CNC要么不回包要么返回异常结束代码。我碰过的两类问题最常见一类是字节序搞反。MC协议大量使用低字节在前命令0401要发成01 04D0的地址00 00 A8要发成00 00 A8点数000A要发成0A 00。如果发现能收到响应但数据明显不对先检查是不是把高低位颠倒了。另一类是请求数据长算错。很多新手把整个请求帧的长度都填进去实际上它只统计“请求数据长”字段之后的部分。按前面的例子读D0的请求数据长是11字节监视定时器2 命令2 子命令2 地址3 点数2不是整个帧的21字节。这个值填错CNC解析边界就错了。4.3 界面卡死与线程中止界面卡死十有八九是跨线程操作控件。看到“线程间操作无效”的异常说明你直接在后台线程操作了UI控件。正确姿势是用Invoke或BeginInvoke把操作封送到UI线程。还有一个小坑频繁Invoke也会导致界面响应变慢。如果你开了50ms一次的读取循环每次都InvokeUI线程会一直被塞满消息。我习惯把读取数据缓存到一个队列UI用Timer定时去队列取这样读取线程和UI线程解耦界面流畅很多。线程中止方面我现在统一用CancellationTokenSource协作取消循环里检查IsCancellationRequested退出前关闭Socket、释放资源。这是相对规范的做法不推荐用Thread.Abort。4.4 通讯不稳定心跳与重连车间环境不比办公室网络抖动、CNC断电、人为误操作都会导致通讯断开。我的DEMO里加了几道保险第一道是超时控制。TcpClient的Read会无限等待必须设置ReadTimeout比如1000ms超时就认为通讯异常。第二道是断线重连。读取循环里捕获到异常后不直接退出程序而是自动尝试重连三次每次间隔2秒。catch (Exception ex) { UpdateStatus(通讯异常尝试重连 ex.Message); for (int i 0; i 3 !token.IsCancellationRequested; i) { Thread.Sleep(2000); if (_client.Connect(txtIp.Text, int.Parse(txtPort.Text))) { UpdateStatus(重连成功); break; } } }第三道是心跳检测。如果长时间没有数据交互CNC侧可能把连接断掉所以即使不读数据也定期发一个读取指令保持链路活跃。这个在正式项目里很有必要。5. 从DEMO到正式项目还差这些事5.1 配置、日志与打包DEMO做验证足够但拿到生产环境还需要补几块内容。连接参数我建议放到配置文件里而不是写死在界面。用App.config或appsettings.json都可以方便维护换一台机床不用重新编译。日志不能只靠RichTextBox生产环境要把通讯日志写入文件。我习惯用NLog或Serilog按天归档记录时间、收发帧、错误码、操作记录。出了故障翻日志定位问题比在现场硬猜高效太多。最后是打包发布。WinForm项目可以用Visual Studio自带的InstallShield也可以使用Inno Setup后者更小巧灵活。打包时记得把目标平台设为x64或AnyCPU并确认目标机器装了对应版本的.NET运行时。工控机上不一定有联网环境离线安装包是必须的。5.2 我的几点经验三菱CNC通讯和PLC通讯虽然同属MC协议但CNC侧的软元件地址映射更依赖型号。M70和M80的坐标地址、系统变量编号可能就不一样所以做项目前一定先把对应型号的“以太网通讯手册”和“NC参数手册”下载下来找到你要读的数据对应的地址再写代码。还有一点CNC上电后可能不会立即开放以太网通讯有些系统需要在维护界面里把“以太网功能”开关打开。我第一次在客户现场就遇到这种情况代码没报错就是连不上最后发现是CNC侧的通讯功能压根没启用。这套DEMO从写到能稳定跑前后花了两天时间大头都在协议排查上。希望这篇文章能把MC协议那层窗户纸捅破让你少走点弯路。如果你也在调三菱CNC或类似设备的通讯欢迎多交流实际遇到的坑工控这东西现场问题永远比书本上写的精彩。本文还有配套的精品资源点击获取