☰
三菱PLC MC协议通信实战:3E帧结构解析与Python读写实现
2026/10/15 7:24:36 网站建设 项目流程

简介:三菱MC协议配置与使用简析是一份面向PLC工程师与自动化初学者的入门文档,重点解决外部设备通过TCP/IP读写三菱PLC内部寄存器的通信配置问题。文档解释了MC协议的作用,对比485与TCP/IP两种承载方式,并围绕3E帧格式讲解二进制与ASCII两种传输形态的差异与效率优劣;同时给出PLC内置以太网端口的具体参数设置流程,配有写入D7000、连续读取软元件等典型报文实例及逐字段解析,让读者能直接对照报文格式进行调试。资源为单份docx文档,共1.12MB,结构紧凑,内容涵盖协议目的、通讯格式、软元件代码表、指令列表及错误返回帧的排查方法,适合在学习MC协议通信时作为速查与实操参考。该资源已有4091人浏览学习,尤其适合需要快速上手三菱MC协议并完成以太网通信配置的工程技术人员。

1. 从“读不到数据”说起:MC协议到底解决什么问题

产线上位机要往PLC的D100里写一组配方参数,最快也最稳的路径不是OPC、不是组态软件,而是直接走TCP连上PLC的以太网口,按三菱MC协议的帧格式收发数据。这条路径没有中间服务、没有授权费用,整套通信逻辑就是“拼帧—发送—收帧—解析”,但恰恰因为太底层,一个字节写错就能让PLC沉默到底,连错误信息都不给。这就是MC协议最真实的脾气:配通了你感觉不到它存在,配不通它让你怀疑网线、怀疑IP、怀疑人生。

我写这篇笔记的原因很直接:这些年帮不同产线做过上位机与三菱PLC的通信对接,发现MC协议的坑高度集中,翻来覆去就那么几类。标题里那份docx只是资料名,真正要掌握的是帧结构、PLC侧参数和几行能跑的读写代码。文章从3E帧拆起,走完PLC配置和Python实现,最后把高频报错一次排清,新手能照着做,熟手也能顺手核对几个边界参数。

2. 把3E帧拆开:每个字节都有它的用途

MC协议家族里现在最通用的是3E帧,也叫Qna兼容3E帧。它不挑CPU系列,Q、L、iQ-R走以太网基本都是它;你在新手册里看到的SLMP叫法,报文和这套3E帧也是同一族。3E帧没有校验和字段,头部和请求数据拼完直接发,所以拼错一个字节,PLC要么不响应,要么返回一个让你猜半天的错误码。

2.1 帧头与请求数据:一张表看清整帧骨架

一个完整的3E帧由两部分组成:固定9字节的帧头,加上长度可变的请求数据。帧头里真正需要每次确认的只有最后两字节“请求数据长度”,其他字段在单机单CPU场景下全是默认值,不用动。

字段字节数常见值说明
子头20x50 0x00二进制帧固定值
网络号10x00单机默认0
PC号10xFF直接指定PC
请求目标模块IO号20x03 0xFFCPU本体以太网口
请求目标模块站号10x00单站默认0
请求数据长度2按实际计算只算请求数据,不算帧头9字节
请求数据N因指令而异读/写指令、地址、数据

很多人第一次拼帧就栽在“请求数据长度”上,它只统计请求数据字段的字节数,不是整帧长度。比如一条“读D100开始的10个字”的请求数据是10字节,长度字段就写0x000A,整帧发出去是19字节,但长度字段里永远是10。因为3E帧没有校验和,这个长度就是PLC判断请求边界的唯一依据,算错一字节,后面全乱。

2.2 请求数据里的地址编码:设备代码和软元件地址怎么换算

请求数据的核心是“指令(2字节)+子命令(2字节)+目标软元件地址(4字节)+点数(2字节)”。其中地址部分不是直接写十进制,而是拆成“1字节设备代码 + 3字节小端地址”。

常见的软元件设备代码如下表。D寄存器用得最多,M、X、Y在需要读点位时也常用:

软元件设备代码说明
D0xA8数据寄存器,16位
M0x90内部继电器(位)
X0x9C输入继电器(位)
Y0x9D输出继电器(位)
W0xB4链路寄存器
ZR0xB0文件寄存器(扩展)

地址换算规则:把十进制地址转成十六进制,再按低字节在前的顺序填满3字节。以D100为例,100转十六进制是0x0064,填成三字节就是64 00 00,前面加上设备代码0xA8,最终地址部分为 A8 64 00 00。位软元件X/Y/M的地址要按“每16点一组”折算,具体组号查对应CPU手册的软元件编号表,这个规则和字软元件不一样,用到时务必先查手册再拼帧。

下面这条Python片段展示了读D100开始的10个字时,整帧的构造逻辑,逐字节写出来比用库更直观:

import struct head = bytes([0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00]) # 9字节帧头前7字节 req_data = ( b'\x04\x01' # 指令:0401 批量读取(字单位) b'\x00\x01' # 子命令:0001 按字读取 b'\xA8' # 设备代码:D寄存器 bytes([0x64, 0x00, 0x00]) # 地址100 = 0x0064,小端三字节 + struct.pack('<H', 10) # 读取点数 10 个 ) length = struct.pack('<H', len(req_data)) # 请求数据长度,小端 frame = head + length + req_data

代码里head只拼了8字节,你数一下会发现差一字节:subheader占2、网络号占1、PC号占1、IO号占2、站号占1,这7字节后紧跟着的是length占2,所以head实际是前7字节,length单独拼在后面,合计9字节。这块容易看花眼,但对照2.1的表就清楚。req_data里指令0401代表批量读取字软元件,子命令0001是字单位;如果要读位软元件,子命令改成0000,指令不变。struct.pack('<H', ...)表示按小端打包两个字节,三菱协议里的多字节数值都走小端,后面解析响应时同样要用小端解包。

3. PLC侧参数:配不好一切都白搭

帧拼得再漂亮,PLC侧参数不对也白搭。MC协议通信绝大多数“连不上”问题,最后都查到PLC内置以太网设置上。这一章我按GX Works系列软件的操作路径讲一遍,Q、L、iQ-R系列界面大同小异,菜单层级基本一致。

3.1 内置以太网参数:IP、协议开关和端口号

先进入PLC参数界面,找到“内置以太网端口设置”,点“打开设置”进入详细配置,核心就四项:

参数项推荐值说明
IP地址与上位机同网段,如192.168.1.10不能和上位机、其他设备冲突
子网掩码255.255.255.0与上位机一致
通信协议支持勾选“MC协议”对应SLMP,报文一致
端口号自定义,如5000避开常用端口,不与其他服务冲突

这里要注意,“MC协议”这个开关在部分CPU型号里不是默认打开的,你从GX Works里打开设置后要专门勾上。还有个选项叫“允许RUN中写入”,默认往往是不允许,如果上位机需要在PLC运行状态下写D寄存器,必须一并勾选,不然后面写操作会直接失败。端口号建议固定成某个内部约定值,比如5000,现场多台设备对接时约定一致,省得以后排查“明明IP能通但TCP连不上”。

改完这些参数后,别急着关界面,逐项核对三遍再现网下载。尤其IP地址改过的情况下,下载完成后旧IP立即失效,你会看到以太网连接断开,这是正常现象,不是设备坏了。

3.2 改完参数为什么连不上:下载与复位的完整路径

参数修改后必须走“在线→PLC写入→参数”,写入后还要让CPU复位一次才能生效。常见做法是先在GX Works里把修改好的参数写入PLC,然后对CPU执行软复位,或者直接断电重启,二选一都行,但复位这步不能省。只改工程文件不下载,PLC跑的还是旧参数,这是新手最容易翻车的一步。

如果修改了IP地址,下载时要用USB或编程口连接,因为以太网连接在IP切换瞬间已经断开了。我遇到过一次现场调试,工程师用网线下载参数,改了IP后通信断掉,误以为PLC坏了,其实是新IP已经生效,他还在用旧IP去找这台设备。

参数生效后,先用ping验证网络层通不通,再用telnet或简单socket脚本验证端口通不通。只通IP不通端口,问题大概率在PLC侧协议开关没打开;IP都不通,优先查网线、IP冲突和子网掩码。

3.3 帧格式选二进制还是ASCII:一个影响排错体验的决定

PLC参数里还能选“二进制代码”或“ASCII代码”。生产环境我一般选二进制,帧长短、解析快、代码简单。ASCII模式的优势是帧内容全部可打印,用串口调试工具肉眼看报文很方便,但帧长接近两倍,解析时还要做字符串转十六进制,代码里多一层转换。

一个实用习惯:调试阶段如果反复对不上报文,可以把PLC临时切成ASCII模式,用串口工具或抓包软件直接读出帧内容,人工核对指令码和地址,定位完再切回二进制。ASCII模式下每个字节都变成两个十六进制字符,比如二进制子头0x50 0x00会变成字符“5000”,一眼就能看出帧结构有没有拼错。但要注意,切换帧格式后PLC参数也要重新下载复位,不能只改上位机代码。

4. 上位机读写实现:最小可跑通的Python客户端

帧结构和PLC参数都清楚了,剩下的就是写代码。用Python做原型最快,一个socket连接、两个函数就能跑通读写。这一章给出最小可运行实现,再把它封装成能复用的通用函数。

4.1 最小实现:构造请求帧、解析响应

