三菱 MC 协议(MELSEC Communication Protocol / SLMP)详解

本篇定位:面向有 Java 基础、正在做工业物联网平台(FastBee)数据采集的工程师。读完应能手工拼出一条 3E 二进制报文、手工拆开一条 PLC 应答、把位数据从三菱的"半字节压缩"格式还原成 boolean[],并知道现场最常踩的坑在哪。文中所有字节值均来自本地两份开源库源码(HslCommunication Java 3.9.3、iot-communication),已标注文件路径与行号。

目录

  1. 三菱协议家族:MC 协议与 SLMP 的关系
  2. 帧类型总览:1E / 2E / 3E / 4E 与 ASCII、二进制
  3. QnA 兼容 3E 帧二进制结构(逐字节拆解)
  4. 核心指令区:命令 + 子命令 + 软元件 + 地址 + 长度
  5. 软元件编码表(1 字节设备码)
  6. 地址编码:3 字节小端 + 十进制/十六进制陷阱
  7. 报文实例(6 个完整字节流)
  8. ASCII 帧:与二进制的逐字段对应关系
  9. 1E 帧(A 兼容):结构与实例
  10. 结束代码(错误码)对照表
  11. 地址语法与 D100.0 位访问的实现
  12. 批量读上限与自动切片算法
  13. UDP 版 SLMP(MC over UDP)
  14. 现场配置清单
  15. 常见坑清单与小结

1. 三菱协议家族:MC 协议与 SLMP 的关系

MC 协议(MELSEC Communication Protocol) 是三菱定义的上位机(PC / 采集网关 / 边缘盒子)通过串口或以太网访问 PLC 软元件(D、M、X、Y……)的应用层协议,问答式:一问一答完成一次读写。

SLMP(Seamless Message Protocol) 是 iQ-R / iQ-F 时代给同一套协议起的新名字,本质是同一套帧结构的标准化与扩展(增加 4E 帧、标签访问、安全认证)。业界习惯:MC 协议 = 3E/4E/1E 帧统称(Q/L/QnA/FX5U 文档与开源库);SLMP = iQ-R/iQ-F 官方手册用法;MELSEC 通信协议 = 中文手册译名。

做采集平台不必纠结名词,看帧类型即可。 驱动里真正的分支是 1E / 3E / 4E × 二进制 / ASCII。

1.2 支持机型

机型 以太网接口 常用帧 说明
Q 系列 QJ71E71-100 3E、4E、1E 最主流现场机型
L 系列 内置以太网 / QJ71E71 3E、4E 与 Q 共用 QnA 兼容 3E
QnA 系列 QJ71E71 3E(QnA 帧) 老机型
iQ-R 系列 RJ71EN71 / 内置网口 4E(SLMP)、3E 子命令值不同(见 4.2)
iQ-F / FX5U 内置以太网口 3E、1E、4E FX5U 原生支持 MC 协议
FX3U 需外加以太网模块 1E(A 兼容) FX3U 本体不支持 3E
A 系列 A 系列以太网模块 1E 老机型,软元件码是 ASCII 字符

iot-communication 的注释最直白(_source/iot-communication/.../melsec/enums/EMcSeries.java:46-52):FX5U/Q/QnA/L 通常用 QnA 兼容 3E 帧;FX3U 需加以太网模块、采用 A 兼容 1E 帧;串口设备一般用 QnA 兼容 2C 帧和 4C 帧。

1.3 帧命名规律(1C/2C/3C/4C 与 1E/2E/3E/4E)

命名格式是 xxx 兼容 n m 帧,规则引自 EMcSeries.java:30-45:

1、xxx 表示兼容对象 PLC CPU:A = A 系列,QnA = QnA 系列
2、n 对应的以前产品模块的帧
    1 : A 系列计算机链接模块、以太网接口模块的指令帧    2 : QnA 串行通信模块的 QnA 简易帧
    3 : QnA 串行通信模块的 QnA 帧及兼容 QnA 以太网接口模块的帧
    4 : 兼容 QnA 串行通信模块的 QnA 扩展帧
3、m 是进行数据通信的对象模块:C = C24(串行),E = E71(以太网)

1E = A 兼容 + 以太网(老 A 系列 / FX3U+ENET);3E = QnA 兼容 + 以太网(Q/L/FX5U 主力,本文重点);4E = QnA 扩展 + 以太网(iQ-R / SLMP,多 4 字节序列号);2C/3C/4C = 串行版本。


2. 帧类型总览:1E / 2E / 3E / 4E 与 ASCII、二进制

2.1 四种以太网帧

帧 副头部(请求) 副头部(应答) 头部长度 典型机型 源码
1E 1 字节 = 指令码(00/01/02/03/04/05) 请求副头 + 0x80 2 字节 A 系列、FX3U+ENET MelsecA1ENet.java:186、MelsecA1EBinaryMessage.java:35
3E 50 00 D0 00 11 字节 Q / L / FX5U / QnA McBinaryHelper.java:27-28、MelsecQnA3EBinaryMessage.java:19
4E 54 00 D4 00 15 字节(多序列号+固定值 4 字节) iQ-R / SLMP EMcFrameType.java:45、McHeader4EReq.java:71-84

2E(QnA 简易帧)少见,两份源码均未实现。副头部常量(_source/iot-communication/.../melsec/enums/EMcFrameType.java:45-55):

FRAME_4E("4E", 0x0054, 0x00D4),
FRAME_3E("3E", 0x0050, 0x00D0),
FRAME_1E("1E", 0x00,   0x00),

2.2 为什么叫"3E"

因为帧的前两个字节(副头部 / Sub Header)就是 50 00——小端字节序下代表 16 位值 0x0050,而 0x0050 正是"3E 帧请求"的编号。Hsl 硬编码(Profinet/Melsec/Helper/McBinaryHelper.java:27-28):_PLCCommand[0] = 0x50; _PLCCommand[1] = 0x00;,应答校验则认 D0 00(Core/IMessage/MelsecQnA3EBinaryMessage.java:19:(HeadBytes[0] & 0xff) == 0xd0 && HeadBytes[1] == 0x00)。

记忆法:50 00 发,D0 00 收;4E 是 54 00 发、D4 00 收。

2.3 选型表

你的 PLC 推荐帧 编码 Hsl 类 备注
Q / L + QJ71E71 3E 二进制 MelsecMcNet 首选,性能最好
Q / L(模块设成 ASCII) 3E ASCII MelsecMcAsciiNet 帧长翻倍,吞吐减半
FX5U / iQ-F 3E(也可 1E / 4E) 二进制 MelsecMcNet GX Works3 里选 SLMP 设备连接
iQ-R / iQ-F(SLMP) 4E(兼容 3E) 二进制 MelsecMcRNet 子命令 0x0002/0x0003,地址 4 字节
A 系列 / FX3U + 以太网模块 1E 二进制 MelsecA1ENet 软元件码是 2 个 ASCII 字符
不确定 先试 3E 二进制 二进制 — 不通再试 ASCII、1E

iot-communication 里 EMcSeries.IQ_R 有一条 TODO:不是很确定IQ-R是否为3E帧类型(EMcSeries.java:82)。实践上 iQ-R 同时支持 3E 与 4E,新项目优先试 4E,回落 3E。


3. QnA 兼容 3E 帧二进制结构(逐字节拆解)

完整 3E 二进制请求 = 11 字节头部 + 核心指令区。头部由 McBinaryHelper.PackMcCommand 生成(Profinet/Melsec/Helper/McBinaryHelper.java:24-41):

