前一阵在给一条装配线做上位机改造,遇到一个很典型的场景:爱普生机器人要把当前坐标、抓取结果写到三菱PLC里,同时读PLC侧的下发型号、允许启动信号。现场是老产线,不想加总线模块,机器人控制器和三菱PLC都有以太网口,最省事的方案就是让机器人/上位机直接走以太网,按三菱MC协议跟PLC通话。市面上的三菱通信库不是收费就是黑盒,出了问题根本不知道报文错在哪。最后我干脆自己用Socket封装了一套基于MC协议3E帧的通信驱动,把D区读写、M点位读写、32位数据和浮点数传输都做了进去。这篇文章把封装思路、MC协议报文拆解、源码实现、编程基础要求和排障经验完整写出来,给正在做机器人集成或工控上位机开发的同行做个参考。
1. 项目背景与方案选型
1.1 为什么自研通信驱动
先说结论:如果项目只用一次、PLC侧点位不超过一二十个、时间又紧,买现成控件确实省事。但我这次选择自己封装,核心原因有三个。
第一是部署环境太乱。三菱官方的通信组件往往带着一堆运行库和授权机制,装到客户工控机上经常跟杀毒软件、其他组件的版本冲突,现场越急越装不稳。自己封装只有一层TCP Socket,只要机器人或工控机能ping通PLC,驱动就能跑。
第二是黑盒问题。现成库给你一个ReadD()方法,但你不知道它内部是不是有隐藏的重试、超时、协议差异。一旦客户现场出现偶发性通信失败,你连排查的抓手都没有。自己封装之后,每一帧报文都能打印出来,哪里不对一眼就能看到。
第三是定制需求。这个项目里PLC侧同时用了D寄存器(字数据)和M点位(位数据),还要读32位浮点数,而且机器人侧有时候要主动发起批量读取。现成库适合标准场景,一旦要扩展就很别扭。自己封装完全可以按项目点位表定制接口,后续换PLC型号也只改一个地址映射文件。
当然,自研也不是没有代价。你需要花时间啃三菱MC协议手册,需要处理TCP粘包、超时重试、断线重连这些底层问题。这篇文章的价值就是把这些坑提前告诉你,帮你把三天的踩坑时间压缩到半天。
1.2 总体架构:机器人如何与PLC互通
这个项目里有两套系统需要打通:爱普生机器人控制器和一台三菱FX5U系列PLC(FX5U内置以太网口)。两者通过以太网直连,走的是TCP/IP协议,应用层使用三菱MC协议。
一个容易搞混的点是:爱普生机器人控制器并没有直接提供“读取三菱D寄存器”的指令,它只是提供了TCP Socket通信能力。真正要让数据互通,必须由你在Socket之上按MC协议组织报文。好比打电话,机器人和PLC之间是打通了,但双方说的语言(报文格式)需要你来翻译。
整体架构是这样的:
- 物理链路:爱普生控制器 <—— 网线/交换机 ——> 三菱PLC以太网口
- TCP连接:由客户端主动连接PLC的开放端口(默认6000)
- 应用协议:TCP承载三菱MC协议3E帧(二进制格式)
- 业务接口:封装成ReadD(startAddr, count)、WriteWord(addr, value)、ReadBit等函数
这里要特别说明一下“谁是客户端”。三菱PLC的以太网模块通常工作在被动监听模式,也就是等待外部设备来连接。所以机器人控制器或上位机是TCP客户端,主动发起连接。不要把角色搞反,否则PLC侧连接永远建立不起来。
如果项目里还有MES系统、视觉系统要协调,我建议不要在爱普生RC+软件里写太多通信逻辑,而是让一台上位机(工控机)来做统一的数据中枢,机器人通过开放自己的TCP协议跟工控机交互。这样驱动可以复用,也方便后续扩展。
1.3 编程语言与封装形态的选择
驱动封装形态我见过三种常见做法,各有适用场景:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 在爱普生RC+中用SPEL+语言直接写Socket通信 | 不依赖额外硬件,机器人独立完成数据交互 | SPEL+的字符串和字节处理能力偏弱,复杂逻辑比较痛苦 | 项目简单,只有机器人+PLC |
| 上位机用C#封装驱动,机器人通过TCP或IO与上位机交互 | 代码好维护,调试方便,日志能力强,可复用 | 多一台工控机,增加一个故障点 | 产线上本来就有上位机,或需要连MES |
| 用三菱官方通信组件/第三方控件 | 开发快,代码量少 | 授权、部署、黑盒问题多 | 快速原型验证,不追求长期维护 |
我这次选的是“上位机C#封装驱动 + 爱普生机器人通过Socket与上位机交换数据”的方案。原因很简单:产线上还有扫码枪和MES交互,上位机本来就在,把PLC通信放上位机里比放在机器人里更顺手。C#处理byte数组、写日志、做重试也比SPEL+舒服得多。
后面代码讲解我也以C#为例,但报文构建的原理完全不限语言,用Python、Java、甚至自己写SPEL+都一个套路。
2. MC协议报文级拆解:先把协议啃透
2.1 你需要了解的第一件事:3E帧还是4E帧
三菱MC协议根据物理链路和帧结构分了好几种。常见的以太网通信有4E帧和3E帧两种。
- 4E帧:早期用于Q系列PLC,带有“请求目标模块”信息,报文里要填CPU编号,结构相对复杂。
- 3E帧:当前主流,适用于多数新型号PLC,报文结构更简洁。3E帧又分二进制格式和ASCII格式两种。
我做这个项目直接用3E帧二进制格式。原因很实际:二进制格式报文短、解析快、代码容易写。ASCII格式会把每个字节拆成两个可见字符,数据量翻倍,调试看起来反而更费眼睛。
如果你手头PLC是Q系列老模块,或者在使用串口通信,帧结构会有差别。但只要你跑的是以太网、PLC型号是近五六年内的,优先用3E帧二进制格式通常没问题。发现连不上或回包格式不对时,第一件事就是去查这台PLC对应型号的手册,确认它支持的帧类型。
2.2 二进制格式3E帧请求报文逐字节解读
MC协议报文看着吓人,拆开了其实就是一层又一层的字段拼起来。以“读取D100开始的2个字寄存器”为例,我把实测报文贴出来:
50 00 00 FF FF FF FF 0D 00 0A 00 01 04 00 00 64 00 00 00 02 00 A8这段报文一共22个字节。按顺序拆解:
| 字段 | 字节内容 | 长度 | 说明 |
|---|---|---|---|
| 帧头 | 50 00 00 | 3 | 3E帧固定帧头,用于识别帧开始 |
| 请求目标模块I/O号 | FF FF | 2 | 开放通信时固定为FF FF |
| 请求目标模块站号 | FF FF | 2 | 开放通信时固定为FF FF |
| 请求数据长度 | 0D 00 | 2 | 后续请求数据的字节数,低字节在前,0D 00即13 |
| 监视定时器 | 0A 00 | 2 | 2字节小端,代表10个250ms单位,即2.5秒 |
| 命令 | 01 04 | 2 | 0401,字读取命令,注意低字节在前是01 04 |
| 子命令 | 00 00 | 2 | 0000,连续访问/批量读取子命令 |
| 起始软元件编号 | 64 00 00 00 | 4 | 小端表示,0x64即十进制100,对应D100 |
| 软元件点数 | 02 00 | 2 | 读取2个连续的D寄存器 |
| 软元件类型代码 | A8 | 1 | A8代表D寄存器(拆线存储器) |
看到这里你应该明白了:所谓MC协议报文,本质上就是“你要干什么(命令)+ 操作哪里(地址)+ 操作多少(点数)+ 数据(如果是写操作)”,全部用固定字节表示。
这里有两个特别容易翻车的地方。
第一,字节序一定要用小端。比如命令码0401在报文中是01 04,地址100在报文中是64 00 00 00,不是00 00 00 64。很多人第一次写的时候按习惯从左到右写,结果PLC直接不回包或回错误码。
第二,请求数据长度字段的统计口径要确认。我这个例子里它统计的是“监视定时器开始到设备代码结束”这13字节。但是不同PLC型号、不同手册里,这个长度字段可能把前面的请求目标模块I/O号和站号也算进去,甚至把长度字段自身都算进去。最保险的做法是先用网络调试助手或PLC仿真软件发一遍、验证一次,再写进代码。代码里这个长度也应当是程序自动计算的,不要手写死。
2.3 响应报文结构与结束码识别
请求发出后,PLC会给一个响应帧。正常响应同样以3E帧格式返回,固定头部结构与请求类似。关键区别在于请求数据部分:
50 00 00 FF FF FF FF 0E 00 00 00 01 04 00 00 12 34 56 78 9A BC拆解响应数据部分:
- 结束码(2字节):
00 00表示正常。如果PLC认为请求非法,这里会返回非零错误码。 - 命令(2字节):回显请求命令,如0104。
- 子命令(2字节):回显请求子命令。
- 响应数据(后续字节):读取到的数据,按字为单位,低字节在前。比如读回来两个字,就是四个字节:第一个字的低字节、高字节、第二个字的低字节、高字节。
解析响应的核心逻辑只有一条:先确认结束码为0,再往后取数据;如果结束码非0,立刻把错误码记进日志,不要继续解析数据。
错误码处理是很多人容易偷懒的地方。你在现场遇到过“PLC姓张”这种诡异问题再回头看,就知道错误码日志多重要了。我习惯在驱动里维护一张常见错误码映射表,比如:
| 结束码 | 含义 | 处理建议 |
|---|---|---|
| 0000 | 正常完成 | 继续解析 |
| C050 | 请求帧格式错误 | 检查报文长度、字段边界 |
| C051 | 请求数据长度错误 | 重点核对长度字段统计口径 |
| C052 | 不支持的软元件类型 | 查设备代码表 |
| C055 | 地址越界 | 确认软元件编号和点数范围 |
| C05F | 写入点位不可用/被保护 | 检查PLC程序对该软元件的访问权限 |
这些错误码并非所有PLC型号都完全一致,实际项目中以PLC手册为准,但它能帮你快速缩小问题范围。
2.4 常用命令、设备代码与地址换算
MC协议命令很多,但工业项目里真正高频用到的就那么几个。我做驱动时只实现了四类,完全够用:
| 功能 | 命令码 | 子命令码 | 说明 |
|---|---|---|---|
| 字读取 | 0401 | 0000 | 读取连续D、W、R等字软元件 |
| 字写入 | 0402 | 0000 | 写入连续字软元件 |
| 位读取 | 0401 | 0001 | 读取M、X、Y等位软元件 |
| 位写入 | 1401 | 0001 | 写入M、X、Y等位软元件 |
对了,位读取的子命令和字读取不一样,这一点很容易被忽略。位读取命令虽然也是0401,但子命令要填0001,请求数据段里还需要额外指定“位地址偏移”。示例:读取M100这一个位,报文请求数据部分为:
- 监视定时器:0A 00
- 命令:01 04
- 子命令:01 00
- 起始软元件编号:64 00 00 00(M100的编号是100)
- 软元件点数:01 00
- 软元件类型代码:90(M寄存器)
注意设备代码不是随便填的。三菱MC协议里每种软元件有固定代码,我整理常用的几个:
| 软元件 | 设备代码(十六进制) | 说明 |
|---|---|---|
| D | A8 | 数据寄存器,字访问 |
| M | 90 | 内部继电器,位访问 |
| X | 9C | 输入继电器,位访问 |
| Y | 9D | 输出继电器,位访问 |
| SM | 91 | 特殊继电器 |
| SD | A9 | 特殊数据寄存器 |
| W | B4 | 链接寄存器 |
| R | AF | 文件寄存器 |
地址换算也是绕不过的一关。D寄存器的地址就是实际编号,D100的软元件编号就是100(十六进制0x64)。但X和Y的地址是按十六进制编号的,而且带块号,比如Y40其实是十六进制的Y40,对应软元件编号0x40即十进制64。如果点位表写的是Y40,你直接传“40”就是错的,要传64。所以驱动里我专门放了一个ConvertDeviceAddr函数,统一处理这种进制换算,避免写点位表时人工换算出错。
3. 驱动源码实现:类库设计与会话管理
3.1 核心类McClient的设计
驱动我封装成一个独立的类,核心职责就三块:TCP连接管理、报文构建与解析、重试与异常处理。
先上类的核心结构:
public class McClient { private Socket _socket; private readonly object _syncObj = new object(); private readonly int _timeoutMs = 3000; /// <summary>连接PLC以太网口</summary> public bool Connect(string ip, int port = 6000) { try { _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _socket.NoDelay = true; // 禁用Nagle算法,降低小报文延迟 _socket.ReceiveTimeout = _timeoutMs; _socket.SendTimeout = _timeoutMs; _socket.Connect(ip, port); return true; } catch (SocketException ex) { Log($"连接失败: {ex.Message}"); return false; } } public void Close() { try { _socket?.Shutdown(SocketShutdown.Both); } catch { } _socket?.Close(); _socket = null; } }连接成功只是第一步。工业通信讲究稳定,我建议你在连接建立后立刻做一次“握手测试”——最简单的办法是读取PLC的一个特殊寄存器或自己定义的运行状态字,确认连接通了、协议字段没问题,再做业务读写。这能帮你把“连接假成功”和“协议不匹配”两类问题一次性暴露出来。
类里所有读写操作都用一个_syncObj锁串行化。你可能觉得多线程并发读写效率更高,但PLC这种工业设备并不适合高并发轮询,串行操作配合批量读取已经足够快,还能避免报文交错的灾难。
3.2 报文构建、发送与粘包处理
报文构建是整个驱动最核心的地方。我把它拆成了“构建请求体”和“发送接收”两步。构建请求体,以字读取为例:
private byte[] BuildReadWordFrame(int startAddr, ushort count, byte deviceCode) { var body = new List<byte>(); // 监视定时器:0A 00 表示2.5秒(10 * 250ms) body.Add(0x0A); body.Add(0x00); // 命令:0104 body.Add(0x01); body.Add(0x04); // 子命令:0000 body.Add(0x00); body.Add(0x00); // 起始软元件编号(小端4字节) body.AddRange(BitConverter.GetBytes((uint)startAddr)); // 软元件点数(小端2字节) body.AddRange(BitConverter.GetBytes(count)); // 软元件类型代码 body.Add(deviceCode); // 组包:帧头 + 目标IO + 目标站 + 长度 + body var frame = new List<byte>(); frame.Add(0x50); frame.Add(0x00); frame.Add(0x00); frame.Add(0xFF); frame.Add(0xFF); frame.Add(0xFF); frame.Add(0xFF); frame.AddRange(BitConverter.GetBytes((ushort)body.Count)); frame.AddRange(body); return frame.ToArray(); }代码里有个细节:起始地址和点数都用BitConverter.GetBytes生成小端字节,不用手动倒字节顺序,省心也不容易错。设备代码在写调用的时候根据点位类型传入,一个方法通吃D、M、X、Y。
发送和接收我单独抽了一个SendReceive方法,专门处理TCP流式传输带来的“粘包/半包”问题。工业TCP通信经常踩这个坑:一次Recv收到的可能不止一帧,也可能只收到半帧。所以接收必须按“期望长度”循环收,直到收够为止。
private byte[] SendReceive(byte[] request) { lock (_syncObj) { try { _socket.Send(request); } catch (SocketException ex) { Log($"发送异常: {ex.Message}"); throw new McCommunicationException("发送失败", ex); } // 先接收固定头部9字节:50 00 00 + FF FF + FF FF + 长度2 var header = ReceiveExactly(9); // 从头部第7、8字节取出请求数据长度(小端) ushort bodyLen = BitConverter.ToUInt16(header, 7); var body = ReceiveExactly(bodyLen); return header.Concat(body).ToArray(); } } private byte[] ReceiveExactly(int length) { var buffer = new byte[length]; int received = 0; while (received < length) { int n = _socket.Receive(buffer, received, length - received, SocketFlags.None); if (n <= 0) throw new McCommunicationException("连接被对端关闭"); received += n; } return buffer; }解析响应时,从头部偏移9字节开始读结束码,读出来非0就抛异常,让调用方知道通信本身通了但PLC拒绝了请求。这类异常和网络异常要区分开,业务层才能决定是重试还是提示报警。
3.3 地址换算与32位数据/浮点数读写
点位表里经常出现“D100是32位数据、D200是浮点数”这种规划。MC协议读回来的数据默认按字(16位)一个个排列,要自己拼32位。
比如D100和D101两个寄存器通过一次字读取读回来,返回4字节:12 34 56 78。它们代表的顺序是D100=0x3412,D101=0x7856,也就是低字节在前。拼成一个32位整数时,D100对应低位字,D101对应高位字。
public int ReadDint(int lowAddr) { // 读取两个连续字:lowAddr和lowAddr+1 ushort[] words = ReadWords(lowAddr, 2); int low = words[0]; int high = words[1]; return (high << 16) | low; }浮点数同理,但不同项目的字节排列习惯可能不一样。我遇过两种:一种是西门子风格,高字在前;三菱PLC默认拼接后,低字在前、低字节在前。正确做法是先用已知数值做联调,在PLC里写一个固定的浮点数(比如1.5),然后驱动读回来跟预期比对,确认字节顺序后再全面开发。千万别假设,一定要实测。
这个经验明确写进代码注释里:
public float ReadFloat(int lowAddr) { ushort[] words = ReadWords(lowAddr, 2); uint bits = ((uint)words[1] << 16) | words[0]; return BitConverter.ToSingle(BitConverter.GetBytes(bits), 0); }32位数据读取的边界问题也要留意。如果D100~D101已经组成了32位数据,下一个32位数据应该从D102开始,而不是D101。点位表规划时最好把同类数据连续排列,读起来高效也省心。
3.4 超时重试与断线重连策略
通信驱动的价值不在“能通”,而在“断了能恢复”。我设计的重试策略很简单但有层次:
- 单帧超时重试:单次请求超过3秒无响应,重试两次,间隔200ms。超过重试次数则判定连接失效。
- 断线重连:收到Socket异常或者重试失败,关闭旧连接,按指数退避重连(1秒、2秒、4秒,最高30秒),一旦重连成功,先做握手测试,再做业务读写。
- 看门狗机制:业务空闲时,周期性(比如30秒)读一次PLC侧心跳字。心跳字由PLC程序或驱动自身维护,确保链路一直是通的。
实现重连代码时,有一个非常关键的点:所有业务线程读取失败时不能自己无限重连,否则多线程会同时抢着重建连接,造成连接风暴。我采用的做法是每一次底层操作失败都调用统一的重连入口,由它内部加锁、判断当前连接是否已重建,再决定要不要继续重试。
上面这套策略的代码量不小,但稳定性的提升非常明显。这个驱动在产线上连续跑了两三个月,遇到PLC重启、网线松动、交换机断电,都能在最短时间内自动恢复,没有一次需要工人手动重启上位机。
4. 做这个项目需要哪些编程基础
4.1 必备基础点清单
每次有人问“我也想做类似的开发,需要学什么”,我都是这么答的——你不需要等到把网络编程全学完再动手,但你至少要具备下面几个基础点,否则遇到问题连排查方向都没有。
第一是Socket编程基础。TCP是流式传输,不是发一次收一次,必须懂“粘包”和“半包”。你至少要会建立一个TCP连接、发送字节流、循环接收数据。这个基础不过关,你写的驱动能跑通最常见的短报文,但一旦报文一长就会偶发丢数据。
第二是字节序和进制转换。MC协议到处是低字节在前,地址有的是十进制、有的是十六进制。如果你不善长把十六进制报文和十进制地址来回换算,调试时会极其痛苦。建议先做几道练习:0xA8转十进制是多少(168)、小端表示的0x0401在字节流里长什么样(01 04),能做到闭眼写出来才算过关。
第三是PLC软元件的基本概念。你要清楚D寄存器存字数据、M继电器存位数据、X是输入、Y是输出,要看得懂I/O分配表。不需要你会写梯形图,但要能在PLC程序里找到通信区域在哪里、被谁读写。
第四是文档查阅能力。三菱MC协议手册和爱普生机器人通信手册都是几百页的PDF,你要能在里面快速找到“3E帧格式”“设备代码表”“错误代码表”。这个能力比背熟协议结构更重要,因为型号不同细节就会变。
4.2 零基础起步的推荐学习路线
如果你是刚入行、基础还薄弱,我给一条踩过坑的经验路线:
- 先把一组报文跑通:找一台PLC或用PLC仿真软件,用网络调试助手直接发“读取D100”的原始报文,观察回包。这一步可以不写代码,但能让你建立对协议最直接的体感。
- 写一个固定功能的代码:不用一开始设计大而全的驱动,先用C#或Python写一个“连接PLC、读取D100一个地址、打印结果”的最小程序。注意不要在界面卡住,就用控制台。
- 把读写做成函数:把报文构建、发送、接收、解析拆成独立函数。这时候你自然会遇到超时处理和粘包问题,逐个解决。
- 再扩展到批量读写和位读写:批量读能极大提高性能,位读写能覆盖M、X、Y点位。完成这一步,你已经能应付七成产线需求了。
- 最后补上日志和异常恢复:把每一帧发送和接收报文以十六进制写入日志,补上断线重连。这两点是现场排查故障的保命技能。
这个路线最大的特点是“小步快跑”,每一步成果都可以立刻验证,不会让你在学协议阶段就信心受挫。
4.3 实操调试三板斧:抓包、日志、最小复现
在自己封装驱动的过程中,你会遇到大量“为什么PLC不回包”“为什么数据不对”的问题。我调试时固定用三板斧,屡试不爽。
第一斧:抓包。用Wireshark抓TCP端口6000的报文,直接看十六进制载荷。抓包能告诉你“请求到底发没发出去”“PLC到底回了什么”。很多时候代码里看半天,一抓包发现请求帧根本没离开本机,问题出在Socket发送逻辑,而不是协议。
第二斧:日志留痕。驱动里一定要有开关控制的报文日志。发送的每一帧、接收的每一帧,按十六进制字符串打出来。有日志的驱动和没日志的驱动,现场排查时间差五倍以上。我习惯把点表配置、地址换算结果、错误码一起打进去,故障复盘时非常直观。
第三斧:最小复现。协议问题最怕跟业务逻辑搅在一起。遇到疑难,我用网络调试助手手工构造一个最小报文,直接发给PLC,绕过自己的驱动,确认PLC侧行为。如果手工报文都失败,那就不是驱动的问题,而是点位表或PLC配置的问题;如果手工报文成功,那就是代码逻辑问题,回去对照代码和手工报文逐字节找差异。
这三板斧配合得好,大部分通信故障能在半小时内定位到具体层。
5. 排障实录:三个真实故障复盘
5.1 读请求发出去PLC就是不回包
现象:用上位机连PLC,TCP连接建立成功,发送读取D100的请求,但等不到任何响应,直到超时。
排查过程:先用抓包确认请求帧已经完整发出,而且TCP层已收到对方的ACK。这说明物理链路和TCP会话都没问题,问题出在PLC对请求内容不认可。
对照手工报文逐字节检查,发现请求数据长度字段填的是整个报文的总长(0x0016),而按照当前PLC型号的手册,这个字段应当填写“监视定时器之后的数据体长度”(0x000D)。PLC因为长度解读错了直接丢帧,连错误码都没给。
这个案例告诉我们,长度字段口径错了表现为“无响应”,不是“错误码”,排查起来最棘手。所以代码里长度一定要由构建逻辑现场计算,并且保留一个开关可以打印最终报文,做联调时跟手册核对一次。
5.2 长时间运行后TCP连接悄悄断开
现象:驱动刚启动时一切正常,运行几个小时或一个晚上后,第一次读写失败,重连后又恢复正常。
排查过程:首先排除了业务代码死循环,因为故障时间点与业务无关。抓包发现TCP连接在空闲一段时间后既没收到FIN包也没收到RST包,像是被中间设备或PLC侧静默回收了。
进一步查看PLC以太网模块参数发现,它对空闲连接有自动断开策略,且同一时间只允许有限个连接。之前手动调试时又额外建立过连接,挤占了有效的连接槽位。
解决方法是两件事:一是在驱动里增加应用层心跳(定时读取PLC特殊寄存器/心跳字);二是每次重连前先关闭旧Socket,并确保同一IP只有一个连接实例,杜绝重复连接占用槽位。
5.3 从D区读上来的32位数值总是“差一点”
现象:PLC里放了一个温度值,上位机读出来数值差距很大,但单看每个16位寄存器的值似乎又对得上。
排查过程:先在PLC里写入一个固定值0x3F800000(即浮点数1.0),上位机读回来拆成两个字,得到0x0000和0x3F80。如果按高位字在前拼接,就是0x00003F80,明显不对;按低位字在前拼接,得到0x3F800000,正好是1.0。
问题出在驱动里32位拼接时用了“高字在前”的顺序,而三菱MC协议读取的字顺序是低字在前。改一行代码后,所有浮点数和32位整数数据恢复正常。
这个案例建议固化成一类测试用例:在PLC中预置几个已知的32位数值和浮点数,驱动上线前先跑一遍自动校验,能提前发现绝大多数字节序问题。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| TCP连接不上 | IP/端口配错、PLC未启用开放通信 | ping测试,检查PLC模块参数 |
| 连接建立但无响应 | 报文长度字段口径错、帧头错、设备代码错 | 抓包并对照手册逐字节检查 |
| 响应正常但数据全为0 | 读取地址错误、PLC侧通信区未刷新 | 在PLC监控窗口确认软元件当前值 |
| 偶发性读一次失败一次 | 粘包/半包处理不当 | 检查ReceiveExactly是否按长度循环 |
| 运行一段时间后断线 | 连接数占满、空闲回收 | 加心跳、重连前释放连接 |
| 读上来的数值翻倍或减半 | 32位拼接顺序有误 | 用已知值做字节序冒烟测试 |
| 写入不生效 | 写入命令或子命令错误、软元件被程序保护 | 检查命令码,查看PLC侧对该区访问权限 |
最后再分享一点个人体会:我自己封装这套驱动花了三天,其中两天半都在跟报文格式和字节序较劲,真正写业务接口只用了半天。但在后续几个月里,每次现场出问题,只要翻日志、抓包、对照手册,基本半小时内就能定位,这种可排查性带来的收益远远大于当时直接买现成控件省下的开发时间。如果你手头正在做机器人跟PLC对接,我也建议别急着堆功能,先把D区读写这一个点从发报文到解析彻底跑通,后面所有事情都会顺很多。