先写一个能读取D寄存器的小客户端。代码不长,但已经覆盖了TCP连接、请求帧构造、精确接收响应、结束码判断四条核心链路:

import socket import struct PLC_IP = "192.168.1.10" PLC_PORT = 5000 def recv_exact(sock, n): """精确接收n字节,避免TCP粘包拆包导致的数据不完整""" buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise RuntimeError("连接被对端关闭") buf += chunk return buf def build_read_req(dev_code, start_addr, count): """构造批量读取请求帧""" head = bytes([0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00]) req_data = ( b'\x04\x01\x00\x01' + bytes([dev_code]) + start_addr.to_bytes(3, 'little') + struct.pack('<H', count) ) return head + struct.pack('<H', len(req_data)) + req_data def read_words(sock, dev_code, start_addr, count): req = build_read_req(dev_code, start_addr, count) sock.sendall(req) head = recv_exact(sock, 9) resp_len = struct.unpack('<H', head[7:9])[0] # 响应体长度 body = recv_exact(sock, resp_len) end_code = struct.unpack('<H', body[0:2])[0] if end_code != 0: err = struct.unpack('<H', body[2:4])[0] raise RuntimeError(f"PLC返回错误码: {hex(err)}") # 从第2字节起每2字节一个小端int16 return [struct.unpack('<h', body[i:i+2])[0] for i in range(2, len(body), 2)] with socket.create_connection((PLC_IP, PLC_PORT), timeout=3) as s: values = read_words(s, 0xA8, 100, 10) print(values)

recv_exact这个函数在MC协议通信里很重要,因为TCP是流式协议,PLC返回的响应可能一次收不全,也可能和后一条粘在一起,必须按长度字段精确取数。响应帧的头部和请求帧一样是9字节,长度字段在head[7:9],解析响应体时先读这个长度,再完整收下响应体。结束码0x0000表示正常,0xFFFF表示异常,异常时紧接着的两字节是错误码,常见的有C051、C057、C0D0等,这部分在下一章展开。

read_words里struct.unpack('<h')按有符号16位解析,对应D寄存器的默认数据类型。如果D寄存器里存的是无符号数或32位浮点数,解析方式要相应调整,这也是后面避坑章里会提到的高频问题。

4.2 把读写封装成通用函数:设备代码和地址参数化

4.1的read_words已经能把设备代码和起始地址作为参数传入,扩展到M、W、ZR只需换设备代码。写入一侧同样套路,构造写请求帧时把数据区追加到请求数据末尾:

