ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三菱CNC数据采集Demo用C#实现全流程详解:EZSocket通信与宏变量读取

三菱CNC数据采集Demo用C#实现全流程详解:EZSocket通信与宏变量读取

简介:C#数据采集Demo面向三菱CNC系统,专为机床自动化工程师、上位机开发者和工业物联网从业者设计,旨在解决机床实时监控、加工参数读取、设备状态追踪及故障日志获取等工业数据采集难题。压缩包约3.52MB,内部包含PDF格式的参考手册和三菱CNC模拟环境SimCNC的源码目录;参考手册用于查询通讯协议、寄存器地址与数据格式,SimCNC则支持在无实体机床的情况下搭建仿真调试环境,方便反复验证采集逻辑。Demo源码演示了配合A2.exe通讯软件与CNC建立连接,通过C#实现对多种型号三菱机台的数据读取,并将采集功能封装为独立软件或插件,可灵活接入现有产线信息化系统。整体代码结构清晰,包含连接建立、数据解析、异常处理等关键模块,是学习工业通讯编程的良好范本。已有620人学习下载,适合需要快速落地三菱CNC数采项目或进行技术预研的开发者参考,能显著缩短环境搭建与排错时间。

1. 三菱CNC数据采集Demo:先想清楚要采什么,别急着开VS

车间里一台三菱加工中心正在跑活,操作员盯着控制面板,你在办公室电脑前想看到它主轴转速、当前坐标、有没有报警——这就是三菱CNC数据采集Demo附C#源码这个标题真正要解决的事。它不靠串口线,不靠人抄屏,而是走CNC自带的以太网口,用C#把宏变量、坐标、报警号实时拉回来。反直觉的地方在于:新手最想要的往往是"能把数读出来"的激动感,但真正决定这活儿成不成的,是动手之前先想清楚要采哪几个地址、用什么协议、帧怎么解析。常见做法是走三菱的EZSocket中间件,DLL一引,C#里P/Invoke一调,十几行代码就能连上,但参数不对时一个Open函数能卡你一下午。这个方向适合做设备联网、MES对接、机加工自动化的工程师,也适合刚入行想拿一台真设备练手的同学。看完这篇文章,你至少能把这套Demo跑通,并且知道坑在哪。

2. 准备工作:三菱CNC数据采集Demo的网络前提与协议选型

2.1 网口通信是前提,串口方案到了Demo阶段就该放弃

很多老工程师的第一反应是“CNC后面不是有RS232口吗,直接串口读不就行了”。这个思路在二十年前没问题,但现在做数据采集,串口的三个短板非常明显:一是传输速度慢,115200波特率下刷新一次状态要几百毫秒,别说做高频采集;二是好多老旧串口方案只能单向输出,想写宏变量回机台很难;三是串口线超过15米信号就开始飘,车间现场拉线很不现实。

所以这个Demo的正确起点是以太网口。三菱的中高端系统比如M70、M700、M80系列,标准配置里都带以太网口,参数里面设好IP地址、子网掩码之后,上位机直接用网线连交换机就能访问。通信链路的示意图其实很简单:C#程序通过TCP协议发一个ASCII字符串命令帧给CNC,CNC回一个带数据的响应帧,程序把帧解析出来就完事了。

这里有个关键选型点:直接用TCP发MC协议帧虽然可行,但工程上没人这么干,因为帧头、校验、分包都得自己拼,出错率太高。业界通用做法是安装三菱的EZSocket通信库,它本质上是把MC协议封装成了C风格函数,提供固定接口,往上位机这边暴露动态链接库,C#项目里声明一下就能调用。Demo阶段选它,省掉的是协议拼帧的麻烦,留下的是数据解析的核心逻辑。

2.2 访问对象A/B/C/D:Demo该用哪一个通道