public static byte[] PackMcCommand(byte[] mcCore, byte networkNumber, byte networkStationNumber) {
    byte[] _PLCCommand = new byte[11 + mcCore.length];
    _PLCCommand[0] = 0x50;                                               // 副标题
    _PLCCommand[1] = 0x00;
    _PLCCommand[2] = networkNumber;                                      // 网络号
    _PLCCommand[3] = (byte) 0xFF;                                        // PLC编号
    _PLCCommand[4] = (byte) 0xFF;                                        // 目标模块IO编号
    _PLCCommand[5] = 0x03;
    _PLCCommand[6] = networkStationNumber;                               // 目标模块站号
    _PLCCommand[7] = (byte)((_PLCCommand.length - 9) % 256);             // 请求数据长度
    _PLCCommand[8] = (byte)((_PLCCommand.length - 9) / 256);
    _PLCCommand[9] = 0x0A;                                               // CPU监视定时器
    _PLCCommand[10] = 0x00;
    System.arraycopy(mcCore, 0, _PLCCommand, 11, mcCore.length);
    return _PLCCommand;
}

3.2 逐字节表格(请求)

偏移 字节数 示例值 字段 含义
0 2 50 00 副头部(Sub Header) 固定,标识 3E 帧请求;小端即 0x0050
2 1 00 网络号(Network No.) 直连以太网填 00;经 MELSECNET/H 中转填实际网络号
3 1 FF PLC 编号(PC No.) 访问自己填 FF;访问其它站填站号
4 2 FF 03 目标模块 I/O 编号 小端 = 0x03FF,多 CPU / 冗余系统才改
6 1 00 目标模块站号 直连填 00
7 2 0C 00 请求数据长度 小端 = 从"CPU 监视定时器"到报文末尾的字节数
9 2 0A 00 CPU 监视定时器 小端 = 0x000A,单位 250ms
11 N … 核心指令区 命令 + 子命令 + 软元件 + 地址 + 长度 + 数据,见第 4 节
偏移:  0    1    2    3    4    5    6    7    8    9    10   11 ...
     +----+----+----+----+----+----+----+----+----+----+----+===========+
     | 50 | 00 | NN | FF | FF | 03 | SS | LL | LL | TT | TT |  核心指令区  |
     +----+----+----+----+----+----+----+----+----+----+----+===========+
      \___副头部___/   \_______访问路径(5 字节)_______/  \_数据长_/ \_定时器_/

3.3 请求数据长度 = 粘包解法(重点)

这个字段就是 TCP 粘包/拆包的答案。 TCP 是字节流,接收端必须知道一帧有多长。3E 的做法:先读 9 字节头部(副头部 2 + 访问路径 5 + 数据长度 2);从第 7、8 字节取出长度 L(小端);再读 L - 2 字节(应答的 L 从结束代码算起,减掉的 2 字节是结束代码)。

Hsl 实现(Core/IMessage/MelsecQnA3EBinaryMessage.java:4-13):

public int ProtocolHeadBytesLength() { return 9; }
public int GetContentLengthByHeadBytes() {
    byte[] HeadBytes = getHeadBytes();
    if (HeadBytes == null) return 0;
    return (HeadBytes[7] & 0xff) + (HeadBytes[8] & 0xff) * 256;
}

iot-communication 先读 11 字节再读 L-2,总长同样是 9 + L(service/McNetwork.java:171-196:remainLength = header.getDataLength() - 2;)。

应答的 L 含结束代码,请求的 L 含监视定时器——最易写错。源码佐证(model/McMessageReq.java:79-85):

public void selfCheck() {
    if (this.header.getFrameType() == EMcFrameType.FRAME_1E) return;
    McHeader3EReq header3EReq = (McHeader3EReq) this.header;
    header3EReq.dataLength = 2 + this.data.byteArrayLength();   // 2 = 监视定时器
}

3.4 CPU 监视定时器:单位换算

线上 2 字节小端整数 T,实际等待时间 = T × 250ms。

T(小端字节) 十进制 等待时间 出处
00 00 0 无限等待(不推荐,会挂死采集线程) McHeaderReq.java:64
0A 00 10 2.5 s Hsl 二进制默认,McBinaryHelper.java:36
0C 00 12 3.0 s iot 默认 monitoringTimer=3000,McNetwork.java:93
28 00 40 10 s(连接站上限) McHeaderReq.java:66
F0 00 240 60 s(其它站上限) McHeaderReq.java:67

源码对取值区间的说明(model/McHeaderReq.java:62-68):0000H(0) 无限等待;0001H~FFFFH 等待时间(单位 250ms);连接站 0001H~0028H(0.25~10 秒);其它站 0002H~00F0H(0.5~60 秒)。⚠️ ASCII 帧的定时器是 4 个 ASCII 字符,Hsl 写 "0010"(McAsciiHelper.java:318-321),即 0x0010 = 16 → 4.0 s,与二进制的 2.5 s 不同。

3.5 应答报文结构(逐字节)

偏移 字节数 示例值 含义
0 2 D0 00 应答副头部(3E)
2 1 00 网络号(回显)
3 1 FF PLC 编号(回显)
4 2 FF 03 目标模块 I/O 编号(回显)
6 1 00 目标模块站号(回显)
7 2 16 00 应答数据长度(从结束代码算起,含结束代码 2 字节)
9 2 00 00 结束代码(0 = 成功;非 0 见第 10 节)
11 N … 应答数据(读=软元件值;写=空)

结束代码在偏移 9,硬编码(McBinaryHelper.java:48-54):int errorCode = Utilities.getUShort( content, 9 );。取数据区时砍掉前 11 字节(MelsecMcNet.java:112-117):SoftBasic.BytesArrayRemoveBegin(response, 11)。


4. 核心指令区:命令 + 子命令 + 软元件 + 地址 + 长度

核心指令区(core)从报文偏移 11 开始,通用布局:

+-------+--------+----------+----------+----------+--------+
| 命令  | 子命令 | 起始地址 | 软元件码 | 软元件点数 | 数据…  |
| 2 字节| 2 字节 |  3 字节  |  1 字节  |  2 字节   | 仅写有 |
+-------+--------+----------+----------+----------+--------+

4.1 命令(Command,2 字节,小端)

⚠️ 字节序陷阱:源码写成 {0x01, 0x04}(线上字节序),但逻辑值是 0x0401。命令是小端:先发低字节。

线上字节 逻辑值 名称 源码
01 04 0x0401 批量读(Device Read Batch) McBinaryHelper.java:67-68
01 14 0x1401 批量写(Device Write Batch) McBinaryHelper.java:91-92
03 04 0x0403 随机读(字单位,每地址 1 字) McBinaryHelper.java:169-170
02 14 0x1402 随机写(字单位) McBinaryHelper.java:217-218
06 04 0x0406 多块批量读(每块可指定长度) McBinaryHelper.java:192-193
06 14 0x1406 多块批量写 EMcCommand.java:113
01 08 / 02 08 0x0801 / 0x0802 监视数据登录 / 监视读取 EMcCommand.java:119/125
01 10 / 02 10 / 06 10 0x1001 / 0x1002 / 0x1006 远程 RUN / STOP / RESET McHelper.java:455/468/481
01 01 / 17 16 0x0101 / 0x1617 读 CPU 型号 / 出错信息清除 McHelper.java:495/512

关于"监视"指令:iot-communication 把 0x0613 定义为 BUFFER_MEMORY_ACCESS_BATCH_READ(缓冲存储器批量读,EMcCommand.java:155),Hsl 注释也是"读取缓冲数据命令"(McBinaryHelper.java:356)。监视对应的是 0x0801 / 0x0802。若现场文档写 0x0603,需实测确认。

4.2 子命令(Sub Command,2 字节,小端)

位/字访问的分水岭,也是 Q/L 与 iQ-R 的主要差异:

线上字节 逻辑值 适用机型 含义 源码
00 00 0x0000 Q / L / QnA 字单位批量读写 McBinaryHelper.java:69、McReadDeviceBatchInWordReqData.java:54
01 00 0x0001 Q / L / QnA 位单位批量读写 McBinaryHelper.java:69/118、McReadDeviceBatchInBitReqData.java:54
02 00 0x0002 iQ-R / iQ-F 字单位(替代 0x0000) McReadDeviceBatchInWordReqData.java:54、MelsecMcRNet.java:286
03 00 0x0003 iQ-R / iQ-F 位单位(替代 0x0001) McReadDeviceBatchInBitReqData.java:54、MelsecMcRNet.java:286
80 00 / 81 00 0x0080/0x0081 Q/L 扩展指定 字/位 + 扩展文件寄存器 McBinaryHelper.java:142

