三菱 MC 协议(MELSEC Communication Protocol / SLMP)详解
本篇定位:面向有 Java 基础、正在做工业物联网平台(FastBee)数据采集的工程师。读完应能手工拼出一条 3E 二进制报文、手工拆开一条 PLC 应答、把位数据从三菱的"半字节压缩"格式还原成 boolean[],并知道现场最常踩的坑在哪。文中所有字节值均来自本地两份开源库源码(HslCommunication Java 3.9.3、iot-communication),已标注文件路径与行号。
目录
- 三菱协议家族:MC 协议与 SLMP 的关系
- 帧类型总览:1E / 2E / 3E / 4E 与 ASCII、二进制
- QnA 兼容 3E 帧二进制结构(逐字节拆解)
- 核心指令区:命令 + 子命令 + 软元件 + 地址 + 长度
- 软元件编码表(1 字节设备码)
- 地址编码:3 字节小端 + 十进制/十六进制陷阱
- 报文实例(6 个完整字节流)
- ASCII 帧:与二进制的逐字段对应关系
- 1E 帧(A 兼容):结构与实例
- 结束代码(错误码)对照表
- 地址语法与
D100.0位访问的实现 - 批量读上限与自动切片算法
- UDP 版 SLMP(MC over UDP)
- 现场配置清单
- 常见坑清单与小结
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 排错优先级(现场速查)
- 一上电就
0xC059→ 命令/子命令错 → 检查 iQ-R 是否用了0x0002/0x0003。 0xC051~0xC054→ 点数超限 → 加自动切片(见 12 节)。0xC05C/0xC060/0x0058→ 位/字单位与软元件类型不匹配 → 检查子命令。0xC056/0x0002→ 地址越界 → 检查 X/Y 的十六进制进制(见 6 节)。0x0055→ PLC 在 RUN 且不允许运行中写入 → GX Works 勾上"运行中允许写入"。0xC05E→ 定时器太短 → 把0A 00调大(如28 00= 10s)。0x0054/0xC050→ 二进制/ASCII 编码不匹配 → 改 PLC 侧通信数据代码设置。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 上限的来历(取三者最小值)
- 协议规定(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 自己要做的三件事
- 超时重传:建议 3 次重试,超时递增(500ms / 1000ms / 2000ms)。Hsl 的
MelsecMcUdp只依赖NetworkUdpDeviceBase的收发超时,没有实现重传(源码未见重试逻辑),工业采集建议上层自己加重传或直接用 TCP。 - 请求-应答配对:3E 帧头部没有事务 ID(4E 帧才有 2 字节
serialNumber,见McHeader4EReq.java:44-47)。建议优先用 4E 帧;用 3E 帧时校验应答的命令码回显或长度是否符合当前请求,不符则丢弃重读。 - 单帧不要太大: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 监视定时器设得太长的后果
- 设成
0000(无限等待):PLC 因 CPU 忙、多 CPU 切换、网络模块故障而不响应时,采集线程会永久阻塞 → 线程泄漏 → 任务堆积 → 平台假死。 - 设得过长(如 60s):超时后 PLC 仍在处理,此时重发可能触发 PLC 侧请求队列堆积。
- 建议:直连取
0x000A(2.5s)~0x0028(10s);走网络取0x0014(5s)~0x00F0(60s);并在驱动层再套一个 socket 超时。
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)的核心可压缩成三句话:
- 3E 二进制请求 = 固定 11 字节头 + 10 字节起的指令区;
50 00是身份证、偏移 7 的 2 字节长度是粘包解药、0A 00是 2.5 秒耐心。 - 命令小端、子命令定位/字、地址 3 字节小端、软元件码 1 字节、点数 2 字节小端;X/Y/B/W/SB/SW/DX/DY 的地址是十六进制,其余是十进制。
- 位数据 1 字节存 2 位(0x10 偶数位、0x01 奇数位),读超 950 字(ASCII 460)必须切片。
写采集驱动时,先用第 7 节的报文做字节级单元测试(构造请求 → 比对字节流 → 喂入假应答 → 比对解析结果),再上真机。这样 90% 的坑在办公室就能排掉。
本文是《工业通信与数据采集》栏目第 9/22 篇 · 上一篇:S7 协议(西门子 S7comm)详解 | 栏目总览 | 下一篇:HslCommunication 3.9.3 项目总览与架构