简介:《三菱MC协议配置和使用简析》是一份面向PLC通信初学者与工控调试人员的入门文档,重点解决外部设备通过MC协议读写三菱PLC寄存器时配置思路不清、报文格式难理解等问题。资源包共1个docx文件,大小约1.12MB,围绕TCP/IP通信展开,涵盖MC协议开放寄存器的目的、3E帧二进制收发格式、PLC内置以太网参数设置、向D7000写入H000C及连续读取5个软元件的示例,并整理软元件代码对应关系、指令与子指令列表和错误返回帧排查方法。已有4092人学习,适合希望快速掌握MC协议配置与报文解析、对照实操查漏补缺的读者。
1. 网线通了不等于协议通了:三菱 MC 协议最容易翻车的地方
网线插好、IP 配完、ping 得通、端口也 telnet 得开,不少人到这一步就认定三菱 MC 协议已经跑通了,结果第一次发帧收回来的是一串看不懂的十六进制,或者干脆一个字节都不回。这套协议本身不复杂,难的是三件套必须同时对上:PLC 侧内置以太网的打开设置要开 TCP 加 MC 协议并指定端口,上位机发的帧必须是 3E 帧二进制且两字节字段低位在前,软元件代码和起始地址要按手册的二进制表算。三样错一样,PLC 要么回错误帧,要么静默。这篇笔记按我实际配过的一套流程走:先定环境和帧格式,再配 PLC 参数,然后手搓帧验证读写,最后把踩过的坑和验证方法摊开写清楚。适合第一次对接 PLC 采集、手上只有一份手册和一根网线的人。
2. 环境先定死:3E 帧、二进制与 4999 端口的三件套
动手之前,先把「用什么帧、传什么格式、连哪个端口」这三件事写进纸面,后面所有配置都围着它们转。工控现场最常见的时间浪费不是配置难,而是配到一半才发现上位机按 ASCII 组帧、PLC 侧选的是二进制,或者两边端口号只差一位。这一章把选型说透,一次配对的概率能高很多。
2.1 帧格式选型:3E、3C、4C、4E 到底选哪个
MC 协议的帧格式有 3E、3C、4C、4E 几种。3C 属于早期 A 兼容系列的格式,头部字段宽度和 3E 不一样,新一点的设备基本不再用它;4C 与 3C 同源,差别在支持的能力范围;4E 可以理解为 3E 的扩展,把请求数据长度这类字段加宽了,一帧能带更多数据,适合大批量读写。3E 是当下最通用的一档,头部结构规整,各家上位机库、示例代码基本都以它为主。
我一般默认选 3E,理由不是它先进,而是排错资料最多。帧一旦发错,能对着手册一条一条比对字段;用 4C 这种冷门格式,出了问题连搜都不好搜。只有在单次要读写的软元件点数特别大、3E 一帧装不下需要频繁分包时,才值得切到 4E。具体差异以《MELSEC 通讯协议参考手册》的帧结构章节为准,手册里每种格式的发送帧、响应帧都画了字段图,比任何二手资料都准。
选型时还有一个容易被忽略的点:帧格式和「通信数据代码」是两件事。前者决定头部字段怎么排,后者决定字段值以二进制还是 ASCII 字符呈现。两者组合起来才是完整的一套,配 PLC 的时候要一起对。
2.2 二进制还是 ASCII:为什么推荐二进制
通讯内容分二进制和 ASCII 文本两种形式,传输的内容一致,只是表现形式不同。ASCII 形式下,帧里的每个字节都写成两个可读字符,用抓包工具一看就懂,调试时确实直观。代价是一帧长度明显更长,而且上位机发之前要做一次编码、收到之后要做一次解码,多两道转换就多两个出错点。
二进制形式一帧的数据长度更短,数据不需要转换,直接按字节发、按字节收,通讯效率更高。对周期采集的场景,比如 100ms 采一轮、每轮读几十个寄存器,这个差距会实打实地体现在轮询周期上。所以默认推荐二进制,我自己所有现场也都是二进制。
提示:手册里软元件代码表、指令代码表通常都分 ASCII 和二进制两张。查表前先确认自己翻的是哪一张,二进制表里 D 是 A8,ASCII 表里是「D*」这种两字符形式,拿错表算出来的地址一定是错的。
2.3 网络与端口预置清单
配置 PLC 之前,先把下面这张表填满。表中每一项都要能写出来,写不出来的先补,别急着开软件。
| 项目 | 本次取值 | 说明 |
|---|---|---|
| 帧格式 | 3E 帧 | 通用度最高,排错资料最多 |
| 数据形式 | 二进制 | 帧短、无需转换、效率高 |
| 传输层 | TCP/IP | 一发一收,上位机主动发起 |
| PLC 侧 IP | 与工控机同网段 | 例如 192.168.1.10,避开网关冲突地址 |
| 端口号 | 4999 | 打开设置里自选,注意别撞已占用端口 |
| 通信数据代码 | 二进制 | 必须与上位机一致 |
| 允许 RUN 中写入 | 勾选 | 不勾只能读,写入会回错误 |
端口号在 1024 以下的低段位很多被系统服务占着,选 4999 这种高位端口比较省事。真正的硬约束是:PLC 打开设置里填的端口号、上位机 socket 连接的端口号、现场防火墙放行的端口号,三个必须是同一个。这一条听起来废话,但我见过不止一次因为中间一台交换机做了端口过滤,排查了一下午。
2.4 上位机调试环境:Python 标准库就够
调试环境不用装什么重型工具,Python 3.x 加标准库的 socket 就能把收发链路跑通。这样做的好处是可控:每一个字节都是自己拼的,出错时能精确知道错在第几个字段,而不是被某个封装库的黑匣子挡在外面。等链路跑通、帧结构吃透了,再换成现成的通讯库提效也不迟。
先做一步连通性确认,只验证 TCP 能不能建起来:
# 确认工控机到 PLC 的 TCP 通路,nc 返回成功即端口可达 # -v 输出详细过程,-z 只扫不发送数据,-w 2 超时 2 秒 nc -vz 192.168.1.10 4999这条命令只说明「网络层和传输层通了」,不代表 MC 协议配对了。很多现场就卡在这个误判上:nc 显示 succeeded,就以为大功告成,其实 PLC 的打开设置里可能压根没开 MC 协议,只是端口被别的东西占着应答了。真正的验证必须发一帧真数据出去、收到一帧正确响应,这一步放到第 4 章做。
再准备一个最小的 socket 连接脚本,后面所有帧都往这个骨架里塞:
import socket PLC_IP = "192.168.1.10" # PLC 内置以太网口地址,与打开设置里一致 PLC_PORT = 4999 # 打开设置里配置的端口号 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) # 一发一收模式,必须设超时,否则收不到会一直阻塞 sock.connect((PLC_IP, PLC_PORT)) print("connected") # 这里后续填入具体的 3E 二进制帧 sock.close()settimeout是这套脚本里最关键的一行。MC 协议是一发一收,发出去之后如果 PLC 没回,没有超时的 socket 会一直挂着,程序看起来像假死,现场排查时极容易被误判成 PLC 死机。
3. PLC 侧配置:内置以太网端口设置到烧写生效
PLC 这边不需要写任何程序,只在工程软件的参数里改几处设置,然后把配置烧写进去。听起来简单,但每一步都有个默认值在等着坑人。这一章按操作顺序走一遍,顺带说明每个参数背后的含义。
3.1 从工程栏进入 PLC 参数
打开工程软件,在右侧工程栏中双击「PLC 参数」,弹出参数设置对话框。整个入口只有这一层,找不到多半是因为当前打开的是智能功能模块的参数页,不是 CPU 的参数页。
进入后先确认一件事:这台 PLC 的 CPU 型号带不带内置以太网口。带口的(比如常见的 Q 系列带内置以太网的型号、L 系列、iQ-R 系列)直接在内置以太网端口设置里配就行;不带口的,看 3.4 节挂模块的做法。这一步判断错了,后面怎么配都不会通,因为参数根本没有生效的物理通道。
3.2 内置以太网端口设置:IP、通信数据代码、允许 RUN 中写入
在弹框里选中「内置以太网端口设置」,这里要改三项:
第一项是 IP 地址。填一个和工控机同网段、且没被占用的地址。别用网段里的 .1 或 .254,那是网关和交换机的常驻位置,撞上了会出现「能 ping 通但不是 PLC 在应答」的玄学现象。
第二项是通信数据代码设置。这里选二进制,和上位机保持一致。这一项一旦选错,帧发过去 PLC 能收到、也会回,但回的是错误帧,很容易被误判成地址算错。
第三项是勾选「允许 RUN 中写入」。不勾这一项,PLC 在运行状态下所有写操作都会被拒,读操作则正常。表现就是「能读不能写」,很多人第一反应是帧格式错了,其实是权限没开。
3.3 打开设置:TCP 协议、MC 协议、端口号
点「打开设置」,在弹框第一栏里配置连接方式。这里有两个必选项:协议选 TCP,通信方式选 MC 协议。选完再填端口号,按自己需求来,比如 4999。
这三个值里,协议和通信方式选错是最致命的。选了 UDP,上位机用 TCP 连,握手阶段就直接失败;协议选了 TCP 但通信方式没选 MC 协议(选成其他开放方式),端口能连上,发 MC 帧过去对方不认,回的东西完全对不上。这种错最难查,因为 TCP 层一切正常,只有应用层在默默失败。
填完点「设置结束」,回到上一级,再点一次「设置结束」。两层弹框都要确认,只关掉一层的话参数并没有真正落到工程里,下一次编译烧写还是旧配置。
3.4 没有内置网卡时:网络参数里挂模块
如果 PLC 本身没有自带网卡模块,参数项里就不会有内置以太网端口设置,需要到「网络参数」里选择对应的以太网模块,按模块占用的槽位和起始 I/O 号添加,然后在该模块的设置里做同样的配置:IP、通信数据代码、打开设置里的 TCP 加 MC 协议加端口号。
流程和内置口基本一致,唯一要额外留心的是模块的 I/O 号。多模块系统里挂错槽位的情况不少见,配置写进去了,但写到了另一个空槽上,现象是参数看着都对、就是不通。添加模块后核对一下起始 I/O 号,和机架上实际插的位置对得上再往下走。
3.5 烧写与生效确认
配置改完必须烧写到 PLC 才生效。烧写的内容是参数区,不是程序区,操作时注意别把整个工程连程序一起覆盖掉——现场设备在跑的时候这么干,后果不用多说。
烧写完成后做两步确认:一是重新打开 PLC 参数,看刚才填的 IP 和端口还在不在,有些情况烧写失败但软件不报错,重开一次就能发现;二是按 2.4 节的 nc 命令扫一次端口,能通说明以太网口已经按新配置工作了。到这里 PLC 侧就算配完了,接下来的问题全部在上位机的帧上。
4. 组帧:3E 帧二进制从副头部到软元件代码
协议对接的核心就一件事:把要读写的软元件、地址、点数,按 3E 帧二进制的规则拼成一串字节。这一章拿两个真实用得到的例子走完:向 D7000 写 H000C,从 D7000 起连续读 5 个软元件。拼完再逐字段拆开讲。
4.1 发送帧骨架:头部由 TCP/IP 自动添加
先说清楚边界。发送帧的最前面一部分是 TCP/IP 协议栈自己加的,写代码时不用管;我们要拼的是 TCP 载荷里的应用层部分。3E 帧二进制下,这部分从副头部开始,依次是网络号、PLC 号、请求目标模块 I/O 编号、请求目标模块站号、请求数据长度、监视定时器,然后才是指令、子指令和软元件信息。
这里有一条贯穿全帧的硬规则:所有占两个字节的字段,低位字节在前、高位字节在后。软元件的起始地址占三个字节,同样是低位在前。这条规则记不住,后面每一帧都会算错,而且错得很隐蔽——地址 7000 的十六进制是 1B58,写成「1B 58 00」和「58 1B 00」在抓包工具里看起来都挺像那么回事。
监视定时器我一般填 0010,也就是 16,单位是 250ms,折算下来 4 秒。这个值决定 PLC 等上位机后续数据的时间,太长会拖慢异常发现,太短在网络抖动时会误超时,4 秒在产线环境里够用。
4.2 写 D7000 = H000C:一帧 24 字节
指令是向软元件 D7000 写入值 H000C,发送帧内容:
50 00 00 FF FF 03 00 0E 00 10 00 01 14 00 00 58 1B 00 A8 01 00 0C 00逐字段拆开看:
| 字节位置 | 内容 | 含义 |
|---|---|---|
| 1–2 | 50 00 | 副头部,请求帧固定 5000 |
| 3 | 00 | 网络号 |
| 4 | FF | PLC 号,FF 表示不指定 |
| 5–6 | FF 03 | 请求目标模块 I/O 编号,即 03FF |
| 7 | 00 | 请求目标模块站号 |
| 8–9 | 0E 00 | 请求数据长度,0x000E 即 14 字节 |
| 10–11 | 10 00 | 监视定时器,16 × 250ms = 4 秒 |
| 12–13 | 01 14 | 指令 0x1401,批量写 |
| 14–15 | 00 00 | 子指令 0000,按字(16 位)为单位 |
| 16–18 | 58 1B 00 | 起始软元件地址,0x1B58 = 7000,三字节低位在前 |
| 19 | A8 | 软元件代码,D 寄存器二进制代码 A8 |
| 20–21 | 01 00 | 软元件点数,1 个 |
| 22–23 | 0C 00 | 写入数据 H000C,16 位,低位在前 |
请求数据长度 0x000E 是从监视定时器开始数到帧尾的字节数:监视定时器 2 字节 + 指令 2 字节 + 子指令 2 字节 + 起始地址 3 字节 + 软元件代码 1 字节 + 点数 2 字节 + 数据 2 字节,正好 14。这个长度字段是自己算的,不是固定的,改点数、改数据长度都要重算,算错了 PLC 会直接回错误帧。
用 2.4 节的 socket 骨架把这帧发出去:
# 写 D7000 = H000C,3E 帧二进制 frame = bytes.fromhex( "500000FFFF03000E00100001 140000581B00A801000C00".replace(" ", "") ) sock.sendall(frame) # 一发一收,发完立刻等回 resp = sock.recv(1024) # 一帧回一帧,1024 字节足够容纳 print(resp.hex(" ").upper()) # 打成大写十六进制,方便和手册对照sendall而不是send,是因为大帧可能被拆成多次发送,send不保证一次发完。recv(1024)一次读到的通常是完整一帧,但这只是经验值,生产代码里应该先读头部两字节、再按长度字段读剩余部分,避免粘包。
4.3 响应帧:D0 00 00 FF FF 03 00 02 00 00 00 怎么读
写入成功后的响应帧:
D0 00 00 FF FF 03 00 02 00 00 00和请求帧结构对称:副头部变成 D000 表示响应,网络号、PLC 号、I/O 编号、站号原样返回,然后是响应数据长度和结束代码。这里的 02 00 表示响应数据长度 2 字节,紧接着的 00 00 就是结束代码,0000 代表正常结束。也就是说,判断一次写入是否成功,只看最后两个字节是不是 00 00 就够了,前面的字段基本是回显。
这条判据要记牢,因为它是后面所有排错的分水岭。很多人看到「有响应」就认为通了,实际上响应帧长度字段不是 2、结束代码不是 0000 的,全都是失败。失败时结束代码的具体含义要去对应型号的 PLC 手册里查,比如 L 系列手册里对每个错误码都有明确的描述,对着查比猜快得多。
4.4 批量读 5 个软元件:点数与响应数据长度
读取 D7000 开始的连续 5 个软元件,发送帧:
50 00 00 FF FF 03 00 0C 00 10 00 01 04 00 00 58 1B 00 A8 05 00和写入帧比,变化有三处:指令从 1401 变成 0401(批量读),请求数据长度从 0E 变成 0C(14 变 12,因为读请求不带写入数据),点数从 01 变成 05。起始地址和软元件代码完全不变,这也是实际开发中最省事的写法——读和写共用同一套地址计算逻辑。
响应帧:
D0 00 00 FF FF 03 00 0C 00 00 00 0C 00 00 00 00 00 00 00 00 00响应数据长度 0C 00 即 12 字节,其中结束代码占 2 字节,剩下 10 字节是 5 个软元件的值,每个 2 字节。按低位在前还原:0C 00 是 000C,也就是上一次写进去的那个数,后面四个都是 0000。这条响应同时验证了写入和读取两条链路,是我现场调试时必跑的一次闭环。
解析时注意,5 个软元件的值是紧挨着排的,没有分隔符。按 2 字节一组切分,再用int.from_bytes(chunk, "little")转成数值,切错一位后面全错。32 位指令(比如 DWORD 双字)占 4 字节,低字在前高字在后,和 16 位是同一个方向。
4.5 软元件代码与指令子指令对照
软元件代码是组帧里最容易查错表的一项。基本型 QCPU 里,软元件名称和代码在 ASCII 与二进制两种形式下是两套完全不同的值,手册里通常给两张表。下面这几个是二进制表里高频用到的:
| 软元件 | 二进制代码 | 说明 |
|---|---|---|
| D | A8 | 数据寄存器,最常用 |
| M | 90 | 内部继电器 |
| X | 9C | 输入继电器 |
| Y | 9D | 输出继电器 |
| B | A0 | 链接继电器 |
| W | B4 | 链接寄存器 |
其他型号(L 系列、iQ-R 系列等)的代码表不一定完全一致,换型号时按手册的二进制表重新核一遍,别照抄。指令方面,批量读是 0401、批量写是 1401,子指令 0000 表示按字(16 位)处理、0001 表示按位处理。读写位软元件(M、X、Y)时必须把子指令改成 0001,否则 PLC 会按字去解析地址,返回的要么是错误帧,要么是错位的数据。随机读、多块批量读这些指令等真的需要优化轮询次数时再查手册引入,一开始用批量读写足够。
5. 避坑与排查:错误帧、字节序和那根看着没问题的网线
现场大部分「配不通」最后都能归到几类固定原因上。这一章按我遇到的顺序列,每条按现象、原因、解决写,方便对照。
5.1 先看响应帧,再看别的
排错顺序错了会比错误本身更耗时间。我习惯的第一步永远是:把发出的帧和收到的帧都打成十六进制打印出来,对照手册逐字段比对。先看副头部是不是 D000,再看响应数据长度是不是预期值,最后看结束代码是不是 0000。这三步能在两分钟内把问题范围缩到「帧格式」「地址」「权限」三者之一。
反过来,上来就怀疑网线、交换机、防火墙,往往查了两小时发现帧里地址少写了一字节。链路层的问题特征很明确:连不上、连上就断、超时无响应。只要收到了任何一帧数据,说明链路是通的,问题一定在帧内容或 PLC 参数上。
5.2 五条踩坑记录
现象一:nc 扫描端口成功,但发帧后完全无响应,超时断开。原因:端口被 PLC 上别的服务占着,或者打开设置里通信方式没选 MC 协议,PLC 收到帧后不认识,直接丢弃不回。 解决:回 PLC 参数里逐层核对打开设置的两层弹框是否都点了「设置结束」,协议是否 TCP、通信方式是否 MC 协议,改完重新烧写。别在 nc 通就以为万事大吉。
现象二:能正常读数据,写操作一律返回非 0000 的结束代码。原因:「允许 RUN 中写入」没勾选,PLC 在运行状态下拒绝所有写请求;或者子指令写成了 0000 但目标是位软元件。 解决:勾上 RUN 中写入并重新烧写;写 M、X、Y 这类位软元件时把子指令改成 0001,并按位地址换算,不要直接用字地址。
现象三:写入的值和读回来的值对不上,比如写 H000C 读回来 H0C00。原因:字节序搞反了。两字节字段低位在前,写成高位在前在十六进制上看不出问题,但数值全错。 解决:把起始地址、点数、写入数据这三处的字节顺序统一改成低位在前。地址 7000 必须是 58 1B 00,不是 00 1B 58。这个错属于血泪经验级别的常见,写完帧先自己按字节倒推一遍再发。
现象四:小批量读写正常,一次读几十个点就报错或数据截断。原因:请求数据长度字段没跟着点数重算,PLC 按长度字段解析,长度对不上就判定帧格式错误;或者一帧数据量超过了 3E 帧的承载范围。 解决:每次改点数都重新算一遍请求数据长度,公式是固定字段字节数加数据字节数;点数确实很大时拆成多次批量读,或者评估切到 4E 帧。
现象五:换了一台同型号 PLC,参数照抄过去却读不到数。原因:软元件代码或起始地址沿用了上一台的算法,但型号或系列不同,二进制代码表不一样。 解决:换设备时把软元件代码表重新查一遍,别复制粘贴。我一般的做法是把型号、软元件代码、地址算法写在同一段注释里,换设备时强制自己看一遍。
6. 装完怎么验证:三条命令跑通读写闭环
配置和组帧都完成后,别急着往上叠业务代码,先用一段最小脚本把「写—读—比对」这个闭环跑通。跑通了,后面所有采集逻辑都是在这上面加软元件和轮询,跑不通,加再多代码也是把错误放大。
验证逻辑很直接:先向 D7000 写一个已知值,再读回来,比对数值是否一致。选 D7000 是因为它是数据寄存器,不参与设备实际动作,写坏了也不影响产线;如果你手上的 PLC 这个地址已被占用,换一个空闲的 D 区地址,改帧里的地址三个字节即可。
import socket PLC_IP, PLC_PORT = "192.168.1.10", 4999 def send_frame(frame_hex): """一发一收:发一帧、收一帧,返回十六进制字符串""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(3) # 无响应时快速失败,不阻塞 s.connect((PLC_IP, PLC_PORT)) s.sendall(bytes.fromhex(frame_hex)) return s.recv(1024).hex(" ").upper() # 第一步:写入 D7000 = H000C w = send_frame("500000FFFF03000E00100001 140000581B00A801000C00".replace(" ", "")) print("write:", w) assert w.endswith("00 00"), "写入失败,结束代码非 0000" # 第二步:从 D7000 起读 5 个软元件 r = send_frame("500000FFFF03000C00100001 040000581B00A80500".replace(" ", "")) print("read :", r) # 第三步:按 2 字节一组、低位在前解析,比对第一个值 data = bytes.fromhex(r.replace(" ", ""))[11:] # 跳过 11 字节头部与结束代码 vals = [int.from_bytes(data[i:i+2], "little") for i in range(0, 10, 2)] assert vals[0] == 0x000C, f"回读不一致:{vals[0]:#06x}" print("closed loop ok:", [hex(v) for v in vals])assert这两行是这段脚本的价值所在。很多人验证时只 print 不判断,看到有响应就收工,结果后面业务代码里跑了两周才发现读出来的一直是零。把判断写死在验证脚本里,跑一次就知道通不通。
几个验证项的期望现象和不通过时的排查方向:
| 验证项 | 期望现象 | 不通过先看哪里 |
|---|---|---|
| TCP 连接 | 秒连,不超时 | IP、端口、打开设置里的协议 |
| 写入响应 | 响应帧以 00 00 结尾 | RUN 中写入、指令 1401、子指令 |
| 读取响应 | 响应数据长度 0C 00,共 12 字节 | 帧长度字段、点数换算 |
| 回读数值 | 与写入值一致,低位在前 | 字节序、软元件代码表 |
| 连续轮询 | 多次收发稳定,无粘包错位 | recv 读取策略、监视定时器 |
有一条容易被忽略:验证不要只跑一次。连发二十次,看有没有偶发超时或数据错位。产线上真正让人头疼的从来不是一次性配不通,而是跑几小时才出一次的数据漂移,而这类问题的种子往往在第一次验证时就埋下了。
从那以后我每次对接新设备,都强制先跑这套「写一个已知值、读回来比对、连发二十次」的闭环,通过了才动业务代码。省下来的不是调试时间,是后面被叫到现场排查「数据偶尔不对」的时间。希望帮到你。
本文还有配套的精品资源,点击获取