iot-communication 的三元表达式最清楚(McReadDeviceBatchInWordReqData.java:53-54):this.subcommand = series != EMcSeries.IQ_R ? 0x0000 : 0x0002;;位版本(McReadDeviceBatchInBitReqData.java:53-54):... ? 0x0001 : 0x0003。Hsl 的 iQ-R 实现(MelsecMcRNet.java:286):command[2] = isBit ? (byte)0x03 : (byte)0x02;。

坑:用 Q/L 的子命令 0x0000 访问 iQ-R,PLC 回 0xC059。

4.3 起始地址(3 字节小端)与点数(2 字节小端)

地址是 int → 4 字节小端 → 取前 3 字节。McBinaryHelper.java:71-76:

command[4] = Utilities.getBytes(addressData.getAddressStart())[0];      // 起始地址的地位
command[5] = Utilities.getBytes(addressData.getAddressStart())[1];
command[6] = Utilities.getBytes(addressData.getAddressStart())[2];
command[7] = (byte) addressData.getMcDataType().getDataCode();          // 软元件码
command[8] = (byte) (addressData.getLength() % 256);                    // 点数低字节
command[9] = (byte) (addressData.getLength() / 256);                    // 点数高字节

(Utilities.getBytes(int) 是小端,见 Utilities.java:73-81。)iQ-R 用 4 字节地址(McDeviceAddress.java:134-142)。软元件码见第 5 节。


5. 软元件编码表(1 字节设备码)

逐条核对自 Profinet/Melsec/MelsecMcDataType.java(行号已标注)。FromBase 列是第 6 节进制陷阱的依据。

软元件 设备码 类型 FromBase ASCII 码 源码行
X(输入继电器) 0x9C 位 16 X* / X*** 70 / 191
Y(输出继电器) 0x9D 位 16 Y* 74 / 196
M(中间继电器) 0x90 位 10 M* 78 / 201
SM(特殊继电器) 0x91 位 10 SM 82 / 206
S(步进继电器) 0x98 位 10 S* 86 / 226
L(锁存继电器) 0x92 位 10 L* 90 / 211
F(报警器) 0x93 位 10 F* 94 / 216
V(边沿继电器) 0x94 位 10 V* 98 / 221
B(链接继电器) 0xA0 位 16 B* 102 / 231
SB(特殊链接继电器) 0xA1 位 16 SB 106 / 236
DX(直接访问输入) 0xA2 位 16 DX 110 / 241
DY(直接访问输出) 0xA3 位 16 DY 114 / 246
D(数据寄存器) 0xA8 字 10 D* 120 / 251
SD(特殊寄存器) 0xA9 字 10 SD 124 / 256
W(链接寄存器) 0xB4 字 16 W* 128 / 261
SW(特殊链接寄存器) 0xB5 字 16 SW 132 / 266
R(文件寄存器) 0xAF 字 10 R* 136 / 271
Z(变址寄存器) 0xCC 字 10 Z* 140 / 276
ZR(文件寄存器 ZR 区) 0xB0 字 10(Hsl)/16(iot) ZR 144 / EMcDeviceCode.java:262
TN(定时器当前值) 0xC2 字 10 TN 149
TS(定时器触点) 0xC1 位 10 TS 153
TC(定时器线圈) 0xC0 位 10 TC 157
SS / STS(累计定时器触点) 0xC7 位 10 SS / STS* 161 / 297
SC / STC(累计定时器线圈) 0xC6 位 10 SC / STC* 165 / 301
SN / STN(累计定时器当前值) 0xC8 字 10 SN / STN* 169 / 305
CN(计数器当前值) 0xC5 字 10 CN 173
CS(计数器触点) 0xC4 位 10 CS 177
CC(计数器线圈) 0xC3 位 10 CC 181

iQ-R 专有扩展(同文件 185-357 行,R_ 前缀,2 字节设备码):R_LTS=0x0051、R_LTC=0x0050、R_LTN=0x0052、R_LSTS=0x0059、R_LSTC=0x0058、R_LSTN=0x005A、R_LCS=0x0055、R_LCC=0x0054、R_LCN=0x0056。

核对结果:D=A8 / M=90 / X=9C / Y=9D / L=92 / F=93 / V=94 / B=A0 / W=B4 / R=AF / ZR=B0 / SD=A9 / SM=91 / S=98 / TS=C1 / TC=C0 / TN=C2 / CS=C4 / CC=C3 / CN=C5 / SB=A1 / SW=B5 / DX=A2 / DY=A3 —— 全部与源码一致。

两处源码冲突(需实测确认):① ZR 进制:Hsl 是 10(MelsecMcDataType.java:144),iot-communication 是 16(EMcDeviceCode.java:262),Q/L 通常十进制、iQ-R 十六进制,现场以 ZR 报错为准逐台试。② ASCII 软元件码第二字符:两库用 "D*",三菱手册常写 "D "(空格 0x20),多数 PLC 都接受。


6. 地址编码:3 字节小端 + 十进制/十六进制陷阱

6.1 规则

X、Y、B、SB、DX、DY、W、SW 这 8 类按十六进制解释地址;其余(D、M、L、F、V、S、SM、SD、R、ZR、TS/TC/TN、CS/CC/CN)按十进制。 依据就是 MelsecMcDataType 构造函数的第 4 个参数 fromBase 及其在 McAddressData.ParseMelsecFrom 中的使用(Core/Address/McAddressData.java:62):

addressData.setAddressStart(Integer.parseInt(address.substring(1), MelsecMcDataType.M.getFromBase()));

6.2 例子(最易错点)

地址写法 软元件 进制 计算 3 字节小端
D100 D 10 100 64 00 00
M100 M 10 100 64 00 00
X10 X 16 0x10 = 16 10 00 00
X1F / X20 X 16 0x1F = 31 / 0x20 = 32 1F 00 00 / 20 00 00
Y10 Y 16 0x10 = 16 10 00 00
B1A B 16 0x1A = 26 1A 00 00
W0 / R0 / SD0 / TS0 — 16/10/10/10 0 00 00 00
ZR1000 ZR 10 或 16 1000 或 0x1000 = 4096 E8 03 00 或 00 10 00

X1F 之后的处理:X/Y 是十六进制,X1F(31)的下一个编号是 X20(32),不是 X32。按十进制理解会读错 12 个点,现场表现为"读 X0~X1F 正常,读 X20 之后数据对不上"。

6.3 八进制陷阱(源码里的隐藏逻辑)

McAddressData.ParseMelsecFrom 对 X/Y 有特殊分支(Core/Address/McAddressData.java:65-84):

case 'X':
case 'x': {
    addressData.McDataType = MelsecMcDataType.X;
    address = address.substring(1);
    if (address.startsWith("0"))
        addressData.setAddressStart(Integer.parseInt(address, 8));   // ← 八进制!
    else
        addressData.setAddressStart(Integer.parseInt(address, MelsecMcDataType.X.getFromBase()));
    break;
}

X/Y 地址以字符 0 开头时,Hsl 按八进制解析(Core/Address/McAddressData.java:65-84 与 1E 的 MelsecHelper.java:68-87):

if (address.startsWith("0")) addressData.setAddressStart(Integer.parseInt(address, 8));   // ← 八进制!
else addressData.setAddressStart(Integer.parseInt(address, MelsecMcDataType.X.getFromBase()));

X017 → parseInt("017", 8) = 15 → 0F 00 00,而非十六进制的 0x17 = 23。平台建议:FastBee 的地址解析器最好显式禁止隐式八进制;想读 X 的第 17 号点就写 X11,别写 X017。

