简介:工业数据采集是智能制造与设备联网的前提。数控机床(CNC)的实时坐标、主轴转速、倍率与告警等数据,通常通过专用网络协议(如三菱EZSocket基于TCP/IP)读取。使用C#对SDK进行DllImport封装,可在上位机中快速实现设备状态监控、数据转发及MES对接,适用于车间设备联网、加工中心实时监测等场景。一套三菱CNC数据采集的C#源码Demo,覆盖EZSocket通信选型、报文组包、数值换算、断线自愈等关键环节,帮助工程师快速打通设备到信息系统的数据通路。
1. 三菱CNC数据采集Demo:这套C#源码到底帮你打开了哪扇门
给车间里的三菱加工中心做数据采集,最容易卡住的不是C#写不好,而是设备通信协议像一口黑匣子:手册几百页,地址映射藏在附录,能直接跑通的示例代码又少。这套三菱CNC数据采集Demo附带C#源码,就是把黑匣子打开一条缝——用网口通信库把坐标、主轴转速、进给倍率、报警信息读出来,再在C#上位机里展示和转发。它适合正在做设备联网、MES对接的工程师,也适合想搞懂CNC通信原理的上位机新手。看懂这个Demo,改一改地址表,就能接到自家机床上。
2. 三菱CNC通信选型:为什么走EZSocket而不是改PLC程序
三菱CNC不是一个简单的PLC,它自带完整的数控系统,里面跑的坐标、转速、倍率、告警并不是都暴露在PLC寄存器里。想把这些数据拿出来,常见有三条路:改PLC程序、买协议网关、直接用CNC以太网库。改PLC要动设备厂家程序,车间不敢让你动;协议网关要花钱还要看机型;而三菱官方提供了一套基于TCP/IP的以太网通信库,业内常叫EZSocket接口,机床侧只要在参数里打开以太网功能,就能用C#直接读内部数据。这就是标题里那个Demo的路线,也是目前做三菱CNC数据采集最贴合、最少外部依赖的方案。
2.1 三条采集路线怎么选:网口库、OPC、硬接线
先说结论:如果目标只是把一台三菱加工中心的数据搬出来,优先走网口SDK(EZSocket)。如果你要同时接发那科、西门子、马扎克这些异构设备,才考虑OPC UA或者MTConnect这类标准化协议,但前提是机床侧得支持,而且不少老机型不支持这种接口,最后还是得回到各家私有协议。硬接线IO是最老的办法,拉线到PMC的I/O点,能拿到运行/停止、报警灯这种开关量,但拿不到坐标和程序号,而且布线工程大,适合对实时性要求极高又不差钱的老车间。
这个Demo走网口SDK,核心原因有三个。一是三菱CNC把大部分内部数据都通过SDK开放了,坐标、转速、倍率、程序号、告警都读得到;二是不需要碰PLC梯形图程序,对现场风险最小;三是一台普通工控机加一个网口就能实现,硬件成本低。代价也很明确:协议是二进制帧格式,文档是英文和日文,地址表要按机床型号查附录,第一次读写有一定学习成本。这也是为什么我说“会写C#不等于会做采集”,真正的门槛在协议理解。
2.2 EZSocket的报文与地址:先读懂“读什么”再动手
EZSocket的通信过程可以拆成三件事:建立连接、发读取请求、解析响应。连接是普通的TCP(也有UDP模式),常见默认端口是6832,具体以机床参数为准。连接建立后,客户端要发一个同步包,三菱侧会回一个确认包,这个同步包在官方文档里叫SYNC,很多人在重连时翻车就是因为漏了这一步。
读取请求是二进制帧,不是ASCII文本。帧结构大致分两块:固定头部和请求数据区。头部里有子信道号、消息类型、帧长度这类字段;请求数据区里放指令码(读状态、读坐标、读参数等)、起始地址、读取长度。响应帧同样分头部和数据区,数据区就是你要的原始值。关键问题是:这些字段的偏移量和校验算法在不同 CNC 型号、不同固件版本里不完全一致,所以任何人给你的Demo里的组包代码,都必须跟你手上的官方手册核对一遍再上产线。
地址编码是第二个坑。三菱CNC对外暴露的数据点以“地址编号”组织,类似PLC的寄存器地址,但分类更细:有状态字、轴坐标、主轴数据、程序/刀具数据等。手册附录里的数据一览表会列出每个编号代表什么、什么格式、单位是什么。我一般会让客户先去机床操作面板翻到“参数-网络设置”确认IP,再找到手册附录的“数据地址表”,把我需要的那几行用荧光笔标出来,然后才去读代码。顺序反过来的新手,多半会把相对坐标当成机械坐标采回来。
2.3 最小采集清单:坐标、转速、倍率、告警一张表说清
给车间做Demo,建议一开始不要贪多。先把下面这张表里的数据读通,你的Demo就能横向展示“机床实时状态”了。
| 数据类型 | 典型格式 | 换算与说明 |
|---|---|---|
| 程序号 | 数字编码 | 显示当前运行的NC程序号,注意编码方式按手册 |
| 机械坐标(X/Y/Z) | 有符号整型 | 常见最小指令单位为0.001mm,除以1000得到mm |
| 主轴实际转速 | 有符号整型 | 单位rpm,读取后可与面板核对 |
| 进给倍率 | 编码值 | 通常不是百分比,需要查编码对照表 |
| 主轴倍率 | 编码值 | 同上,编码区间按手册 |
| 运行状态 | 位/字状态 | 区分自动/手动/报警,按位掩码解析 |
| 报警信息 | 告警号与文本 | 文本可能是日文编码,见后面乱码一节 |
这个清单覆盖了设备联网最关心的“在干什么、干多快、出没出事”。采集频率上,坐标和倍率按200ms轮询足够,告警建议单独线程提高频率,或者用NOTICE回调。要注意的是,不同类型点位对应的指令码可能不同,比如读坐标和读状态不是同一个指令,所以一次轮询里通常要发多个请求。Demo里的“读多字”函数就是把同一指令下连续地址一次读回来,减少网络往返。
3. 用C#封装三菱CNC通信库:DllImport、报文组包与数值换算
路线定下来,第二步就是把三菱SDK里的C风格接口请进C#。三菱官方SDK一般提供Windows下的DLL和头文件,C#不能直接include头文件,要用DllImport做P/Invoke封装。这块封装做得稳不稳,直接决定后面所有读取逻辑好不好写。我见过不少人把DLL调用、组包、解析全部堆在窗体按钮事件里,最后动一下界面就卡死,那不是协议难,是封装结构没搭好。
3.1 把三菱SDK的DLL请进C#:DllImport声明与位数匹配
先说位数匹配:拿到动态库第一件事,看它是32位还是64位。常见的老版本Ezsock.dll是32位,你的C#进程就必须在x86模式下编译;新版有的做了x64,就按64位编译。位不匹配不会编译报错,运行时调用直接进程崩溃,这是第一道要踩的坑。
下面是最小主义的DllImport声明,用于连接和关闭:
using System; using System.Runtime.InteropServices; internal static class NativeCnc { // 以你手上官方SDK头文件里的导出名为准,这里用常见命名占位 [DllImport("Ezsock.dll", EntryPoint = "EZ_Open", CallingConvention = CallingConvention.StdCall)] internal static extern int Open( string host, // CNC的IP地址 int port, // 以太网端口,常见6832 int timeoutMs, // 连接超时,毫秒 ref byte subch, // 子信道号,起始为0 byte[] clientId); // 客户端ID缓冲,用来接收分配的ID [DllImport("Ezsock.dll", EntryPoint = "EZ_Close", CallingConvention = CallingConvention.StdCall)] internal static extern int Close(byte subch); }逻辑说明:Open的参数里,subch是子信道号,三菱协议里同一TCP连接可以带有多个子信道,简单Demo用0号就行,但必须传引用,因为SDK会返回可用的信道号。clientId是SDK分配给你的客户端标识,多机床连接时会用到。Close时把subch原样传回去,否则服务端不认。
参数说明:连接超时一般设2到3秒比较合适,太短在网线没插好时直接报错,太长会让UI假死。端口不要想当然填6832,先到CNC网络参数页面看一眼。另外,不同SDK版本导出函数名有差异,有的叫EZ_Open,有的叫EZSOCK_OPEN,还有的SDK封装成了mc_open这类名字,拿到手先看一眼头文件或dumpbin导出表,别照抄。
提示:如果程序一调用就进程崩溃且没有异常抛出,优先怀疑位数不匹配,这是P/Invoke最常见的事故源。
3.2 组一个读多字的请求:连接、发送、接收的最小闭环
连接建好后,读取流程是:组请求帧 → 发送 → 接收响应帧 → 按帧头里的长度切出数据区。三菱的帧格式我在2.2说过,字段偏移以手册为准,这里给一个可改的骨架:
public byte[] BuildReadRequest(byte subch, int instruction, int startAddr, int count) { // 头部长度与字段布局必须对照你手上SDK的报文格式章节 int headerLen = 30; var buf = new byte[headerLen + 8]; // 以下只是占位:实际要把子信道、指令、起始地址、数量按手册布局写入 buf[0] = subch; BitConverter.GetBytes(instruction).CopyTo(buf, 10); BitConverter.GetBytes(startAddr).CopyTo(buf, 14); BitConverter.GetBytes(count).CopyTo(buf, 18); return buf; }这块的注释不是偷懒,是血的教训:不同机床固件版本对头部字段的定义有差异,直接抄网上代码翻车的概率很高。正确做法是拿官方头文件里的结构体定义或报文格式图,一行行对字段偏移。
发送和接收也一样,Socket超时要单独设置。一般做法是给Socket设ReceiveTimeout为1到2秒,读不到数据就主动清空缓冲区并重连。注意不要把Socket操作放在UI线程,不然CNC没回包时界面就冻住。这里我习惯把Socket封装成一个CncChannel类,把Open、Send、Receive、Close收在同一个类里,业务层只跟CncReader打交道,不直接碰字节流。
3.3 原始值转业务值:机械坐标换算与倍率解码
数据读回来是一堆short/int,直接显示是乱码数值。这一步要按手册做换算。常见换算有:
// 假设坐标寄存器的最小单位为0.001mm double xMM = rawX / 1000.0; // 倍率常以编码值返回,需要按对照表解码 int DecodeOverride(ushort raw, string model) { // 不同面板倍率编码区间不同,这里用开关示例 switch (raw) { case 0x4000: return 50; // 占位示例,具体查手册 case 0x4001: return 60; default: return 100; } }注意事项:坐标值有正有负。寄存器可能是16位有符号,读出来是ushort时要做符号扩展,否则负坐标会显示成六万多。转速和倍率也一样,先确认是有符号还是无符号,再决定用什么类型解析。我惯用的做法是:拿面板上的实际值,和一个已知读数对照,换算对了再往下写。这个步骤偷懒,后面MES端全是清洗数据的麻烦。
4. 跑通Demo的最小上位机:连接配置、轮询线程与断线自愈
前面封装好了通信层,接下来要把它组织成一个能常驻运行的C#上位机。这个Demo程序应该有三层:配置层负责读IP、端口、轮询间隔;采集层负责在后台线程里按固定节奏发请求;分发层把数据送给界面或MES。三层不拆开,采集和界面耦合在一起,将来加一台机床就是一场灾难。
4.1 工程结构:配置、读取器、轮询线程三层拆开
我一般会在解决方案里建三个类:CncConfig、CncReader、CncPollingService,加上一个MainForm做展示。配置文件用JSON,启动时反序列化,这样换机床不用改代码重新编译:
{ "Cnc": { "Host": "192.168.1.10", "Port": 6832, "PollIntervalMs": 200, "ConnectTimeoutMs": 3000, "Items": ["X", "Y", "Z", "SPEED", "FEED_OVERRIDE", "ALARM"] } }参数说明:PollIntervalMs默认200毫秒够一般展示用,低于50毫秒会让CNC的通信负担明显上升,除非有明确需求不要追极限。Items数组是你关心点的清单,CncReader按这个清单组装读取请求。把配置外置到JSON后,现场调试时改IP不用动工程,对车间维护人员友好很多。
4.2 轮询线程与数据分发:不要阻塞UI线程
轮询服务用一个独立线程跑循环,读到数据后通过事件发布出来。先定义数据结构,再写循环主体:
public sealed class CncSnapshot { public double X, Y, Z; public int SpindleSpeed; public int FeedOverride; public bool IsAlarm; public DateTime Timestamp; } public sealed class CncPollingService { private readonly CncReader _reader; private readonly int _intervalMs; public event Action<CncSnapshot> DataArrived; public void Run(CancellationToken token) { while (!token.IsCancellationRequested) { try { var snap = _reader.ReadSnapshot(); DataArrived?.Invoke(snap); } catch (Exception ex) { // 记录异常,连续失败N次后触发重连 } Thread.Sleep(_intervalMs); } } }逻辑说明:Thread.Sleep放在读取之后,保证两次采集之间至少间隔固定毫秒数。DataArrived事件在后台线程触发,界面订阅时要用BeginInvoke或async/await切回UI线程,直接改控件跨线程会偶发崩溃。这个结构的好处是采集频率和展示频率解耦:界面卡一下,后台采集不受影响。
参数说明:轮询间隔一般取100到500毫秒。取100毫秒时,界面上坐标看起来丝滑,CNC也扛得住;取500毫秒则数据有半秒延迟,适合只做报表记录。如果现场要求毫秒级看主轴负载波形,就得改走三菱的周期通知功能,不能用轮询硬顶。
注意:轮询间隔低于50毫秒时,要评估CNC是否有丢包或通信中断的迹象。真到了这种需求,先确认协议手册里的最小采样周期,再决定方案。
4.3 断线重连:三菱连接数限制下的自愈策略
CNC侧对连接管理比普通TCP服务端更严格,常见问题是断线后马上重连失败,因为服务端还认为上一个会话没断。我惯用的做法是:连续读失败3次后,先Close旧连接,等待1到2秒,再重新Open。不要做无限快速重连,会把CNC通信模块搞出假死状态。
public void EnsureConnection() { if (_connected) return; _native.Close(_subch); Thread.Sleep(1500); // 给服务端释放会话的时间,这个等待值也算玄学,但有效 _subch = 0; int rc = _native.Open(_host, _port, 3000, ref _subch, _clientId); _connected = rc == 0; }逻辑说明:Sleep 1500毫秒是现场调出来的经验值,太短CNC不认,太长影响恢复时间。Open前把subch清零再传引用,避免把旧的无效信道号传给服务端。重连成功后,最好重新SYNC一次,这个Demo里同步失败会自动走EnsureConnection自愈。
这章就是把这个Demo程序做成一个能丢在车间工控机上连续跑一周不重启的最小骨架。下一章我会把最容易翻车的几个点单列出来,照着排查能省下一整天。
5. 三菱CNC数据采集避坑清单:连不上、零值、乱码等5个高频坑
写三菱采集的这段时间里,我把现场踩过的坑按“现象→原因→解决”整理成了一份清单。下面五条最典型,照着排查,能少走不少弯路。
5.1 连不上机床和重连失败
现象1:C#程序里Open返回超时,但机床IP能Ping通。原因:机床侧以太网参数里没启用CNC通信功能,或者端口不对,Ping通只说明网络通,不代表协议通。解决:到机床参数界面找到“网络/接口”设置,确认启用以太网功能,确认端口(常见6832),然后用SDK自带的测试工具把连接跑通,再回C#侧调。调连接顺序应该是测试工具→C#封装→业务代码,倒过来查会很累。
现象2:程序运行几小时后断线,重连失败。原因:CNC侧同一时间只允许一个上位机连接,旧连接没释放;或者现场网线接触不良。解决:按4.3节的EnsureConnection做延迟重连,日志里记录每次重连时间。如果多台电脑同时连同一台机床,要把工作电脑固定为唯一客户端。
5.2 坐标全是零和倍率对不上
现象:读数能跑通,但坐标始终是0。原因:典型的是读错了地址,把“相对坐标”当成“机械坐标”读,或者坐标寄存器没使能。解决:先拿手册附录的地址表,找到机械坐标对应的地址编号,再在面板上摇动一个轴,看读数是否变化。能连上不代表地址对,这一步必须人工对一遍。
现象:读到的进给倍率是“60”这种整数,但面板明明显示的是80%。原因:倍率寄存器返回的是编码值,不是百分比。解决:查该机型的编码对照表。三菱不同面板倍率编码区间不同,有的0x4000对应50%,有的对应100%。把这个关系做成上面的DecodeOverride方法,写成配置文件,换机型改配置不动代码。
5.3 中文乱码与DllImport崩溃
现象:程序号和报警文本读回来全是“锟斤拷”或日文符号。原因:CNC返回的文本是Shift-JIS编码(日文Windows的处理方式),C#默认字符串是UTF-8,编码不一致。解决:拿到原始字节后显式转换:Encoding.GetEncoding(932),“932”是Shift-JIS在.NET里的代码页。注意前提是程序能跑在Windows上,Linux服务器上要先注册代码页。
现象:调用DLL的瞬间进程崩溃,异常都来不及捕。原因:动态库位数和进程位数不匹配,或者函数签名和真实导出名不一致。解决:先用DependencyWalker或dumpbin查看DLL导出函数名,核对EntryPoint;再把C#工程的Platform目标改成x86或x64,和DLL对齐。这个坑最容易掩藏在一堆眼花缭乱的报错里,所以拿到SDK第一件事就是做位数对齐。
这五条都是我在现场真实翻车过的,其中倍率编码和重连等待这两条,几乎每台新机床第一次接入都会遇到。排查顺序建议是:先确认能连,再确认地址对,再谈数据换算,最后才优化稳定性和代码结构。
6. 从Demo到产线数据服务:用队列把采集值稳定送进MES
Demo跑通只是第一步,真正值钱的是把它从“能读”变成“能稳定读、能送数”。这里我分享一个最简单的升级:把轮询和入库解耦。
6.1 用ConcurrentQueue把“轮询”改成“生产-消费”
采集线程只负责把快照塞进队列,另一个入库线程负责攒批写入数据库。这样即使MES侧数据库慢了一下,采集线程也不会被拖住。示意代码:
private readonly ConcurrentQueue<CncSnapshot> _queue = new(); // 采集线程里 private void OnDataArrived(CncSnapshot snap) { _queue.Enqueue(snap); } // 独立入库线程 private async Task FlushLoop(CancellationToken token) { while (!token.IsCancellationRequested) { if (_queue.TryDequeue(out var snap)) { await Db.InsertAsync(snap); // 单条写也可以,批量更快 } else { await Task.Delay(200, token); } } }参数说明:队列累积量要监控,如果短时间内积压上千条,说明下游写不动了,就要降轮询频率或者升级数据库结构,而不是无限等待。批量入库一般以50条或2秒为窗口,两者先到先写。
6.2 多机床接入与落库:从Demo程序到常驻服务
多台机床接入时,每台机床启动一个CncPollingService实例,但连接数和线程数都要限制,避免一台工控机同时开二三十条线程。常见做法是采集层共享一个线程池,连接管理各自独立。落库方面,建议直接把快照按时间戳全表保存,给“机床ID+时间”建索引,后面做OEE和利用率分析都靠这张明细表。MES要汇总时再按分钟/小时聚合,不要在采集线程里做聚合计算。
我在第一台机床上线时吃过的亏是:当时觉得采集程序只是个Demo,日志随便写写,后来半夜断网重连,第二天想看历史记录什么都没有。现在我的习惯是,凡是产品化的采集服务,第一版就做好三件事:时间戳校准(和机床时间对表)、重启自恢复、日志留够30天。这三点比花哨的界面有用得多。
这个方向做下来,三菱这台机器你搞通了,再去接其他品牌CNC,方法论是一样的:先查协议手册,再做最小闭环,最后考虑稳定的数据通路。希望帮到你。
本文还有配套的精品资源,点击获取