爱普生机器人直连三菱PLC:基于MC协议自定义通信驱动实战
工业现场把爱普生机器人和三菱PLC放在同一个电控柜里配合干活是再常见不过的场景。但机器人怎么从PLC里高效、批量、实时地拿数据往往是整个项目里最磨人的一环。IO硬接线简单粗暴点位一多就变成蜘蛛网买协议转换网关又经常被点位映射数量、寄存器块限制和黑盒调试折磨。我最终的选择是抛开中间网关直接基于三菱MC协议TCP自己封装了一套通信驱动让爱普生机器人作为TCP客户端直连PLC读写D区、M区的数据表。这篇博文把驱动设计思路、MC协议报文结构、源码封装方式、爱普生侧集成方法和现场踩坑记录一次性整理出来给正在用爱普生RC开发、又不想被网关黑盒卡住的同行做一个参考。1. 为什么放着现成的IO和网关不用非要自己写驱动1.1 常见数据交互方案的短板先摆开说说现场最常见的三条路IO硬接线、协议转换网关、MC协议直连。我把它们的实际差别放在一张表里看会比较直观。方案适合场景主要问题IO硬接线点位个位数、实时性要求极高、逻辑简单加点位要改线改程序信息量密度低批量数据和坐标值根本传不了协议转换网关想快速打通点位几十到几百点位映射配置繁琐黑盒难排查批量读写受限部分网关有寄存器块数量和长度上限MC协议直连点位多、要传输坐标/配方/计数值、需要批量读写需要自己处理帧结构、校验、连接管理前期工作量大IO方案的问题很好理解信号少的时候它很可靠但一旦要传十几个坐标值、几十条配方参数用IO去拼数据会累死人。网关方案我实际用过最大的感受是“中间加了一层不透明的东西”明明只是读PLC一个D区网关里要建映射表、要选字节序、要分块出了问题你根本不知道是PLC没送出来还是网关转换坏了还是机器人侧读错地址。尤其当现场设备在客户那边运行时你只能远程对着日志猜这种体验非常痛苦。1.2 我踩过的第一次坑把协议当成函数库抄最开始我也没打算自己写而是想找一套现成的C#或Python开源库改一改直接跑。这其实是个误区。MC协议的开源代码并不难找但大多数代码只覆盖了“读一个D区、写一个D区”这种最基础场景。等你真把它塞进机器人任务里就会发现帧长度计算、响应粘包、断线重连、PLC侧连接数限制这些都没有人替你处理。我记得第一次在实验室调试时照着示例代码改了一个读D区的功能用网络调试助手手动发同样的帧能读到数据换成程序发就是超时。查来查去最后发现是请求帧里的长度字段没按二进制帧的规则算导致PLC认为帧不完整直接丢弃。那一次之后我意识到通信驱动的核心不是那几行Socket收发代码而是对帧结构、长度计算和状态管理的理解。这也直接决定了我后来封装驱动的分层方式后面会详细讲。2. MC协议3E帧基础读懂报文比会写代码更重要2.1 3E帧长什么样现在爱普生机器人和三菱PLC之间做TCP/IP通信主流用的就是MC协议里的3E帧也叫QnA兼容3E帧。这个帧格式的好处是无需像老式1E帧那样先建立连接再收发直接一条报文一个响应简单干净。三菱Q系列、L系列、FX5U等内置以太网口基本都支持。3E帧从结构上可以分成几段子头、长度、地址信息、指令和数据段。我以二进制模式为例画一个直观的表格字段长度说明子头2字节二进制模式固定 D0 D0ASCII模式则是字符串 F9F9请求长度2字节从网络号开始到数据段结束的字节数网络号1字节一般填 0PC号1字节一般填 0xFFIO编号2字节目标模块编号通常填 0x03FF站号1字节一般填 0数据长度2字节从监视定时器开始到数据段结束的字节数监视定时器2字节0x0000表示无限制也可以设具体超时时间指令2字节功能码读是0401写是0402子指令2字节一般填0000数据段变长起始地址、点数、或要写入的数据CRC2字节二进制模式为CRC-16校验举个例子读D100开始的10个字二进制帧数据段需要放“起始地址0100等待”不对这里要说明一下三菱软元件编号在MC协议报文里是直接用十进制编号的十六进制值D100就是0x0064。所以读D100、10个字的请求帧数据段是0064 000A。整帧拼出来大概是D0 D0 00 0F 00 FF 03 FF 00 00 0A 00 00 04 01 00 00 00 64 00 0A最后再跟两个字节的CRC。2.2 ASCII帧和二进制帧怎么选MC协议的3E帧还有传输形式的区别ASCII形式和二进制形式。ASCII形式整个报文用十六进制字符表示比如二进制里的D0 D0ASCII形式就是字符串F9F9。两者指令含义相同但报文长度差一倍调试便利性也差很多。从我的实践来说调试阶段强烈建议先用ASCII帧配合网络调试助手手动发送因为你看到的报文就是一行能读懂的字符串哪里少了字符、哪里校验不对一目了然。稳定之后如果机器人侧用C#插件或者高级语言处理数据再换成二进制帧效率会好一些。但如果你和我一样早期在爱普生机器人上用SPEL语言直接处理报文我反而建议一直用ASCII帧因为SPEL的字符串处理函数确实方便后面第4章会说这个原因。2.3 核心指令0401和0402就够大部分场景MC协议的指令很多但做设备数据交换绝大多数场景只用两个0401批量读取0402批量写入。0401既能读字软元件如D区、W区也能读位软元件如M区、X区。稍微需要注意的地方是读位软元件时三菱PLC是按照16个位打包成一个字来返回的。假设你读M0到M15这16个点它返回一个字数哪怕你只读M3这一个点返回的仍然是一个字M3对应返回值的bit3实际程序里还要做一次位判断。另外一个老生常谈的细节X和Y的编号是三菱习惯的八进制M和D是十进制在报文里填地址的时候要按PLC侧的软元件编号规则来别把PLC里看到的编号直接抄进报文。3. 驱动封装的骨架连接、构造、解析三块各司其职3.1 连接管理比报文构造更容易翻车报文构造虽然是整个驱动里看起来最“技术”的部分但真正常出问题的反而是连接管理。PLC侧作为TCP服务器同一时刻能够接受的连接数有限有些模块甚至严格到只允许一两路。所以机器人侧的驱动必须使用长连接不能每次读写都重新建立Socket。一个典型的现场场景是机器人每几十毫秒就要读一次PLC数据表如果每次都新建连接很快PLC就会拒绝服务。我自己的驱动里连接模块单独维护一个TcpClient实例加一个互斥锁保证同一时刻只有一个请求在途。因为MC协议是请求应答式如果你在同一个连接上并发发多个请求响应回来时根本对应不上是哪一条。断线重连也要做但要有退避策略比如断开后先等500ms再重连连续失败就逐步拉长到几秒避免PLC一恢复就收到一堆连接请求。3.2 一个可用的C#报文构造器下面这段C#代码是我驱动里的核心构造逻辑按3E帧二进制格式生成批量读请求。需要说明的是这里实现了字软元件D区等的读写这也是设备数据交换的绝大多数场景。public static class McFrameBuilder { // 构造3E二进制帧批量读取字软元件指令0401 public static byte[] BuildReadWordFrame(ushort startAddr, ushort count, ushort wdt 0x0000) { Listbyte frame new Listbyte(); // 子头二进制模式固定 D0 D0 frame.Add(0xD0); frame.Add(0xD0); // 请求长度占位稍后回填 int lenPos frame.Count; frame.Add(0x00); frame.Add(0x00); // 网络号、PC号、IO编号、站号 frame.Add(0x00); // 网络号 frame.Add(0xFF); // PC号 frame.Add(0x03); // IO编号高字节 frame.Add(0xFF); // IO编号低字节 frame.Add(0x00); // 站号 // 数据长度占位稍后回填 int dataLenPos frame.Count; frame.Add(0x00); frame.Add(0x00); // 监视定时器 frame.Add((byte)(wdt 8)); frame.Add((byte)(wdt 0xFF)); // 指令0401子指令0000 frame.Add(0x04); frame.Add(0x01); frame.Add(0x00); frame.Add(0x00); // 数据段起始地址 点数均按字 frame.Add((byte)(startAddr 8)); frame.Add((byte)(startAddr 0xFF)); frame.Add((byte)(count 8)); frame.Add((byte)(count 0xFF)); // 回填长度字段 int dataLen frame.Count - dataLenPos - 2; int reqLen frame.Count - lenPos - 2; frame[lenPos] (byte)(reqLen 8); frame[lenPos 1] (byte)(reqLen 0xFF); frame[dataLenPos] (byte)(dataLen 8); frame[dataLenPos 1] (byte)(dataLen 0xFF); // CRC-16校验范围从子头后开始 ushort crc CalcCRC16(frame, 2, frame.Count - 2); frame.Add((byte)(crc 8)); frame.Add((byte)(crc 0xFF)); return frame.ToArray(); } private static ushort CalcCRC16(byte[] data, int offset, int len) { ushort crc 0x0000; for (int i offset; i offset len; i) { crc ^ (ushort)(data[i] 8); for (int j 0; j 8; j) { if ((crc 0x8000) ! 0) crc (ushort)((crc 1) ^ 0x1021); else crc (ushort)(crc 1); } } return crc; } }注意CRC计算这个细节。上面代码的校验范围写成“从子头后开始”也就是从长度字段开始到数据段结束这是我实测可用的一种做法。如果你在现场遇到网络调试助手能通、程序不通的情况第一个要排查的就是这个范围是否和PLC参数设置一致。批量写入的帧跟读取基本一致指令换成0402数据段里除了起始地址、点数还要把每个要写入的字的数据依次排列。注意MC协议报文里一个字的高字节在前、低字节在后。3.3 响应解析与接收边界请求发出去之后剩下的就是等响应。响应帧结构和请求帧非常像只是在指令之后多了两个字节的结束代码然后才是真正的数据区。结束代码为0x0000表示正常其他值基本都是错误信息。下面是一个简化版的响应解析方法public static ushort[] ParseReadWordFrame(byte[] response) { if (response.Length 15) throw new Exception(响应帧过短); // 结束代码在指令之后按3E帧的字段顺序取 int endCode (response[13] 8) | response[14]; if (endCode ! 0) throw new Exception($PLC返回结束代码 0x{endCode:X4}); // 数据长度字段指令2字节 结束代码2字节 数据区 int dataLenField (response[9] 8) | response[10]; int dataBytes dataLenField - 4; if (dataBytes 0) throw new Exception(数据长度异常); ushort[] values new ushort[dataBytes / 2]; for (int i 0; i values.Length; i) values[i] (ushort)((response[15 i * 2] 8) | response[15 i * 2 1]); return values; }响应接收最容易踩的坑是TCP粘包。PLC返回的响应可能会被拆成多个TCP段到达也可能多个响应粘在一起到达。所以不能一收到数据就立刻当完整帧解析。我的做法是先接收数据放到缓冲区然后从缓冲区头部解析出响应长度字段如果缓冲区剩余长度不够就继续接收直到收满一整帧为止。这个逻辑写在Receive函数里别的模块完全感知不到包边界的存在。4. 爱普生机器人侧如何接入这套驱动4.1 两条路线SPEL函数库还是C#插件爱普生机器人侧接入这套驱动实际有两条路线各有各的道理。第一条是直接在EPSON RC的SPEL语言里写成函数库。好处是整个项目不依赖外部PC机器人和PLC直接组网就能独立运行符合很多出厂设备的独立性要求。缺点也很明显SPEL处理二进制数据不舒服没有原生字节数组字符串函数又不擅长拼二进制帧所以在这条路线上建议使用ASCII帧把报文当成普通字符串来处理。第二条是用EPSON RC 7.0以后支持的C#插件方式在PC端写一个类库把协议驱动封装成公开方法再供SPEL任务调用。优点是调试方便数据结构处理能力强日志系统也好做适合开发期反复验证。缺点是运行时依赖PC主机环境如果客户现场不允许带工控机这条路线就不太现实。4.2 SPEL里的实测写法ASCII帧我自己在SPEL里实际用的是ASCII帧核心思路是先把报文手动拼成字符串再调用Socket发送函数。以读取D100开始的10个字为例ASCII请求帧字符串大致是F9F9 000F 0000 FF 03FF 00 000A 0000 0401 0000 0064 000A 校验码注意ASCII帧每个二进制字节都用两个十六进制字符表示所以子头变成了F9F9长度字段也按ASCII字符传输但长度值本身仍然是二进制的字节数。校验码则是对整帧字符串里每个字符的ASCII码做累加取低8位再转成两位十六进制字符。SPEL里可以用Asc()函数逐个字符取码值来累加逻辑不复杂。发送和接收在SPEL里走的是Socket指令不同版本RC的函数名略有差异大概是NetSocketOpen、NetSocketSend、NetSocketReceive这一族。核心流程是打开Socket连接PLC的IP和端口把拼好的字符串发过去然后循环接收直到拿到完整响应。如果担心粘包SPEL这边要自己判断响应长度字段收够字节再往下走。因为SPEL缺少天然的字节数组解析响应数据段时我一般直接把十六进制字符串成对拆开用Val(H Mid$(resp$, ...))转成整数。写一个CRC校验函数、一个报文拼接函数、一个响应解析函数机器人主任务里调用起来就很简单了。4.3 与机器人任务的配合方式驱动写好后机器人侧的使用方式很简单。我在主任务里维护一个全局数组比如DData专门存放从PLC读回的数据表然后在机器人需要数据的节点调用读取函数刷新数组。举一个实际例子产线上位机通过触摸屏把不同型号产品的放置坐标写入PLC的D100到D110区域机器人每次抓取前先读这一段数据再根据产品型号选择对应的放置目标。这里有一个非常容易忽略的时序问题写完PLC的D区后立刻去读读回来的很可能是旧值。因为PLC程序是循环扫描的写入寄存器只是把值放进内存PLC程序还没有来得及把它搬运到你实际业务逻辑用到的位置。我的做法是在PLC程序里设置一个“数据版本号”机器人写入数据后把版本号加一PLC处理完业务后把版本号回写到另一个寄存器机器人判断版本号处理完成之后再继续动作。用这种握手方式规避PLC扫描周期带来的数据同步问题。5. 现场运行半年后我整理出的六个典型坑5.1 一次读取的字数上限MC协议3E帧的批量读取一次最多读960个字左右具体数值取决于PLC模块和帧长度限制。我第一次测试时图省事想一次把D区里2000个字全部读回来结果PLC直接返回错误结束代码。解决办法是分块读取比如每次读64个字循环读完所有块然后本地拼接。实际项目里也不要一味追求单次读最大单帧太长会占用PLC大量处理时间还会影响其他通信任务的实时性。5.2 粘包和半包的应对这个问题在第3章提过但值得再说一遍。在爱普生机器人侧用SPEL的时候因为字符串接收是一次性拿一块数据更容易踩半包和粘包。我自己遇到过的真实情况是响应帧长度有200多个字符但一次NetSocketReceive只到了80个字符程序如果不做“等够长度再解析”直接截取结果就是解析出乱码。后来我统一改成循环接收累积到变量里直到长度满足响应帧里的长度字段再加校验码长度才继续往下走。5.3 位软元件读取的“一字16位”问题读M区、X区这类位软元件MC协议返回的是一组16位打包数据。比如你读M0到M15这16个点返回一个字bit0就是M0的状态bit1是M1以此类推。但如果只读M5一个点PLC会返回M5所在字的一整组16位状态你需要自己判断bit5。这个特性在写程序时很容易想当然以为读1个点就返回1个bit实际上不是。建议在封装驱动时就固定好对位软元件的读取函数统一返回整数数组让上层去判断位。5.4 大小端顺序的坑MC协议二进制帧里一个16位数值在数据段中是高字节在前、低字节在后。比如D100里的值是0x1234在报文中显示为12 34。这个跟很多人的直觉相反因为TCP/IP网络序也是大端但这个“大端”应用在寄存器值上时如果你在解析时按小端顺序转换读回来的所有数据就全部错位。判断这个坑最快的方法是用一个已知值去测试比如先在PLC里把D100设为1234然后看解析出来的值对不对。5.5 监视定时器和超时设置报文里的监视定时器字段很多人直接填0x0000表示无限等待。但实际现场不可控机器人读数据时PLC可能会因为通信繁忙而延迟响应如果无限等待驱动就会一直阻塞在那。建议给每个请求设置合理的超时时间比如500到1000毫秒超时后重试一次不行就报故障。另外监视定时器字段也可以配合PLC侧参数使用比如设0x0006表示6秒这里不同PLC对数值单位有差异具体以PLC手册为准原则是“不要把自己的Socket超时设成无限”。5.6 掉线重连与PLC侧连接数量限制现场设备运行中最烦人的问题之一就是PLC断电重启或者程序在线修改后机器人侧Socket连接会断开并且PLC可能暂时不接受新连接。处理方式不复杂但一定不能省驱动要监听连接状态断开后自动重连重连间隔要退避另外不要在断线期间反复快速重试。还有一点确保整个系统只建立一条连接不要因为机器人有多个任务就各开一个Socket很多PLC对同一来源的并发连接数量很敏感。6. 做这件事需要哪些基础以及我的建议学习路径6.1 硬技能清单很多人看到“自行封装驱动程序”就被吓住了其实它要求的基础没有想象中那么高。我自己整理了一份清单按重要性排技能项需要掌握到什么程度PLC基础知道D区、M区、X/Y这些软元件概念能在GX Works里设置以太网参数、开放MC协议端口TCP/IP基础明白Socket、端口、请求应答、粘包和超时的含义编程能力C#、Python或其他语言至少能写Socket程序如果走SPEL路线还要熟悉函数、数组、字符串处理调试工具会用网络调试助手手动发包验证会看PLC软元件监视表最好会一点抓包工具6.2 建议的学习路径如果你现在还没动手我建议严格按照这个顺序走能少走很多弯路先用网络调试助手手动连接PLC的IP和端口自己拼一条最简单的ASCII帧读取PLC里一个已知的D值。这一步能帮你确认PLC侧的端口、角色、帧校验方式都配置正确也能直观理解报文结构。在电脑上用Python或C#写一个最小Demo程序方式完成一次读取。通过这个Demo把请求帧构造、响应解析、CRC校验这几个基础逻辑跑通。再把连接管理、粘包处理、断线重连这些工程化内容加进去形成一个可复用的类库。最后才接到爱普生机器人上。可以先从一个按钮触发一次读取开始再到任务轮询最后再上握手逻辑和生产逻辑。6.3 什么情况下不值得自己写自己写驱动确实灵活但它不是万能的也不是所有场景都值得。我的建议是如果项目就只有十几个点位IO硬接线反而更可靠如果客户现场已经买了网关而且点位数量在网关的舒适区内用网关也不是不行如果项目周期紧你又是第一次接触MC协议那我建议至少留出两到三天的调试余量否则别轻易尝试在交付现场边装边调。反过来如果你的项目注定要传输坐标值、配方表、批次统计这类批量数据而且以后大概率还会扩展点位那我强烈建议把驱动类库沉淀下来。一次投入后面所有项目复用收益会越来越高。我自己的体会是这套驱动真正带来的不是省了网关那几千块钱而是把数据通道的每个环节都攥在自己手里。最后分享一个小技巧在驱动里加一个日志开关平时关掉现场出问题时打开就能看到每个时刻机器人发了什么、PLC回了什么。这个能力在客户现场排查问题时比任何远程工具都好用。