6.4 位软元件按字单位访问时,起始地址必须是 16 的倍数

三菱错误码 0x0058 的第 5 条明确了这一点(Language/DefaultLanguage.java:208)。即用字单位(子命令 0x0000)读位软元件(如 M)时,起始地址必须能被 16 整除,一次读出 16 个位(1 字)——这也是 McHelper.Read 里地址自增 + readLength * 16 的原因(McHelper.java:107)。


7. 报文实例(6 个完整字节流)

以下全部 3E 二进制,默认网络号 00、PLC 编号 FF、I/O 编号 FF 03、站号 00、定时器 0A 00。

例 1:读 D100 开始的 10 个字(批量读 0x0401,字单位)

请求(21 字节)

50 00 00 FF FF 03 00 0C 00 0A 00 01 04 00 00 64 00 00 A8 0A 00
偏移 字节 值 含义
0-1 2 50 00 副头部(3E 请求)
2 1 00 网络号
3 1 FF PLC 编号
4-5 2 FF 03 目标模块 I/O 编号(小端 0x03FF)
6 1 00 目标模块站号
7-8 2 0C 00 请求数据长度 = 21 − 9 = 12
9-10 2 0A 00 CPU 监视定时器 = 10 × 250ms = 2.5s
11-12 2 01 04 命令 = 0x0401 批量读
13-14 2 00 00 子命令 = 字单位
15-17 3 64 00 00 起始地址 = 100(D100,十进制)
18 1 A8 软元件码 = D
19-20 2 0A 00 软元件点数 = 10(字)

对应源码:McBinaryHelper.BuildReadMcCoreCommand(McBinaryHelper.java:65-79)+ PackMcCommand(同文件 24-41)。

应答(31 字节,假设 D100~D109 = 0x0001 … 0x000A): D0 00 00 FF FF 03 00 16 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0A 00 16 00 = 数据长度 2(结束代码)+ 20(数据)= 22;00 00 = 成功;随后 20 字节 = 10 个字,每字小端(0x0001 → 01 00)。

例 2:读 M100 开始的 16 个位(位压缩编码)

请求(21 字节)

50 00 00 FF FF 03 00 0C 00 0A 00 01 04 01 00 64 00 00 90 10 00
偏移 值 含义
11-12 01 04 命令 0x0401 批量读
13-14 01 00 子命令 = 0x0001 位单位
15-17 64 00 00 起始地址 = 100(M100,十进制)
18 90 软元件码 = M
19-20 10 00 点数 = 16 个位

应答(19 字节):D0 00 00 FF FF 03 00 0A 00 00 00 10 01 11 00 10 01 11 00 0A 00 = 数据长度 2 + 8 = 10;00 00 = 成功;10 01 11 00 10 01 11 00 = 8 字节承载 16 个位。

例 2 续:三菱位数据的压缩编码规则(必须掌握)

位读取时,PLC 不是一个位一个字节,而是:

  1 个字节 = 2 个位
  ┌─────────┬─────────┐
  │ 高 4 位 │ 低 4 位 │
  │(bit7-4) │(bit3-0) │
  └─────────┴─────────┘
      ↓          ↓
   偶数号位     奇数号位
   ON = 0x10   ON = 0x01
字节值 含义
0x10 偶数号位 ON,奇数号位 OFF
0x01 奇数号位 ON,偶数号位 OFF
0x11 两个位都 ON
0x00 两个位都 OFF

上面应答展开为:0x10→M100=1,M101=0;0x01→M102=0,M103=1;0x11→M104=1,M105=1;0x00→M106=0,M107=0;0x10→M108=1,M109=0;0x01→M110=0,M111=1;0x11→M112=1,M113=1;0x00→M114=0,M115=0。

为什么是 0x10 而不是 0xF0? 解码用等值判断而非普通按位与:源码是 (response[i] & 0x10) == 0x10(McBinaryHelper.java:491)。PLC 实际只会回 0x00/0x01/0x10/0x11。写代码务必用等值语义(iot-communication 是严格等值:McNetwork.java:903),否则 0x30 这类异常值可能被误判为 ON。

解压 / 打包的 Java 代码

解压(PLC 应答字节 → boolean[]),对应 McBinaryHelper.ExtractActualDataHelper(McBinaryHelper.java:486-505):

// 解压:每 1 字节承载 2 个位,高 4 位=偶数号位,低 4 位=奇数号位
public static boolean[] unpackBits(byte[] response, int length) {
    boolean[] result = new boolean[length];
    for (int i = 0; i < response.length; i++) {
        int even = i * 2, odd = i * 2 + 1;
        if (even < length) result[even] = (response[i] & 0x10) == 0x10;
        if (odd  < length) result[odd]  = (response[i] & 0x01) == 0x01;
    }
    return result;
}

iot-communication 的等价实现更严格(等值判断,McNetwork.java:900-907):

public List<Boolean> getBooleansBy(byte[] bytes) {
    List<Boolean> res = new ArrayList<>();
    for (byte aByte : bytes) {
        res.add((aByte & (byte) 0xF0) == 0x10);   // 偶数号位
        res.add((aByte & 0x0F) == 0x01);          // 奇数号位
    }
    return res;
}

打包(boolean[] → 三菱字节),对应 MelsecHelper.TransBoolArrayToByteData(MelsecHelper.java:230-242):

// 偶数下标 -> +0x10,奇数下标 -> +0x01;奇数个时最后一位补在低位
public static byte[] packBits(boolean[] value) {
    int length = (value.length + 1) / 2;          // 向上取整
    byte[] buffer = new byte[length];
    for (int i = 0; i < length; i++) {
        if (value[i * 2]) buffer[i] += 0x10;                       // 偶数号位
        if ((i * 2 + 1) < value.length && value[i * 2 + 1])
            buffer[i] += 0x01;                                     // 奇数号位
    }
    return buffer;
}

自测:packBits({true,false,true,true}) → {0x10, 0x11};unpackBits({0x10,0x01,0x11,0x00}, 8) → [T,F,F,T,T,T,F,F]。

例 3:写 D200 两个字(批量写 0x1401,字单位)

写入 D200 = 0x3412、D201 = 0x7856(字节流 12 34 56 78,小端)。

请求(25 字节)

50 00 00 FF FF 03 00 10 00 0A 00 01 14 00 00 C8 00 00 A8 02 00 12 34 56 78
偏移 值 含义
7-8 10 00 请求数据长度 = 25 − 9 = 16
11-12 01 14 命令 = 0x1401 批量写
13-14 00 00 子命令 = 字单位
15-17 C8 00 00 起始地址 = 200(0xC8,十进制)
18 A8 软元件码 = D
19-20 02 00 点数 = 2 字(= value.length / 2,McBinaryHelper.java:99-100)
21-24 12 34 56 78 写入数据(每字小端)

应答(11 字节,写操作无数据区):D0 00 00 FF FF 03 00 02 00 00 00(02 00 = 数据长度 2,只有结束代码;00 00 = 成功)。

例 4:随机读 3 个不连续 D 寄存器(0x0403)

命令 0x0403 每条目 4 字节(3 地址 + 1 软元件码),不带点数(固定每地址 1 字)。读 D10、D100、D200 各 1 字,请求(29 字节):

50 00 00 FF FF 03 00 14 00 0A 00 03 04 00 00 03 00
0A 00 00 A8  64 00 00 A8  C8 00 00 A8
偏移 值 含义
7-8 14 00 请求数据长度 = 29 − 9 = 20
11-12 / 13-14 03 04 / 00 00 命令 0x0403 随机读 / 子命令字单位
15 / 16 03 / 00 字访问点数 3 / 双字访问点数 0
17-28 0A 00 00 A8 64 00 00 A8 C8 00 00 A8 D10 / D100 / D200

