简介:面向工控自动化初学者与需要对接三菱PLC的软件开发者,这份文档系统梳理了MC协议从原理到落地的完整脉络,帮助读者解决外部设备读写PLC内部寄存器的通信难题。包内为1个docx文件,压缩包约1.12MB,内容涵盖协议目的、TCP/IP下一发一收的通信方式、3E帧二进制格式解析,以及在PLC中通过内置以太网端口完成参数设置的具体步骤。文中以向D7000写入H000C、读取连续5个软元件为例,逐字节拆解发送帧与响应帧,并附软元件代码对照、指令与子指令列表及错误帧排查方法,便于对照手册定位问题。已有4095人学习,适合入门者快速建立MC协议通信的整体认知。
1. 三菱 MC 协议 3E 帧二进制通讯:从 PLC 参数设置到收发帧跑通
上周帮一个做产线数据采集的 A 同学排查问题,现象很典型:上位机 TCP 连上了 PLC,端口也通,但发出去的帧要么石沉大海,要么回一帧错误码,折腾一下午没定位到原因。最后发现是帧头里两个字节的字节序写反了——MC 协议里两个字节的部分低位字节在前、高位字节在后,这个坑几乎每个第一次接三菱 MC 协议的人都会踩。三菱 MC 协议(MELSEC Communication Protocol)的作用就是把 PLC 内部寄存器开放给外部设备,让上位机、工控机、边缘网关能直接读写 D、M、X、Y 这些软元件,不用在 PLC 里额外写通讯程序。它支持 485 和 TCP/IP 两种链路,这篇只讲 TCP/IP 下的 3E 帧、二进制格式,因为这是目前产线采集里最常见、也最推荐的一种组合。适合刚接手三菱 PLC 数据采集、正卡在“连得上但读不到数”的开发者。
2. 装前把环境列清:3E 帧、二进制与软元件代码怎么选
2.1 帧格式选型:3E 为什么是默认答案
MC 协议的帧格式有 3E、3C、4C、4E 几种,差别主要在帧头结构和是否带请求目标信息。3E 帧是带网络号、PC 号、请求目标模块 IO 号、请求目标模块站号这一组目标信息的,适合走以太网、跨站访问;3C 帧更精简,常见于串口场景。4E 帧是在 3E 基础上扩展了站号位数,用于更大规模的网络。具体差异以《MELSEC 通讯协议参考手册》为准,但落到实际选型上,我一般会这么定:
| 帧格式 | 典型链路 | 帧头特点 | 适用场景 |
|---|---|---|---|
| 3E | TCP/IP 以太网 | 带网络号/PC 号/IO 号/站号 | 产线采集、上位机直连,首选 |
| 3C | 串口 / 485 | 帧头更短 | 老设备、点对点 |
| 4E | 以太网多站 | 站号位扩展 | 多级网络、跨网段 |
选 3E 的理由很直接:现在 PLC 基本都带内置以太网口,TCP/IP 直连最省事,而 3E 是这套链路上文档最全、示例最多的格式。4E 不是不能用,但如果你只是单台 PLC 采集,用 4E 属于给自己加复杂度。
2.2 二进制还是 ASCII:效率账要算清
通讯内容分二进制和 ASCII 文本两种,两者传输的语义完全一致,区别只在编码形式。ASCII 形式把每个字节拆成两个可读字符,一帧长度大约翻倍,而且上位机收到后还要做一次字符到数值的转换;二进制形式直接按字节传,帧更短、不用转换,效率明显更高。所以这篇统一按二进制走。
这里有个容易忽略的连带影响:软元件代码在两种形式下取值不同。比如基本型 QCPU 里,D 寄存器在二进制下是0xA8,在 ASCII 下是字符D加一个填充位。你一旦定了二进制,后面所有软元件代码、读写指令的子指令都要按二进制那张表查,不能混着抄网上的 ASCII 示例,这是新手翻车的高发区。
2.3 软元件代码对照:抄错一个字节就全错
以基本型 QCPU 为例,二进制形式下常用软元件的代码是这样的(其他机型查对应手册):
| 软元件 | 二进制代码 | 说明 |
|---|---|---|
| D(数据寄存器) | 0xA8 | 16 位,最常用 |
| M(内部继电器) | 0x90 | 位软元件,按位读写 |
| X(输入继电器) | 0x9C | 位软元件 |
| Y(输出继电器) | 0x9D | 位软元件 |
| W(链接寄存器) | 0xB4 | 16 位 |
这张表建议直接抄进你的代码常量里,别每次现查。我见过有人把 D 写成0xA9,PLC 返回的错误帧里错误码指向“软元件代码异常”,但如果不查手册根本看不懂那串十六进制是什么意思。
3. PLC 侧参数配置:不写一行程序,只烧参数
3.1 内置以太网端口设置的四步
MC 协议在 PLC 侧不需要写梯形图程序,纯靠参数配置开放端口。以常见的工程软件操作为例,步骤是固定的:
- 在右侧工程栏双击“PLC 参数”。
- 弹框里选“内置以太网端口设置”,先设 IP 地址、子网掩码、默认网关,让 PLC 和你上位机在同一网段。
- 在同一页面找到“通信数据代码设置”,选二进制;勾选“允许 RUN 中写入”,否则 PLC 运行状态下写操作会被拒绝。
- 点“打开设置”,第一栏协议选 TCP,选 MC 协议,端口号按自己需求填,比如 4999,逐级“设置结束”,最后把新配置烧写到 PLC。
烧写完成后建议断电重启一次,让以太网参数彻底生效。如果 PLC 本身没有内置网卡,需要在“网络参数”里选对应的以太网模块做类似设置,逻辑一样,只是入口不同。
3.2 端口与连接数:别把 4999 当唯一解
端口号 4999 只是示例,实际项目里我一般会避开常见服务端口,选一个 5000 以上的空闲端口,并在上位机侧做成配置项而不是硬编码。另外要注意 PLC 对同时连接数是有限制的,内置以太网口通常只允许少量 TCP 连接同时存在。如果你的采集程序有重连逻辑,务必在断开时正常关闭 socket,否则旧连接没释放、新连接建不上,现象就是“第一次能读,重启程序后连不上”,很多人会误判成网络问题。
3.3 用一条命令先验端口通不通
参数烧完后,别急着上业务代码,先用系统自带工具确认 TCP 层是通的:
# 测试 PLC 的 MC 协议端口是否可连,4999 换成你实际设置的端口 nc -vz 192.168.1.10 4999nc -vz只做连接探测,不发业务帧,返回 succeeded 说明 IP、端口、防火墙这一层没问题。如果这一步就失败,先查网段、查 PLC 是否 RUN、查有没有别的设备占着连接数,别往下写帧解析,否则会把网络问题误当成协议问题。
4. 收发帧拆解:写 D7000 与读连续 5 个寄存器
4.1 写单个寄存器:0x000C 写进 D7000
先看向软元件 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逐段拆开看,这是理解 3E 帧的关键:
| 字节段 | 内容 | 含义 |
|---|---|---|
| 50 00 | 0x0050 | 副头部,3E 帧固定 |
| 00 | 0x00 | 网络号 |
| FF | 0xFF | PC 号 |
| FF 03 | 0x03FF | 请求目标模块 IO 号 |
| 00 | 0x00 | 请求目标模块站号 |
| 0E 00 | 0x000E | 请求数据长度(后续字节数) |
| 10 00 | 0x0010 | 监视定时器 |
| 01 14 | 0x1401 | 指令 0x1401 = 批量写 |
| 00 00 | 0x0000 | 子指令,按字为单位 |
| 58 1B 00 | 0x001B58 | 起始软元件地址,D7000 的偏移 |
| A8 | 0xA8 | 软元件代码,D 寄存器(二进制) |
| 01 00 | 0x0001 | 写入点数,1 个 |
| 0C 00 | 0x000C | 写入数据,即 H000C |
PLC 正常返回:
D0 00 00 FF FF 03 00 02 00 00 00D0 00是响应副头部,02 00表示后续只有 2 字节,末尾00 00是结束代码,全 0 代表成功。写操作不看数据区,只看结束代码。
4.2 读连续 5 个寄存器:0x0401 批量读
接着读 D7000 开始的连续 5 个软元件,其中第一个值就是上一次写入的 0x000C。发送帧:
50 00 00 FF FF 03 00 0C 00 10 00 01 04 00 00 58 1B 00 A8 05 00和写帧对比,差异集中在三处:请求数据长度从0E 00变成0C 00(少了写入数据那 2 字节);指令从01 14变成01 04(0x0401 批量读);点数从01 00变成05 00。响应帧:
D0 00 00 FF FF 03 00 0C 00 00 00 0C 00 00 00 00 00 00 00 00 000C 00是响应数据长度,00 00是结束代码,之后每 2 字节一个寄存器值,依次是0C 00、00 00、00 00、00 00、00 00,正好 5 个。注意每个 16 位值同样是低位在前,解析时要按小端拼。
4.3 用 Python 把帧拼出来并解析
手拼十六进制容易错,实际项目里我一般用struct按字段打包,可读性和可维护性都好得多:
import socket import struct def build_write_frame(addr: int, value: int) -> bytes: # 3E 帧头:副头部 0x0050 + 网络号/PC号/IO号/站号 header = struct.pack('<H', 0x0050) + bytes([0x00, 0xFF]) + struct.pack('<H', 0x03FF) + bytes([0x00]) # 请求数据:定时器 + 指令0x1401 + 子指令0 + 起始地址 + 软元件代码0xA8 + 点数 + 数据 payload = struct.pack('<H', 0x0010) + struct.pack('<H', 0x1401) + struct.pack('<H', 0x0000) payload += struct.pack('<I', addr)[:3] # 起始地址占 3 字节,小端 payload += bytes([0xA8]) + struct.pack('<H', 1) + struct.pack('<H', value) return header + struct.pack('<H', len(payload)) + payload def parse_response(data: bytes): # 跳过副头部(2)+网络号(1)+PC号(1)+IO号(2)+站号(1)+长度(2)+结束代码(2) end_code = struct.unpack('<H', data[9:11])[0] if end_code != 0: return None, end_code values = [struct.unpack('<H', data[i:i+2])[0] for i in range(11, len(data), 2)] return values, 0 s = socket.create_connection(('192.168.1.10', 4999), timeout=3) s.sendall(build_write_frame(7000, 0x000C)) resp = s.recv(256) print(parse_response(resp)) # 写成功返回 ([], 0) s.close()struct.pack('<H', ...)里的<就是小端,正好对应 MC 协议“低位在前”的要求,比手写字节序可靠。起始地址那行用struct.pack('<I', addr)[:3]取低 3 字节,是因为 3E 帧的软元件地址固定占 3 字节,D7000 的偏移 0x1B58 正好塞得下。结束代码非 0 时不要猜,直接拿错误码去查对应机型手册,比如 LCPU 系列手册里对每个错误码都有明确描述。
5. 避坑与排查:五条血泪记录
5.1 现象:TCP 连得上,发帧后无响应
原因通常是帧头副头部或请求数据长度写错,PLC 收到后判定帧格式非法,直接丢弃不回。解决:先确认副头部是50 00,再核对“请求数据长度”字段是否等于其后实际字节数,差一个字节都不行。
5.2 现象:返回帧结束代码非 0,提示软元件代码异常
原因多是二进制和 ASCII 的软元件代码混用,比如把 D 写成 ASCII 的0x44。解决:统一按二进制表取值,D 用0xA8,并在代码里做成常量,杜绝手写。
5.3 现象:写操作被拒绝,读正常
原因是 PLC 参数里没勾“允许 RUN 中写入”。解决:回工程软件的内置以太网端口设置里补勾,重新烧写参数并重启 PLC。这个选项默认不勾,是新手最容易漏的一步。
5.4 现象:第一次采集正常,程序重启后连不上
原因是旧 TCP 连接没正常关闭,PLC 连接数被占满。解决:采集程序里加try/finally保证 socket 关闭,重连前先做一次连接探测,别盲目重试。
5.5 现象:读回的数据高低字节颠倒
原因是解析时按大端拼了 16 位值。解决:所有两字节字段一律小端解析,用struct.unpack('<H', ...),别用int.from_bytes(..., 'big')。
6. 装完怎么验证跑通:三步闭环与一个习惯
验证不要一上来就跑业务逻辑,按“链路 → 单点写 → 连续读”三步闭环,出问题能立刻定位到层。
第一步,链路层用nc -vz确认端口可连,这一步过了才说明 IP、端口、PLC 运行状态没问题。第二步,用 4.1 的写帧向 D7000 写一个已知值,只看结束代码是否为 0,确认指令、软元件代码、地址偏移这套组合是对的。第三步,用 4.2 的读帧读回连续 5 个寄存器,确认第一个值等于刚写入的值,同时检查后面 4 个值的字节序有没有颠倒。
三步都过之后,再拿下面这张对照表做一次回归,把每个字段和实际抓到的帧对一遍:
| 检查项 | 期望值 | 出错时的典型表现 |
|---|---|---|
| 副头部 | 50 00 | 无响应 |
| 请求数据长度 | 等于后续字节数 | 无响应或错误帧 |
| 指令(写/读) | 0x1401 / 0x0401 | 错误帧提示指令异常 |
| 软元件代码 D | 0xA8 | 错误帧提示软元件异常 |
| 结束代码 | 00 00 | 非 0 需查手册 |
我一般还会在采集程序启动时强制走一遍“写一个测试值再读回”的自检,值对不上就直接拒绝启动,而不是带着错帧去跑业务。这个习惯是被一次现场事故逼出来的:当时字节序错了,采集上来的温度值全是乱的,但程序没报错,等发现时已经存了半天脏数据。从那以后我每次接新的 PLC 型号,都强制先跑一遍写读闭环再放行。希望这套流程能帮到你,少走那半天的弯路。
本文还有配套的精品资源,点击获取