EZSocket里有一个概念叫“访问对象”,英文叫Access Target,官方分为A、B、C、D四种。A类走的是三菱PLC的MC协议,用来读PLC软元件;B类是Windows下和PC内部数据交换用的;C类是访问CNC数值数据的通道,宏变量、坐标、刀补、报警都归它管;D类是访问机床侧的PLC数据,比如IO信号、继电器状态。

写成表格更直观:

访问对象面向的数据Demo里有没有用
APLC软元件(D、M、Y等)一般不用
BWindows环境内部通信不用
CCNC数值数据(宏变量、坐标、报警)核心通道
D机床侧PLC数据(IO、继电器)按需

C#做CNC采集,绝大多数需求都集中在C类上。你在MES里看到的"主轴转速""当前X轴坐标""报警号",全部通过C类读出来。有些车间还想看机床门有没有关、红灯有没有亮,这些信号在D类里,但那是后话,Demo阶段先把C类吃透。

有个细节要注意:EZSocket的DLL同时支持A类访问,所以如果你这台CNC的PLC里存了工件计数之类的数据,也可以用同一套代码去读,只需要把打开连接时的访问对象类型改一下。Demo阶段不建议一上来就通吃,先把C类读通,再往D类扩展。

2.3 CNC侧参数核对:IP、端口、系统版本差异

连不上CNC的时候,八成不是代码的问题,而是CNC侧的网络参数没核对。三菱系统里以太网参数一般在维护菜单的"以太网设定"或者"网络设定"页面里。需要确认四项:

参数典型值说明
IP地址192.168.1.50和上位机在同一网段
子网掩码255.255.255.0不跨网段就够用
端口号6000不同系统版本可能有差异
通信协议TCP打开对应通道

端口号是个血泪高发区。部分老系统默认用6000,部分系统在参数里单独开了一个端口,还有的系统要同时开UDP和TCP两个通道。我一般会在CNC旁边先用笔记本电脑ping一下IP,通了再跑Demo;ping不通就别折腾代码,先查网线和IP段。

还有两个前置条件容易被忽略:一是EZSocket服务器端程序要和系统匹配,二是CNC侧参数里的"传输协议"要选成允许外部设备访问。某些系统出厂时默认关闭了外部访问,需要到维护模式里打开。这些操作需要操作员权限,做Demo之前最好和车间师傅打个招呼,别自己偷摸改参数,改完机台报警就麻烦了。

3. 核心代码:C#读三菱CNC宏变量、坐标与报警

3.1 创建一个控制台Demo,把EZSocket.dll引进来

拿到三菱EZSocket安装包之后,安装目录下会有一个动态链接库,名字一般是EZSocket.dll。C#这边不能直接添加引用,因为它是C风格导出函数,不是托管DLL,正确做法是用DllImport声明外部函数。先建一个控制台项目,平台目标改成x86,然后写一个静态类来包装接口:

using System; using System.Runtime.InteropServices; public static class EZSocketWrapper { // 声明三个核心函数:打开、读取、关闭 [DllImport("EZSocket.dll", EntryPoint = "EZSocket_Open")] public static extern int Open( IntPtr[, ,] pCNC, // 连接句柄(三维数组) int nType, // 访问对象类型:2表示C类(CNC数据) int nOpen, // 通信方式:1表示TCP string pIPAddr, // CN C的IP地址 string pPort, // 端口号 int nTimeOut); // 超时时间(毫秒) [DllImport("EZSocket.dll", EntryPoint = "EZSocket_Read")] public static extern int Read( IntPtr[, ,] pCNC, // 连接句柄 int nType, // 访问对象类型 string pReadBuf, // 读命令帧 ref string pRetBuf, // 返回数据缓冲区 string pErrMsg); // 错误信息输出 [DllImport("EZSocket.dll", EntryPoint = "EZSocket_Close")] public static extern int Close( IntPtr[, ,] pCNC, int nType); }

这段声明里最容易栽跟头的是句柄类型。EZSocket的手册上写的是三维指针数组,C#这边的对应写法就是IntPtr的三维数组,初始化时每个维度给3就够了。nType传2代表C类访问,nOpen传1代表走TCP。端口参数是字符串不是整数,这是很多人第一次调用时报错的原因。超时时间建议给3000毫秒,太短在车间网络抖动时容易误报失败,太长则程序卡死在那里。

注意DLL的位数必须和编译目标一致。EZSocket.dll在多数版本里是32位的,所以项目平台目标一定要选x86,否则运行到Open那行就抛BadImageFormatException,这个在第5章避坑里展开。

3.2 连接到CNC:Open和Close的写法

连接这块的套路是固定的:先创建句柄数组,调Open,判断返回值,用完调Close。下面是完整的最小连接代码:

static void Main(string[] args) { // 连接句柄,三维数组结构 IntPtr[,,] pCNC = new IntPtr[3, 3, 3]; // 打开C类访问,TCP方式,连192.168.1.50的6000端口 int ret = EZSocketWrapper.Open( pCNC, 2, 1, "192.168.1.50", "6000", 3000); if (ret == 0) { Console.WriteLine("连接成功"); // 在这里执行读取操作 // ... EZSocketWrapper.Close(pCNC, 2); } else { Console.WriteLine("连接失败,错误码:" + ret); } }

返回值ret为0表示成功,非0为失败。这一步很多人第一次跑通后会特别兴奋,但别高兴太早——连接成功只代表TCP链路通了,真正读数据时的帧格式错一丁点,Read照样返回失败。另外Close操作不能省,有的现场出现过程序崩溃后CNC侧连接资源没释放,导致后续无法再次连接的情况,虽然重启机台能恢复,但车间里重启设备你可得等操作员排完当前工件。

3.3 读宏变量:#100的实时值怎么取回来

三菱系统的宏变量是机台逻辑里最常用的数据区,比如把当前工件计数写到#100,把主轴倍率写到#101,MES要的就是这些。读取宏变量用到EZSocket_Read,命令帧格式我一般按"R+地址+长度"来拼:

static void ReadMacro(IntPtr[,,] pCNC, int macroNum, out float value) { // 拼读命令帧:读#100就填 R0,100,1 string cmd = "R0," + macroNum + ",1"; string retBuf = ""; string errMsg = ""; int ret = EZSocketWrapper.Read(pCNC, 2, cmd, ref retBuf, ref errMsg); if (ret == 0) { // 返回数据从第6个字符开始是有效数据区 string hexData = retBuf.Substring(5); // 三菱返回的是倒序十六进制字符串,要反转字节再转float byte[] bytes = HexToBytes(hexData); Array.Reverse(bytes); value = BitConverter.ToSingle(bytes, 0); Console.WriteLine("#" + macroNum + " = " + value); } else { Console.WriteLine("读取失败:" + errMsg); value = 0f; } } static byte[] HexToBytes(string hex) { int length = hex.Length / 2; byte[] result = new byte[length]; for (int i = 0; i < length; i++) result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); return result; }

这里有个工艺问题:CNC读回来的数据是十六进制字符串,而且是倒序字节序。比如浮点数1.5在内存里是3F C0 00 00,三菱回包里往往是0000C03F,必须反转之后用BitConverter转float。这个字节序坑每一台新机型都得试一次,建议把转出来的值和面板上显示的值对一下,对不上就交换一下高低字节顺序。

宏变量号本身也有讲究:公共变量#100到#149一般默认为空变量区,可以随便用;#150到#199在某些系统里是模拟输入通道数据;#500到#999是保持型变量,断电不丢。读#500之前先确认一下机床厂有没有把这部分区域分配给专属功能,避免读到无意义数据还当成宝贝存进数据库。

3.4 读坐标和报警:把机台状态拼出来

宏变量能解决数据交换,但MES上层更关心的是坐标和报警。读取绝对坐标和报警号用到的命令帧前缀分别是E和A,下面是读绝对坐标和报警的核心片段:

static void ReadPositionAndAlarm(IntPtr[,,] pCNC) { string posBuf = ""; string almBuf = ""; string errMsg = ""; // 读绝对坐标 int posRet = EZSocketWrapper.Read(pCNC, 2, "E", ref posBuf, ref errMsg); if (posRet == 0 && posBuf.Length >= 61) { // 第1~3位是附加信息,每个坐标占6个字节 string xHex = posBuf.Substring(6, 12); string yHex = posBuf.Substring(18, 12); string zHex = posBuf.Substring(30, 12); // 转字节再反转再转float,逻辑同上 float x = ConvertHexToFloat(xHex); float y = ConvertHexToFloat(yHex); float z = ConvertHexToFloat(zHex); Console.WriteLine($"X:{x:F3} Y:{y:F3} Z:{z:F3}"); } // 读报警号 int almRet = EZSocketWrapper.Read(pCNC, 2, "A", ref almBuf, ref errMsg); if (almRet == 0 && almBuf.Length >= 9) { // 报警号也是倒序,转int string alarmHex = almBuf.Substring(5, 4); byte[] bytes = HexToBytes(alarmHex); Array.Reverse(bytes); int alarmCode = BitConverter.ToInt16(bytes, 0); if (alarmCode != 0) Console.WriteLine("当前报警号:" + alarmCode); else Console.WriteLine("无报警"); } }

坐标读取返回的帧长度是固定的,每个轴占6个字节十六进制,排列顺序是X、Y、Z;有些系统还带A轴、B轴,长度就不一样,解析前先判断返回帧长度能省不少麻烦。报警读取时要注意报警码按有符号短整型解析,负号代表报警级别。读报警还分NC报警和机床报警,A命令返回的是系统报警,机床报警走D类访问,做Demo时别把两者混在一起。

一个实用技巧是:把坐标和宏变量的读取放在同一个循环里,每隔200到500毫秒刷一次屏。刷新太快没必要,太慢又看不出实时效果,车间里大部分采集需求到500毫秒级别的刷新率已经足够。

4. 参数与内存映射:宏变量、公共变量和回复帧的边界

4.1 三菱宏变量的分区:公共变量、保持型变量和刀补

拿到一套Demo代码之后,最先要问的问题是"我该读哪些地址"。三菱CNC的宏变量地址分布有规律可循,但不同系统之间有一点差异,常用分区是这样的:

变量号范围类型Demo应用场景
#100 ~ #149空(公共变量)跨程序传递数据
#150 ~ #199部分系统为模拟量输入读主轴电流等模拟量
#500 ~ #999保持型变量工件计数、生产统计
#2000 ~ #2199刀补变量(部分系统)刀具磨损管理
#3000 ~ #3999系统设定相关一般不碰

Demo阶段建议优先读#100区间和#500区间。#100区间断电会清零,适合读当班产量这类临时数据;#500区间是断电保持的,适合存班长累计数。很多工厂的做法是让PLC在换班时把#500里的值搬运到另一个地址,上位机只负责读,不负责写。

刀补的地址范围需要重点提示:不同系统版本对#2000系列的映射并不一致,有的系统里#2001对应1号刀的X向刀补,有的系统里却跳过了部分保留段。读取刀补之前找操作员确认一下机床厂的宏变量分配表,或者干脆用机台面板手动改一个已知值,再读出来验证——这是最朴素的测试方法。

4.2 回复帧的字节对齐和大小端坑

EZSocket返回的数据区是十六进制字符串,规则是所有数据都按字倒序。这里说的字倒序不是字节倒序,而是两个字节一组颠倒。举例说明:数据区里如果收到"3412",实际值应该是0x1234。浮点数则四个字节整体反转,前面代码里用了Array.Reverse,其实就是把大小端问题用最简单的方式抹平了。

解析时的另一层坑在于命令帧和数据帧之间可能带空格或控制符。不同版本EZSocket返回的帧长度有细微差异,有的带起始符,有的不带。我建议在写解析逻辑之前先做一步"打印裸包"操作:

// 调试用:把Read返回的原始帧原样打印出来 Console.WriteLine("RAW:[" + retBuf + "] len=" + retBuf.Length);

把这行加到Demo里跑一次,对照三菱手册里的帧结构看一遍,再写Substring的截取逻辑,比对着网上抄解析代码靠谱得多。不同系统的返回帧长度不一样,在M70上试好的偏移量拿到M80上可能就得加一位。

还有一个小提醒:全部用Substring截取十六进制字符串,看起来简单,但在解析多个轴坐标时容易数错位数。碰到解析结果数值怪异的情况,先数一下每个字符的下标,用调试器逐帧看,不要靠猜。

4.3 采集频率与系统负荷:别把Demo写成死循环

数据采集最常见的失控写法是while(true)里不加延时,CPU占满不说,CNC侧的EZSocket服务也会被高频请求拖累。三菱官方对采集频率没有硬性指标,但业界经验是:状态数据(坐标、转速)500毫秒刷一次,报警数据可以做到200毫秒甚至事件触发,产量数据按分钟或按工件触发即可。

一个推荐的安全采集框架是这样的:

// 用定时器代替死循环,避免阻塞UI线程 System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 500; // 500毫秒刷一次 timer.Elapsed += (sender, e) => { ReadPositionAndAlarm(pCNC); ReadMacro(pCNC, 100, out float count); }; timer.Start();

注意这里有个隐患:Timer触发的事件如果在上一轮读取还没结束时又进来,就会导致消息堆积。保险做法是加一个bool锁标志,读取中则跳过本次触发。Demo阶段可以不考虑并发,但往生产走这一步是必须的。

另外,不要同一时间开多个线程去调EZSocket_Read,DLL内部不是线程安全的。多个采集线程同时调用同一个连接句柄,轻则返回数据错乱,重则直接让连接断掉。需要采集多组数据时,串行读,别并行读。

5. 三菱CNC采集Demo的常见踩坑:现象、原因与解决

5.1 Open返回失败,但Ping得通CNC

现象:上位机能ping通CNC的IP,但EZSocket_Open返回非0,读取完全无法进行。

原因:TCP链路通,不代表CNC侧的EZSocket服务在监听。最常见的情况有三种:CNC系统参数里没打开外部访问权限;端口号和实际监听端口不一致;访问对象类型选错了类型(比如C类通道没开,但A类通道是好的)。

解决:登录CNC维护画面,检查"以太网设定"里的端口和访问许可。先把访问对象类型换成A类试一次,如果能连上说明C类通道没开;如果A类也连不上,检查端口号。还有一招是看防火墙,Windows自带的防火墙经常拦截TCP入站连接,临时关掉再测一下就能定位。

5.2 调用DLL时抛BadImageFormatException

现象:代码编译通过,一运行到Open函数就抛出BadImageFormatException,异常信息里通常写着"32位还是64位不匹配"。

原因:EZSocket.dll是32位动态库,而Visual Studio里新建的.NET项目默认平台目标是AnyCPU,在64位系统上运行时会用64位进程加载32位DLL,直接崩。

解决:打开项目属性,找到"生成"选项卡,把平台目标改成x86。改完重新生成,问题消失。这个坑几乎每个第一次接触EZSocket的C#开发者都会遇到,网上搜相关报错一搜一大片,但知道原理之后十秒钟就能解决。

5.3 读到的宏变量是全零或者NaN

现象:Read调用返回成功,但解析出的变量值要么全是0,要么是NaN。

原因:三菱宏变量分为数值型和逻辑型,而数值型里又有整型和浮点型之分。系统默认按双字节整数返回时,你用float解析就会得到诡异值。另外宏变量所在的地址区间可能根本没被写入有效数据,空变量区读出0是正常现象。

解决:先在CNC面板上手动给#100赋一个已知值,比如12.34,然后跑Demo读一次。读到12.34说明解析逻辑正确;读到0说明地址区间为空,换个变量号测试;读到一堆乱码说明数据类型解析错了——尝试把BitConverter.ToSingle换成ToInt16,或者反过来。用已知值校准是排查这类问题最有效的手段。

5.4 程序跑一会儿后连接自己断开

现象:Demo运行几分钟后,Read开始返回错误,重试也无效,只有重启程序才能恢复连接。

原因:EZSocket服务端对空闲连接有超时回收机制,超时时间从几秒到几分钟不等。程序长时间不发命令,服务端主动断开连接。还有一种情况是网络里有广播风暴,TCP连接被干扰。

解决:在采集循环里加入周期重连机制。常见做法是每次Read失败时自动执行一次重连:先Close,再Open。更温和的做法是缩短Timer间隔,把心跳维持在一秒一次以内,避免空闲超时。注意重连时要释放旧句柄,否则句柄会越积越多,最终系统资源耗尽只能重启。

另外排查一下网络里有没有人在跑大流量传输,车间里的监控系统回传视频有时会占满交换机带宽,把CNC和上位机挪到同一个交换机下或者划分独立VLAN能有效缓解。

5.5 报警号读出来和面板显示不一致

现象:报警命令读出的编号和机台面板上显示的报警号对不上,有的差个几千,有的完全不在一个数量级。

原因:报警命令读出的是内部报警指针,不等于面板显示的报警号。面板显示的是格式化后的报警文本,内部编号需要对照报警表映射。另外一个常见干扰是:机床厂在标准系统上叠加了自己的PMC报警,这些报警由D类访问对象负责,C类读不到。

解决:先确认读的是NC报警还是PMC报警。NC报警用A命令读内部编号,再查系统报警表映射成面板报警号;PMC报警要切换到D类访问对象读取。Demo阶段建议直接在CNC上触发一个已知报警,比如急停或者门开关,然后对比读回来的编号和面板值,把对应关系记录下来写进配置表里。这个工作看起来笨,但做完以后对接MES报警模块就顺畅多了。

6. 让Demo往生产方向走:数据落库、心跳重连与MES桥接

Demo跑通了,接下来要做的三件事决定了它在生产环境里能不能站住脚:数据落库、断线自动重连、给上层系统留接口。

数据落库最省事的方案是SQLite,不需要额外装数据库服务,一个文件搞定。把每次读到的坐标、宏变量、报警号连同时间戳写进一张表里,粒度控制在秒级就够,保留最近90天数据。注意写入操作要和采集操作解耦,采集线程只负责读,写库交给独立线程或后台任务,避免磁盘慢的时候拖垮采集循环。

断线自动重连是生产环境刚需。做法是把Open、Read、Close封装成一个类,内部记录连接状态,每次Read失败时触发重连,重连次数达到上限后向上抛出异常,由上层决定是告警还继续重试。关键细节是重连之前必须Close旧句柄,很多"重连几次之后彻底连不上"的问题都出在句柄泄漏上。

给MES桥接的方式有两种,简单一点是Demo里直接提供HTTP接口,MES通过网页接口拉数据;更稳妥的是把采集到的数据推给消息队列,由消费端去落库和展示。Demo阶段不用做太复杂,把采集类写干净,数据格式定义成统一的JSON结构,后续扩展就顺理成章。

最后说一个我在车间里淌过的教训:有一回为了赶进度,我把采集循环直接写进窗体Timer里,刷新间隔压到50毫秒,结果一台老款系统被高频请求打到以太网模块卡死,操作员停机重启才恢复。后来我养成了两个习惯:任何采集代码先量化频率再上线,任何外挂程序都加总开关和限速。做设备采集,慢一点不会出事,快过头一定出事。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进