源码(McBinaryHelper.java:167-182)关键片段:command[0]=0x03; command[1]=0x04; command[4]=(byte)address.length; command[5]=0x00;,之后每条目 4 字节。应答(17 字节):D0 00 00 FF FF 03 00 08 00 00 00 11 22 33 44 55 66(08 00 = 2 + 6,数据 6 字节 = 3 字)。

例 4b:随机写字(0x1402)

写 D100 = 0x1234、D200 = 0x5678。每条目 6 字节(3 地址 + 1 码 + 2 数据)。

50 00 00 FF FF 03 00 14 00 0A 00 02 14 00 00 02 00
64 00 00 A8 34 12  C8 00 00 A8 78 56

02 14 = 命令 0x1402 随机写;00 00 = 字单位;02 = 字访问点数 2;00 = 双字访问点数 0;64 00 00 A8 34 12 = D100 ← 0x1234(小端 34 12);C8 00 00 A8 78 56 = D200 ← 0x5678(小端 78 56)。

源码:McBinaryHelper.BuildWriteRandomWordCommand(McBinaryHelper.java:215-231)。源码注释给出上限:一次最多写入 80/160 个字(McBinaryHelper.java:210)。

例 4c:多块批量读(0x0406)

一次读 2 块:D100 起 2 字 + W0 起 3 字。每条目 6 字节(3 地址 + 1 码 + 2 点数)。请求(29 字节):

50 00 00 FF FF 03 00 14 00 0A 00 06 04 00 00 02 00
64 00 00 A8 02 00  00 00 00 B4 03 00

06 04 = 命令 0x0406 多块读;02 = 字软元件块数 2;00 = 位软元件块数 0;64 00 00 A8 02 00 = D100 读 2 字;00 00 00 B4 03 00 = W0 读 3 字。源码:McBinaryHelper.BuildReadRandomCommand(McBinaryHelper.java:190-207)。三菱约束见 Hsl 注释(McHelper.java:262-263):120 ≧ 字软元件块数 + 位软元件块数;960 ≧ 字软元件各块合计点数 + 位软元件各块合计点数。

例 5:位写入(0x1401 + 子命令 0x0001)

把 M200~M203 写成 [ON, OFF, ON, ON]。打包:i=0 → value[0]=true → 0x10,value[1]=false → 不加 → 0x10;i=1 → value[2]=true → 0x10,value[3]=true → +0x01 → 0x11。数据字节 = 10 11。

请求(23 字节)

50 00 00 FF FF 03 00 0E 00 0A 00 01 14 01 00 C8 00 00 90 04 00 10 11
偏移 值 含义
7-8 0E 00 请求数据长度 = 23 − 9 = 14
11-12 01 14 命令 = 0x1401 批量写
13-14 01 00 子命令 = 0x0001 位单位
15-17 C8 00 00 起始地址 = 200(M200)
18 90 软元件码 = M
19-20 04 00 点数 = 4 个位
21-22 10 11 压缩后的位数据

源码(McBinaryHelper.java:112-129):点数用 value.length(位个数),数据用压缩后的 buffer.length:

byte[] buffer = MelsecHelper.TransBoolArrayToByteData(value);
command[8] = (byte) (value.length % 256);       // 位个数
command[9] = (byte) (value.length / 256);
System.arraycopy(buffer, 0, command, 10, buffer.length);

⚠️ 位写入时"点数"和"数据字节数"不一致(4 个点 vs 2 字节)——新手最常写错的地方。应答(11 字节):D0 00 00 FF FF 03 00 02 00 00 00。


8. ASCII 帧:与二进制的逐字段对应关系

8.1 核心差异

项目 二进制 ASCII
一个字节的表示 1 字节 2 个 ASCII 十六进制字符(高位在前)
地址字段 3 字节(小端) 6 个 ASCII 字符
结束代码 2 字节小端(偏移 9) 4 个 ASCII 字符(偏移 18)
副头部 50 00 5000(4 字符)
头部长度 11 字节 22 字节
位数据 1 字节存 2 位 1 字符存 1 位('0' / '1')

8.2 ASCII 头部(22 字节)

McAsciiHelper.PackMcCommand(McAsciiHelper.java:296-324):

byte[] plcCommand = new byte[22 + mcCore.length];
plcCommand[ 0.. 3] = '5','0','0','0';                       // "5000" 副头部
plcCommand[ 4.. 5] = 网络号 hex;
plcCommand[ 6.. 7] = 'F','F';                               // "FF"  PLC 编号
plcCommand[ 8..11] = '0','3','F','F';                       // "03FF" I/O 编号
plcCommand[12..13] = 站号 hex;
plcCommand[14..17] = 数据长度 hex(plcCommand.length - 18);
plcCommand[18..21] = '0','0','1','0';                       // "0010" 定时器
System.arraycopy(mcCore, 0, plcCommand, 22, mcCore.length);
偏移 字符数 示例 含义
0 4 5000 副头部
4 2 00 网络号
6 2 FF PLC 编号
8 4 03FF 目标模块 I/O 编号(大端字符串,不是二进制的 FF 03)
12 2 00 目标模块站号
14 4 0018 请求数据长度(= 总字符数 − 18)
18 4 0010 CPU 监视定时器(0x0010 = 16 → 4.0s)
22 N … 核心指令区

8.3 ASCII 核心指令区(批量读,20 字符)

McAsciiHelper.BuildAsciiReadMcCoreCommand(McAsciiHelper.java:23-52):命令 "0401"(4 字符)+ 子命令 "0000"/"0001"(4 字符)+ 软元件 ASCII 码 "D*"(2 字符)+ 地址 6 字符 + 点数 4 字符。

地址格式化规则(MelsecHelper.java:181-184):

public static byte[] BuildBytesFromAddress(int address, MelsecMcDataType type) {
    if (Objects.equals(type.getAsciiCode(), "ZR")) return Encoding.ASCII.GetBytes( String.format("%06x", address) );
    return Utilities.getBytes(String.format(type.getFromBase() == 10 ? "%06d" : "%06x", address), "ASCII");
}

即:十进制软元件用 6 位十进制,十六进制软元件用 6 位十六进制,ZR 强制 6 位十六进制。

8.4 完整 ASCII 实例:读 D100 开始的 10 个字

请求(42 字符)

500000FF03FF000018001004010000D*000100000A
片段 位置 含义
5000 0-3 副头部
00 FF 03FF 00 4-13 网络号 / PLC 编号 / I/O 编号(大端字符串)/ 站号
0018 14-17 数据长度 = 42 − 18 = 24 = 0x18
0010 18-21 定时器 = 0x0010(4.0s)
0401 0000 22-29 命令 = 批量读;子命令 = 字单位
D* 000100 000A 30-41 软元件 D;地址 D100(%06d);点数 10 字

应答:D00000FF03FF0000160000 + 40 字符数据(D000 应答副头部;0016 = 2 + 20 = 22;0000 成功;随后 10 字 × 4 字符,每字大端,0x1234 → 1234)。

粘包处理:先读 18 字节头部,再从偏移 14 取 4 字符十六进制作为长度(Core/IMessage/MelsecQnA3EAsciiMessage.java:8-23):

public int ProtocolHeadBytesLength() { return 18; }
public int GetContentLengthByHeadBytes() {
    byte[] buffer = { HeadBytes[14], HeadBytes[15], HeadBytes[16], HeadBytes[17] };
    return Integer.parseInt(new String(buffer, StandardCharsets.US_ASCII), 16);
}

结束代码位置(McAsciiHelper.java:365):Integer.parseInt(new String(SoftBasic.BytesArraySelectMiddle(content, 18, 4), US_ASCII), 16)。

8.5 为什么 ASCII 帧的读写上限"减半"

每个字节变成 2 个字符,帧长翻倍,而 PLC 侧对"一帧最大字符数"有硬限制,于是同样缓冲区能装的数据量减半。源码证据(McHelper.java:25-38):