def build_write_req(dev_code, start_addr, values): head = bytes([0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00]) req_data = ( b'\x04\x02\x00\x01' # 0402 批量写入,子命令 0001 字单位 + bytes([dev_code]) + start_addr.to_bytes(3, 'little') + struct.pack('<H', len(values)) ) payload = b''.join(struct.pack('<h', v) for v in values) return head + struct.pack('<H', len(req_data) + len(payload)) + req_data + payload def write_words(sock, dev_code, start_addr, values): req = build_write_req(dev_code, start_addr, values) sock.sendall(req) head = recv_exact(sock, 9) body = recv_exact(sock, struct.unpack('<H', head[7:9])[0]) end_code = struct.unpack('<H', body[0:2])[0] if end_code != 0: err = struct.unpack('<H', body[2:4])[0] raise RuntimeError(f"PLC返回错误码: {hex(err)}")

注意build_write_req里长度字段计算的是req_data加payload的总字节数,而不是只算req_data。这是写指令最容易错的地方,后面避坑章会单独讲。写入指令的结束码只有两字节,没有数据区,所以write_words拿到响应体后直接判断结束码。

实际项目中我通常把读写函数放进一个类,构造时传入PLC地址和端口,连接管理收敛到类内部。这样上位机其他模块调用时只需要“读D100十个字”这样的语义,不必关心帧怎么拼、长度怎么算。

4.3 连接、超时和重连:稳定性的三个边界

MC协议用TCP承载,连接管理直接决定通信稳定性。三个边界要事先想清楚:超时时间、连接复用、断线重连。

超时设置我一般取3到5秒。PLC响应正常在几十毫秒到几百毫秒,超过2秒基本可以判定异常,设太长会让故障发现变得迟钝。socket.create_connection的timeout参数只控制建立连接,收发超时要单独用sock.settimeout设置。

连接复用上,频繁“连接-请求-断开”在三菱PLC侧会有TIME_WAIT堆积问题,现场出现过连接被拒绝的情况。长时间运行的采集程序,建议保持一个长连接,定时发心跳请求,比如周期性读一个固定D寄存器。如果程序异常退出导致连接未关闭,PLC侧可能短暂占用端口,重连稍等几秒即可。

重连逻辑要捕获socket.timeout、ConnectionResetError、BrokenPipeError三类异常。捕获后做指数退避重连,第一次等1秒,第二次2秒,最多间隔30秒。不要写死循环高频重连,否则PLC日志里全是连接记录,反而掩盖真正的问题。

5. MC协议通信的5个常见坑与排查

MC协议调试到怀疑人生的时候,回头看基本都是这5类问题。每一条我都按“现象→原因→解决”写,对照排查能省下大量抓瞎时间。

5.1 返回C051:起始地址和点数越界

现象:连接正常、请求帧也发出去了,PLC返回结束码0xFFFF,错误码C051。

原因:C051表示请求的软元件范围超出了该CPU支持的上限。每个CPU系列的D寄存器范围不同,比如某些系列D只到D7999,你从D7900开始读200个字,末尾已经超出上限,PLC直接拒绝。

解决:先查所用CPU的软元件编号一览表,确认起始地址加点数不越界。批量读跨越边界时拆成多帧,分别读取,不要试图一次跨过边界。

5.2 返回C057:长度字段算错了

现象:写入数据时PLC返回C057,读操作却正常。

原因:C057表示请求数据长度与实际内容不符。最常见的是写指令的长度字段只算了req_data,漏掉了后面的数据区;或者反过来把9字节帧头也算进去了。

解决:记住一个原则——长度字段只统计帧头之后的所有字节。写操作时一定要把数据区字节数加进去。写完后可以打印请求帧的hex,人工核对长度值。

5.3 数据解析乱跳:字节序问题

现象:读回来的数值看起来有规律但完全不对,比如期望100读出来变成25600,或者负数显示成超大正数。

原因:三菱协议数据区是低字节在前,D寄存器两个字节拼接时要按小端。解析用了大端就会把0x0064读成0x6400,换算过来正好差256倍,看着像数据错位其实只是字节序反了。

解决:解析统一用struct.unpack('<h')或手动低位加高位乘256。另外D寄存器默认是16位有符号,超过32767的值要按无符号或32位组合读取,不能默认按有符号解析。

5.4 请求发出去石沉大海:二进制与ASCII不匹配

现象:TCP连接能建立,请求发出去PLC完全不响应,没有结束码也没有任何数据。

原因:PLC参数里选了ASCII代码,上位机发的是二进制帧,PLC收到的帧头就不是它期望的0x50 0x00,直接丢弃。

解决:抓包看请求帧首字节。二进制帧首字节是0x50,ASCII帧首字节是字符“5”即0x35,一眼可辨。然后核对PLC参数里的帧格式,两边对齐。

5.5 RUN中写入不生效:参数开关没开

现象:写操作PLC返回正常,但PLC里的值没变化,或只在STOP状态下能写,RUN状态下写不进去。

原因:CPU参数里“允许RUN中写入”没勾选,或勾了但参数没下载到位。部分低端CPU本身就不支持运行中写入,这个要看具体型号手册。

解决:在PLC参数里勾选“允许RUN中写入”,重新下载参数并复位CPU。如果还是不生效,查CPU手册确认是否支持该功能,不支持就只能在STOP状态下写。

6. 进阶:随机读取省往返,抓包验证成习惯

6.1 一次往返读多个不连续地址:随机读取指令

批量读取要求地址连续,但配方数据经常是D100、D200、D300散落各处,逐条读要三次往返。其实MC协议有随机读取指令,指令码0403,一次请求里带多个起始地址,PLC按顺序返回对应数据:

def build_random_read_req(dev_code, addrs): req_data = b'\x04\x03\x00\x01' + struct.pack('<H', len(addrs)) for addr in addrs: req_data += bytes([dev_code]) + addr.to_bytes(3, 'little') head = bytes([0x50, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00]) return head + struct.pack('<H', len(req_data)) + req_data

响应解析和批量读一致,数据顺序与请求中的地址顺序一一对应。随机读取能明显减少小数据量高频场景的通信往返,但每次请求的地址数量有上限,同样以CPU手册为准,不要一次塞太多地址。

6.2 上线前先抓包:30秒验证法

我自己的习惯是:任何帧构造代码改完,先不上真机,用一个抓包工具确认发送字节和手册一致,再连PLC。抓包时过滤PLC的IP和端口,看请求帧的长度字段是否等于req_data加数据区的长度,看设备代码和地址编码是否小端正确,这两点对了,通信基本就通了一半。

这个习惯是从一次教训里养成的。有回我写写指令时长度字段漏了数据区,第一反应是去查PLC参数、换网线,折腾了快一小时,最后抓包发现长度字段少了12字节。从那以后,凡是涉及帧结构的改动,先抓包再上真机,30秒的事,别省。

MC协议这套东西,原理不深,但细节密集。把帧结构、参数、地址编码三件事钉死,剩下就是熟练度的问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询