public static int GetReadWordLength( McType type ) {
    if (type == McType.McBinary) return 950;
    return 460;                       // ASCII 460 ≈ 950 / 2
}
public static int GetReadBoolLength( McType type ) {
    if (type == McType.McBinary) return 7168;
    return 3584;                      // ASCII 3584 = 7168 / 2
}

注:ASCII 位数据是 1 字符 1 位,二进制是 1 字节 2 位,两者换算比例不同,位上限最终落在 3584。另外命令/子命令用大写(SoftBasic.java:686),网络号/站号/数据长度用小写 String.format("%02x")(MelsecHelper.java:149/159),写报文比对测试时需先归一化大小写(PLC 对大小写一般不敏感,需实测确认)。


9. 1E 帧(A 兼容):结构与实例

9.1 与 3E 的差异

项目 3E 1E(A 兼容)
副头部 50 00(2 字节) 副标题 1 字节,兼作指令码
网络号 / I/O 编号 有(4 字节) 无
PLC 编号 1 字节 FF 1 字节(偏移 1)
地址 3 字节小端 4 字节小端
软元件码 1 字节二进制 2 字节 ASCII 字符(D = 0x4420)
命令 2 字节(0x0401 等) 无,靠副标题区分
结束代码位置 偏移 9(2 字节) 偏移 1(1 字节)
单帧上限 960 字 / 7168 位 64 字 / 256 位(Hsl 切片值)

9.2 副标题取值

值 含义 源码
0x00 位单位成批读出 MelsecA1ENet.java:186
0x01 字单位成批读出 MelsecA1ENet.java:186
0x02 位单位成批写入 MelsecA1ENet.java:266
0x03 字单位成批写入 MelsecA1ENet.java:235
0x04 / 0x05 位 / 字单位随机写入 EMcCommand.java:68/74

应答副标题 = 请求副标题 + 0x80(即 0x80/0x81/0x82/0x83,MelsecA1EBinaryMessage.java:35):if (((HeadBytes[0] & 0xff) - (SendBytes[0] & 0xff)) == 0x80) { return true; }

9.3 请求逐字节表(12 字节)

源码:MelsecA1ENet.BuildReadCommand(MelsecA1ENet.java:190-208):[0] 副标题、[1] PLC 号、[2-3] 定时器(0A 00)、[4-7] 起始软元件(4 字节小端)、[8-9] 软元件代码(2 字节小端)、[10-11] 点数(2 字节小端)。

偏移 字节数 示例 含义
0 1 01 副标题(00 位读 / 01 字读 / 02 位写 / 03 字写)
1 1 FF PLC 编号
2-3 2 0A 00 CPU 监视定时器(小端,10 × 250ms = 2.5s)
4-7 4 64 00 00 00 起始软元件编号(小端 int)
8-9 2 20 44 软元件代码(D = 0x4420 小端 → 20 44)
10-11 2 08 00 软元件点数

⚠️ 软元件码字节序的坑:MelsecA1EDataType.D 的值是 0x4420(MelsecA1EDataType.java:123),按字符拆是 'D'(0x44) + ' '(0x20);但写入报文用 BitConverter.GetBytes(short)(小端),因此线上字节是 20 44。源码注释也点明了(MelsecA1ENet.java:184:// 默认信息----注意:高低字节交错)。若 PLC 回 0x5B 或数据不对,先试把这两个字节交换成 44 20(需实测确认)。

9.4 1E 软元件码表

MelsecA1EDataType.java(63-133 行),括号内为线上小端字节:D=0x4420(20 44)、X=0x5820(20 58)、Y=0x5920(20 59)、M=0x4D20(20 4D)、S=0x5320(20 53)、F=0x4620(20 46)、B=0x4220(20 42)、W=0x5720(20 57)、R=0x5220(20 52)、TS=0x5453(53 54)、TC=0x5443(43 54)、TN=0x544E(4E 54)、CS=0x4353(53 43)、CC=0x4343(43 43)、CN=0x434E(4E 43)。进制:X/Y/B/W 为 16,其余为 10。

9.5 1E 实例

例 A:读 D100 开始的 8 个字(12 字节) 01 FF 0A 00 64 00 00 00 20 44 08 00 01 = 字单位成批读出;FF = PLC 编号;0A 00 = 定时器 2.5s;64 00 00 00 = 地址 100;20 44 = D;08 00 = 8 字。应答(18 字节):81 00 + 16 字节数据(81 = 应答副标题 0x01+0x80,00 = 成功)。

例 B:读 M100 开始的 16 个位(12 字节) 00 FF 0A 00 64 00 00 00 20 4D 10 00 00 = 位单位成批读出;20 4D = M;10 00 = 16 位。应答(10 字节):80 00 + 8 字节压缩位数据(与 3E 相同的 0x10/0x01 编码,从偏移 2 开始解析,见 MelsecA1ENet.java:301-321)。

例 C:写 D100 起 2 个字(16 字节) 03 FF 0A 00 64 00 00 00 20 44 02 00 34 12 78 56 03 = 字单位成批写入;02 00 = 2 字;34 12 78 56 = 数据(每字小端:D100=0x1234,D101=0x5678)。应答(2 字节):83 00。

9.6 1E 的结束代码与错误定位

MelsecA1ENet.CheckResponseLegal(MelsecA1ENet.java:287-293):

public static OperateResult CheckResponseLegal( byte[] response ) {
    if (response.length < 2) return new OperateResult(StringResources.Language.ReceiveDataLengthTooShort());
    if (response[1] == 0) return OperateResult.CreateSuccessResult();
    if (response[1] == 0x5B)
        return new OperateResult(response[2], ...);   // ← 错误码在偏移 2
    return new OperateResult(response[1], ...);       // ← 错误码就是偏移 1
}

结束代码在偏移 1;当它等于 0x5B 时,真正的错误码在偏移 2——1E 特有的两段式错误报告。

9.7 1E 的粘包处理(没有长度字段!)

1E 帧没有请求数据长度字段,只能靠"已知请求内容"反推应答长度(MelsecA1EBinaryMessage.java:9-29):

public int GetContentLengthByHeadBytes(){
    if (HeadBytes[1] == 0x5B) return 2;                       // 异常代码 + 0x00
    else if (HeadBytes[1] == 0x00) {
        switch (HeadBytes[0]) {
            case (byte)0x80: return SendBytes[10] != 0x00 ? ((SendBytes[10] & 0xff) + 1) / 2 : 128;  // 位读
            case (byte)0x81: return (SendBytes[10] & 0xff ) * 2;                                     // 字读
            case (byte)0x82:                                     // 位写
            case (byte)0x83: return 0;                           // 字写
            default: return 0;
        }
    }
    else return 0;
}

逻辑是:回头看自己发出的请求 SendBytes[10](点数低字节),位读 (点数+1)/2 字节、字读 点数×2 字节;点数字节为 0 时按 128 字节(256 位)算。这也解释了 1E 上限为何是 256 位 / 64 字。iot-communication 则直接开 1024 字节缓冲区读一次(McNetwork.java:152-168)。

1E 上限的两个冲突数据源:Hsl 切片值 64 字 / 256 位(MelsecA1ENet.java:187);iot-communication A 系列 10 字 / 160 位(EMcSeries.java:62-64)。建议按保守值实现:字 32 字/帧,位 160 点/帧,并做自动切片(需实测确认)。


10. 结束代码(错误码)对照表

10.1 位置

3E / 4E 二进制:偏移 9,2 字节 UInt16 小端;3E / 4E ASCII:偏移 18,4 个 ASCII 十六进制字符;1E:偏移 1,1 字节(值为 0x5B 时真实错误码在偏移 2)。

10.2 常见码表(逐条核对源码)

来源:MelsecHelper.GetErrorDescription(MelsecHelper.java:305-370)+ 中文文案 Language/DefaultLanguage.java:202-229。

结束代码 中文含义(源码原文) 现场速判 源码行
0x0000 正常结束 成功 —
0x0002 "读/写"(入/出)软元件的指定范围不正确 地址或长度越界 MelsecHelper.java:307
0x0051 随机访问缓冲存储器通讯时,起始地址超出 0-6143 范围 缓冲存储器地址越界 309
0x0052 起始地址+数据字数超出 0-6143;或数据不能一帧发完 帧太长,要切片 311
0x0054 选择了 ASCII 通讯,但收到无法转二进制的 ASCII 码 编码方式不匹配 313
0x0055 PLC 处于 RUN 且未允许"运行中写入",外部请求写数据 RUN 中禁止写入 315
0x0056 从外部进行的软元件指定不正确 软元件码不对 317
0x0058 起始地址超限;扩展块号不存在;不能指定 R;对位软元件用了字命令;位起始号不是 16 的倍数 多因,常见后两条 319
0x0059 不能指定扩展文件的寄存器 — 321
0x430D 请求数据异常,CPU 启用了用户认证,请禁用 CPU 认证未关 323
0xC04D 以太网模块 UDP/固定缓冲通讯中,应用领域指定的数据长度不正确 UDP 帧长错 325
0xC050 设置为 ASCII 通讯时收到无法转二进制的 ASCII 数据 编码方式不匹配 327
0xC051~0xC054 读/写点的数目在允许范围之外 点数超限 329-334
0xC055 文件数据读/写点的数目在允许范围之外 — 335
0xC056 读/写请求超过了最大地址 地址越界 337
0xC057 请求数据的长度与字符区域(部分文本)的数据计数不匹配 长度字段算错 339
0xC058 ASCII-二进制转换后,请求数据长度与字符区域数据计数不符 ASCII 长度算错 341
0xC059 命令和子命令的指定不正确 命令/子命令错(含 iQ-R 用错 0x0000) 343
0xC05A/0xC05B 以太网模块不能对指定软元件进行读出和写入 该软元件不可访问 345-347
0xC05C 请求内容不正确(以位为单位请求读/写至字软元件) 位/字混用 349
0xC05D 不执行监视注册 未先 0x0801 登录 351
0xC05E 以太网模块和 PLC CPU 之间的通讯时间超过了 CPU 监视定时器的时间 定时器太短 353
0xC05F 目标 PLC 上不能执行请求 — 355
0xC060 请求内容不正确(对位软元件等指定了不正确的数据) 位值不是 0x00/0x01/0x10/0x11 357
0xC061 请求数据的长度与字符区域中的数据数目不相符 长度算错 359
0xC062 禁止在线更正时,通过 MC 协议远程 I/O 站执行 3E/4E 帧写入 远程站写保护 361
0xC070 不能为目标站指定软元件存储器的范围 — 363
0xC072 请求内容不正确(以位为单位请求读写至字软元件) — 365
0xC074 目标 PLC 不执行请求。需要纠正网络号和 PC 号 网络号/PLC 号错 367
其它 请查看三菱的通讯手册来查看报警的具体信息 — 368(DefaultLanguage.java:193)

iot-communication 额外定义一条(McNetwork.java:277-279):0xC0B5 = 指定了 CPU 模块中无法处理的数据。

10.3 排错优先级(现场速查)

  1. 一上电就 0xC059 → 命令/子命令错 → 检查 iQ-R 是否用了 0x0002/0x0003。
  2. 0xC051~0xC054 → 点数超限 → 加自动切片(见 12 节)。
  3. 0xC05C / 0xC060 / 0x0058 → 位/字单位与软元件类型不匹配 → 检查子命令。
  4. 0xC056 / 0x0002 → 地址越界 → 检查 X/Y 的十六进制进制(见 6 节)。
  5. 0x0055 → PLC 在 RUN 且不允许运行中写入 → GX Works 勾上"运行中允许写入"。
  6. 0xC05E → 定时器太短 → 把 0A 00 调大(如 28 00 = 10s)。
  7. 0x0054 / 0xC050 → 二进制/ASCII 编码不匹配 → 改 PLC 侧通信数据代码设置。
  8. 0x430D → CPU 用户认证 → 关掉。

11. 地址语法与 D100.0 位访问的实现

McAddressData.ParseMelsecFrom(Core/Address/McAddressData.java:54-237)支持:D100、X0/X10/X1F、Y10、M100、W0、R0、ZR0、Z0、SD0、SM0、TS0/TC0/TN0、CS0/CC0/CN0、SB0/SW0、DX0/DY0、SS0/SC0/SN0、L0/F0/V0/B0/S0。⚠️ 源码 bug:case 'D' 分支判断 DY 用的是 address.charAt(1) == 'Y' || address.charAt(1) == 's'(McAddressData.java:95)——大写 S 会被误判成 DY,写平台时别依赖该分支,自己正则解析更稳。

11.2 D100.0 位访问的实现(读字再取位)

MC 协议没有"读字寄存器某一位"的原生指令。D100.0 的实现是"读 D100 整个字,再取第 0 位"。Hsl 的做法(McHelper.java:145-152):

if ( supportWordAdd && address.indexOf( '.' ) > 0) {
    return HslHelper.ReadBool(mc, address, length);      // ← 走"读字再取位"的通用实现
}

写操作同理(McHelper.java:202-203):return ReadWriteNetHelper.WriteBoolWithWord(mc, address, values, 16);(16 说明位索引范围是 .0 ~ .15)。源码注释(McHelper.java:141-144):ASCII 格式切割长度为 3584,二进制为 7168,并支持 D100.0 这种位访问写法。自己实现的伪代码:addr.split("\\.") 得到软元件地址与位号 → 读 1 个字 → 小端合成 int → (value >> bitIndex) & 0x01。 ⚠️ 这不是原子操作:"读字 → 改位 → 写字"在 PLC 扫描周期之间可能被 PLC 程序改写。对单个位的写入,用 0x1401 + 子命令 0x0001 对位软元件(M/Y/B 等)直接写;只有对 D/W/R 这类字软元件的某一位才用读改写,且要接受竞态风险。


12. 批量读上限与自动切片算法

12.1 上限的来历(取三者最小值)

  1. 协议规定(iot-communication EMcSeries.java:76-78,Q/L 系列):
Q_L(EMcFrameType.FRAME_3E, 1, 3, 960, 7168, 192, 1920, 188, 120, 4, 960),

参数顺序:frameType, deviceCodeByteLength=1, headDeviceNumberByteLength=3, deviceBatchInWordPointsCount=960, deviceBatchInBitPointsCount=7168, deviceRandomReadInWordPointsCount=192, deviceRandomWriteInWordPointsCount=1920, deviceRandomWriteInBitPointsCount=188, deviceBlocksBlocksCount=120, deviceBlocksWritePointsSize=4, deviceBlocksWritePointsCount=960。即 Q/L:批量读写 960 字 / 7168 位;随机读 192 字;随机写 1920 字 / 188 位;多块 120 块 / 960 点。 2. 库的实现保守值(McHelper.java:25-38):二进制字 950、位 7168;ASCII 字 460、位 3584。源码取 950 而非 960 应是给边界留余量,建议按 950 或更小实现(需实测确认)。 3. 其它站约束(McHelper.java:228-230 注释):Q 系列 C24/E71 站 1 ≤ 字访问点数+双字访问点数 ≤ 192;QnA 系列 ≤ 96;其它 PLC CPU 其它站 1 ≤ 字访问点数 ≤ 10。

12.2 自动切片算法

Hsl 的写法(McHelper.java:86-108):

int alreadyFinished = 0;
while (alreadyFinished < length) {
    int readLength = Math.min(length - alreadyFinished, GetReadWordLength(mc.getMcType()));
    addressResult.Content.setLength(readLength);
    byte[] command = ... BuildReadMcCoreCommand(addressResult.Content, false);
    OperateResultExOne<byte[]> read = mc.ReadFromCoreServer(command);
    if (!read.IsSuccess) return read;
    Utilities.ArrayListAddArray(bytesContent, mc.ExtractActualData(read.Content, false));
    alreadyFinished += readLength;

    // 字的话就是正常的偏移位置,如果是位的话,就转到位的数据
    if (addressResult.Content.getMcDataType().getDataType() == 0)
        addressResult.Content.setAddressStart(addressResult.Content.getAddressStart() + readLength);
    else
        addressResult.Content.setAddressStart(addressResult.Content.getAddressStart() + readLength * 16);
}

关键:地址自增要区分软元件类型——字软元件 +N,位软元件(按字单位读时)+N×16(1 字 = 16 位)。1E 帧的切片用 SoftBasic.SplitIntegerToArray(length, isBit ? 256 : 64)(MelsecA1ENet.java:187,实现在 BasicFramework/SoftBasic.java:369-379:按 everyLength 均分,最后一段取余数)。iot-communication 则用回调式 LoopGroupAlg.loopExecute(actualLength, maxLength, (off, len) -> ...)(McNetwork.java:382-393),每片新建 McDeviceAddress(code, headDeviceNumber + off, len) 再发一帧。

切片不是性能优化,是正确性要求:超过上限 PLC 会直接回 0xC051~0xC054,一条数据都拿不到。


13. UDP 版 SLMP(MC over UDP)

13.1 与 TCP 的差异

TCP 版需建连、保活、断线重连;UDP 版无连接、发完就走。报文结构完全相同;TCP 需按长度字段分包,UDP 天然一包一帧;TCP 保证不丢包与顺序,UDP 会丢、会乱序,需自己超时重传;UDP 吞吐略高(省握手与 ACK)。UDP 适用于高频、可容忍偶发丢包的遥测,采集平台默认建议 TCP。

源码:MelsecMcUdp 直接复用二进制打包函数(MelsecMcUdp.java:90-104):PackCommandWithHeader 调用 McBinaryHelper.PackMcCommand,UnpackResponseContent 校验结束代码后 BytesArrayRemoveBegin(response, 11)。报文字节流与 TCP 版完全一致——第 7 节的 6 个例子可直接用于 UDP。

13.2 自己要做的三件事

  1. 超时重传:建议 3 次重试,超时递增(500ms / 1000ms / 2000ms)。Hsl 的 MelsecMcUdp 只依赖 NetworkUdpDeviceBase 的收发超时,没有实现重传(源码未见重试逻辑),工业采集建议上层自己加重传或直接用 TCP。
  2. 请求-应答配对:3E 帧头部没有事务 ID(4E 帧才有 2 字节 serialNumber,见 McHeader4EReq.java:44-47)。建议优先用 4E 帧;用 3E 帧时校验应答的命令码回显或长度是否符合当前请求,不符则丢弃重读。
  3. 单帧不要太大:UDP 单包建议 ≤ 1472 字节(MTU 1500 − IP/UDP 头 28)。3E 头部 11 字节,所以数据区建议 ≤ 1400 字节 = 700 字,远小于 TCP 的 950 字上限。

14. 现场配置清单

14.1 以太网模块侧(GX Works2 / GX Works3)

配置项 要求 说明
打开方式 MC 协议(SLMP) 不能选"固定缓冲"或"套接字"
通信数据代码 二进制(推荐)或 ASCII 必须与驱动一致;不一致报 0x0054 / 0xC050
端口号 见 14.2 与驱动配置一致
站号 / 网络号 直连:网络号 00、PLC 编号 FF、站号 00 走 MELSECNET/H 或 CC-Link IE 时要改
运行中允许写入 勾选 否则 RUN 下写入报 0x0055
CPU 用户认证 禁用 否则报 0x430D
IP 过滤 / 白名单 加采集服务器 IP 否则 TCP 能连但报文被丢弃

14.2 端口号

端口 来源 说明
6000 iot-communication 常量 GeneralConst.MELSEC_PORT = 6000(common/constant/GeneralConst.java:62) 源码确认的默认端口
5000 / 5001 / 1025~1028 常见 Q 系列 E71、FX5U / iQ-R 配置示例 需实测确认

源码里能确认的只有 6000,其余是业界常见值,务必以现场 GX Works 配置为准。

14.3 CPU 监视定时器设得太长的后果

14.4 两个库的默认定时器对比

二进制:Hsl 0A 00 = 10 → 2.5s(McBinaryHelper.java:36);iot-communication monitoringTimer=3000 → 3000/250=12 → 0C 00 → 3.0s(McNetwork.java:93 + McHeader3EReq.java:60)。ASCII:Hsl "0010" = 16 → 4.0s(McAsciiHelper.java:318-321)。


15. 常见坑清单与小结

15.1 常见坑清单

# 坑 表现 规避
1 X/Y/B/W/SB/SW/DX/DY 是十六进制地址 读 X20 得到第 32 号而非第 20 号 按 getFromBase() 取进制
2 X/Y 以 0 开头被按八进制解析 X017 读到第 15 号点 禁止隐式八进制,统一十六进制
3 命令是小端 把 0x0401 写成 04 01 记住线上是 01 04
4 子命令 0x0000/0x0001 vs iQ-R 0x0002/0x0003 iQ-R 报 0xC059 按机型切换子命令
5 位写入的"点数"是位数而非字节数 写 4 个位写成点数 2 点数 = value.length,数据 = 压缩后字节
6 位数据 1 字节 2 位,0x10/0x01 按位取值得到全 0 用 & 0x10 / & 0x01 等值判断
7 应答数据长度含结束代码 2 字节 多读 2 字节,粘包错位 remainLength = dataLength - 2
8 请求数据长度从监视定时器算起 少算 2 字节,PLC 回 0xC057 L = 2 + core.length
9 超上限不切片 0xC051~0xC054 二进制 950 字 / ASCII 460 字
10 位软元件用字单位读时地址非 16 倍数 0x0058 对齐到 16
11 PLC 未勾"运行中写入" 0x0055 GX Works 勾选
12 二进制/ASCII 编码不匹配 0x0054 / 0xC050 模块侧与驱动侧统一
13 CPU 用户认证开启 0x430D 关闭认证
14 定时器设 0(无限等待) 线程永久阻塞 用 10~40,并加 socket 超时
15 1E 帧软元件码字节序 数据不对 0x4420 → 线上 20 44;不通则试 44 20
16 1E 帧无长度字段 粘包无法处理 用请求点数反推应答长度
17 D100.0 非原子 偶发位丢失 接受竞态;位软元件用原生位写
18 ZR 进制两库不一致 ZR 地址偏移错误 逐台实测(Hsl=10,iot=16)
19 ASCII 帧 I/O 编号是 03FF 字符串 误写成 FF03 ASCII 是大端字符串
20 UDP 无重传、3E 无事务 ID 偶发丢数据 自行重试;或用 4E 的 serialNumber

15.2 小结

MC 协议(SLMP)的核心可压缩成三句话:

  1. 3E 二进制请求 = 固定 11 字节头 + 10 字节起的指令区;50 00 是身份证、偏移 7 的 2 字节长度是粘包解药、0A 00 是 2.5 秒耐心。
  2. 命令小端、子命令定位/字、地址 3 字节小端、软元件码 1 字节、点数 2 字节小端;X/Y/B/W/SB/SW/DX/DY 的地址是十六进制,其余是十进制。
  3. 位数据 1 字节存 2 位(0x10 偶数位、0x01 奇数位),读超 950 字(ASCII 460)必须切片。

写采集驱动时,先用第 7 节的报文做字节级单元测试(构造请求 → 比对字节流 → 喂入假应答 → 比对解析结果),再上真机。这样 90% 的坑在办公室就能排掉。


本文是《工业通信与数据采集》栏目第 9/22 篇 · 上一篇:S7 协议(西门子 S7comm)详解 | 栏目总览 | 下一篇:HslCommunication 3.9.3 项目总览与架构