做设备数据采集这几年,被问得最多的需求之一就是“用Python把三菱PLC的数据读出来”。我在产线上调试过FX5U,也在实验室接过Q系列和L系列,说实话,这个方向的需求量一直很大,但网上能找到的教程大多停留在“装个库、连上、print出来”的程度,真到现场布局、地址换算、数据拼接这一步,很多人就开始懵了。
这篇文章我想把“Python读取三菱PLC数据”这件事从选型到落地完整捋一遍。内容包括:为什么用Python直连PLC、MC协议里的地址到底怎么算、D寄存器和M继电器怎么读怎么写、32位数据和浮点数怎么拼、连接超时和数据类型不对这些坑怎么避。适合两类人看:一类是搞设备数据采集、想做产线看板的工程师,另一类是刚接触工业通讯但对Python比较熟的开发者。看完至少能让你独立写出一套稳定运行的PLC数据采集脚本。
1. 为什么用Python直连三菱PLC:场景与选型思考
1.1 什么场景需要自己写代码读PLC
先说说我遇到的典型场景。工厂里有几台三菱PLC,设备数据散落在D寄存器、M继电器里,比如温度、压力、产量计数、报警标志位。老板说要上数字看板,要数据进数据库,要做历史曲线,这时候你面前有几条路:买组态软件、买工业网关、或者自己写程序。
组态软件能快速做画面,但按点位授权收费,点数多了是真肉疼,而且和MES对接、做二次算法分析时也很别扭。工业网关倒是不错,但通用网关往往需要做寄存器映射表,点位一多配置起来并不省事。于是很多项目都会选择直接写程序采集,而Python在这个位置特别合适:开发快、调试方便、数据处理生态好,拿到数据后交给pandas、numpy或者直接写数据库,都比传统上位机开发顺手得多。
1.2 为什么首选MC协议而不是Modbus
这里有个很容易混淆的点。很多人一提到PLC通讯就想到Modbus,但三菱PLC原生最通用的协议其实是MC协议(也叫SLMP)。Modbus通常需要PLC程序里额外编写协议指令或者通过特殊模块支持,而MC协议是基于三菱PLC内部软元件编号直接批量读写的,几乎不需要PLC侧写任何通讯代码,开箱即用。
Python社区里正好有个比较成熟的库叫pymcprotocol,封装了MC协议的以太网帧格式,几行代码就能读写三菱PLC。也就是说,你不需要自己拼报文、算CRC、处理粘包,大部分底层细节库已经处理好了。选型结论很直接:三菱PLC + 以太网 + pymcprotocol,就是门槛最低、见效最快的组合。
1.3 方案适用范围与边界
需要先把边界说清楚,免得你拿它去做不现实的事。这套方案适合:FX5U、Q系列、L系列、iQ-R系列等支持MC以太网协议的三菱PLC;适合数据采集频率不高、单次数据量不太大的场景,比如每秒轮询一次、每次读几十个几百个字,都是小意思。
如果项目要求实时性特别苛刻,比如伺服位置动态跟踪,几个毫秒内就要把数据送进上位机,那Python这条路不一定合适,更建议走CC-Link IE Field总线或者专用运动控制方案。另外如果是老款FX3U,虽然可以通过扩展以太网模块支持MC协议,但成本不低,这时候反而可以考虑串口方案,不过串口的帧处理要麻烦一些,建议优先评估换模块或者加网关。
2. 准备工作:环境搭建与通讯链路
2.1 Python环境与pymcprotocol库安装
Python环境这里不展开太细,只说结论:建议直接用Python 3.8以上的64位版本,太老的版本在新库上容易碰到兼容问题。
安装pymcprotocol很简单,一条命令搞定:
pip install pymcprotocol安装完成后可以先做个版本确认:
pip show pymcprotocol如果你是在内网离线环境部署,可以在一台能上网的机器上先下载whl包,再拷贝到目标机器安装:
pip download pymcprotocol -d ./packages pip install --no-index --find-links=./packages pymcprotocol2.2 PLC端以太网与MC协议配置
很多新手第一次连不上PLC,问题往往不在Python端,而在PLC侧没有把通讯协议打开。以FX5U为例,在GX Works3里面要确认以太网端口参数里启用了SLMP连接,并记录下端口号。Q系列使用QJ71E71以太网模块时,要在“打开方式”里选择MC协议,并设置对应的端口。
这里有个经验:PLC端的参数设置完成后,一定要重新上电生效,不是点一下写入就能立即起效的。我第一次调试Q系列时就是栽在这里,参数写进去没断电重启,结果Python端怎么连都超时。
如果是FX5U这类内置以太网口的型号,还要注意PLC程序里IP地址不能被占用。常见的坑是电脑和PLC不在同一网段,或者现场有人把PLC的IP改成和网关、摄像头冲突了,导致数据时通时断。
2.3 网络侧基础排查:IP、端口与防火墙
连接之前,先用最原始的办法确认网络是通的。在命令行ping一下PLC的IP地址:
ping 192.168.3.10能ping通只代表底层通,不代表PLC通讯服务已经就绪。接下来需要确认端口是否开放。Windows下可以用telnet或者PowerShell测试:
Test-NetConnection -ComputerName 192.168.3.10 -Port 2000如果端口测试失败,优先检查PLC端的协议配置和端口号,而不是怀疑代码写错了。
电脑端的防火墙也别忽略,尤其是Windows系统,Python进程访问外部端口时经常被防火墙静默拦截,表现就是代码卡在connect()那里一直超时。
3. 读懂MC协议的关键:地址映射与数据类型
3.1 软元件地址体系:D、M、X、Y到底怎么读
MC协议最核心的概念是“软元件编号”。它不是简单的地址偏移,而是按软元件类型分开管理的。pymcprotocol的batchread方法需要你传入三个参数:元件类型、起始地址、读取个数。
先看最常用的D寄存器,它是16位数据寄存器:
# 从D0开始,连续读10个16位寄存器,即D0~D9 data = plc.batchread("D", 0, 10)这里注意,MC协议里D0对应的编号就是0,D100对应的编号是100,写起来很直觉。M继电器是位软元件,用类似方式读:
# 从M0开始,连续读16个位,即M0~M15 bits = plc.batchread("M", 0, 16)返回的列表里,每个元素是0或1,对应每个位的状态。
最容易被坑的是X和Y。三菱PLC的输入输出继电器编号是八进制的,X0到X7之后不是X8、X9,而是X10、X11。也就是说,X10在数值上对应MC地址8。如果你写代码时想读X20,没有做八进制到十进制的转换,把20当十进制的20传进去,读到的一定是错的。
“要不要转换”这个问题得特别留意,pymcprotocol内部接收的起始编号是数值型的,虽然库在某些模式下面会自动处理八进制,但我自己的习惯是尽量用D和M做数据交互,X、Y这种硬件IO点尽量在PLC程序里映射到D寄存器再交给上位机,这样既避开八进制问题,又让点位表更清晰。
3.2 16位、32位和浮点数的字节顺序处理
D寄存器本身是16位的,但实际工程里的温度、压力、流量这些数据,很多都是32位整数或者32位浮点数,也就是跨两个D寄存器存放。
三菱PLC的32位数据存放顺序是:低16位在低地址,高16位在高地址。比如D0和D1组合成一个32位整数,D0是低16位,D1是高16位。用Python组合一下:
low = data[0] high = data[1] value = low + (high << 16)如果是32位单精度浮点数,存放顺序同样遵循低字在前的规则,用struct包组合解析:
import struct # data[0]是低16位,data[1]是高16位 low = data[0] high = data[1] float_value = struct.unpack('<f', struct.pack('<HH', low, high))[0]这里的核心是字节序。PLC端走的是“低字在前”,所以组合时要用小端模式,也就是struct格式里的'<'前缀。如果顺序搞反,读出来的浮点数会是一个非常离谱的大数,这在排障时几乎是最常见的坑。
3.3 地址“差一”和单位换算的那些误区
有一类问题不是协议造成的,而是习惯造成的。MC协议里D0就是编号0,但很多人在脑子里残留着Modbus的40001映射逻辑,觉得PLC的D0是不是对应Modbus地址40001之类的东西。这里要注意:当通过Modbus网关读三菱PLC时,确实会存在地址映射和偏移,但你用MC协议直连时,D就是D,不需要加偏移。
还有一个典型问题是单位换算。PLC侧的整数温度值可能是实际温度乘以10,也就是所谓的一位小数精度。你直接读出来是个整数,如果忘了除以10,画出来的趋势图全是偏高的,而且这个错误在界面上很难一眼看出来。接到项目的第一件事,就是找PLC程序员要一份点位表,确认每个点位的工程单位、缩放系数、数据类型、是否有符号。点位信息比代码重要得多。
4. 实操代码:读取、写入与批量采集
4.1 最小可运行案例:连接并读取D寄存器
以最常见的Type3E以太网连接为例,先写一个最小案例:
import pymcprotocol plc = pymcprotocol.Type3E() plc.connect("192.168.3.10", 2000, timeout=3) data = plc.batchread("D", 0, 10) print(data) plc.close()connect()的第一个参数是PLC的IP地址,第二个是端口号,timeout建议根据现场情况设置。连接成功后,batchread返回一个列表,元素个数就是读取的字数。
建议把连接、读取的过程放到try里:
import pymcprotocol plc = pymcprotocol.Type3E() try: plc.connect("192.168.3.10", 2000, timeout=3) data = plc.batchread("D", 0, 10) print("D0~D9:", data) finally: plc.close()finally里调用close,保证连接最终能被释放。因为PLC端的连接资源是有限的,脚本异常退出后不一定马上释放,反复异常断开会导致后续连接越来越慢。
4.2 读取M、X、Y:位软元件的操作
位软元件读取和字软元件类似,只是语义不一样。实际应用中M寄存器常常用来表示设备状态和报警,比如M100是运行中、M101是故障,读出来就很直观:
# 读取M100~M119的20个位状态 status = plc.batchread("M", 100, 20) for i, value in enumerate(status): print(f"M{100 + i} = {value}")如果你想读X输入点,虽然前面提到尽量避坑,但确实避不开时,也可以这样操作:
x_status = plc.batchread("X", 0, 8) # X0~X7注意这里的0到7对应的是X0到X7,如果读X10到X17,起始编号应该从8开始。不一致的根源就是八进制,写代码时最好用int("10", 8)来换算,避免手算出错。
4.3 写入操作:整型、位写入与置位复位
读取之外,写入是另一个刚需。比如上位机下发启动命令、重置产量计数,都需要写PLC的寄存器。pymcprotocol的batchwrite用法和batchread是对应的:
# 把100和200写入D100、D101 plc.batchwrite("D", 100, [100, 200]) # 把M10置位为1 plc.batchwrite("M", 10, [1]) # 把M10复位为0 plc.batchwrite("M", 10, [0])有一点要注意,批量写和批量读一样,各个地址是连续的。如果你要写的是D100、D200这种不连续的地址,用batchwrite就不方便了,可以用randomwrite或者分多次写,根据实际情况取舍。
对安全性要求高的场景,比如下发电机、下放急停复位这种命令,我的建议是PLC程序里做两段式确认,或者用先写状态字再写命令字的方式,不要单点直接控制,否则一个误写就可能造成现场设备误动作。
4.4 批量采集:组装一个循环记录数据的脚本
实际项目里很少只读一次数据,更多是要按一定周期持续采集并把数据保存下来。下面这个例子每0.5秒读取一组数据,写入CSV文件:
import csv import time import struct import pymcprotocol plc = pymcprotocol.Type3E() plc.connect("192.168.3.10", 2000, timeout=3) csv_file = open("plc_data.csv", "w", newline="") writer = csv.writer(csv_file) writer.writerow(["timestamp", "D0", "D1", "D2", "D100_float"]) try: while True: # 读取D0、D1、D2三个16位数据 data = plc.batchread("D", 0, 3) # 读取D100和D101,组合成一个32位浮点数 raw = plc.batchread("D", 100, 2) float_val = struct.unpack('<f', struct.pack('<HH', raw[0], raw[1]))[0] writer.writerow([time.time(), data[0], data[1], data[2], float_val]) csv_file.flush() time.sleep(0.5) except KeyboardInterrupt: print("采集结束") finally: csv_file.close() plc.close()这个脚本已经能在现场跑一段时间了。几个细节值得说明:csv_file.flush()保证数据及时落盘,避免程序异常退出时丢失最近几秒的数据;time.sleep(0.5)控制轮询周期,不能太快,否则PLC通讯模块会持续高负载;KeyboardInterrupt捕获手动停止,保证连接和文件都能安全关闭。
4.5 随机读取与多PLC连接
实际工程中的点位往往不连续,比如需要D0、D10、D20之类的地址,每次batchread都传起始地址和数量,代码会显得很笨拙。MC协议也支持随机读取,pymcprotocol里有对应的方法,可以把不连续的点位一次性读回来,效率比多次batchread更高。建议点位数多且分散的场景用randomread,连续区域多的场景用batchread,组合起来比较合理。
多台PLC同时采集时,可以用多线程或者异步方式来管理连接。每台PLC一个连接实例,注意线程安全问题,不同线程不要共用同一个pymcprotocol实例。另外,多PLC轮询要错峰,尽量避免所有线程都在同一时刻发请求,否则交换机可能成为瓶颈。
5. 常见问题与排查记录
5.1 连接失败的速查表
实际调试中连接失败是最常见的问题,我整理了一个排障顺序:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| ping不通PLC | 网络不通、IP不在同网段 | 检查网线、交换机、电脑IP |
| ping通但connect超时 | PLC端的MC协议未启用或端口不对 | GX Works里检查SLMP/MC协议设置和端口号 |
| connect报拒绝连接 | PLC通讯模块资源耗尽 | 检查是否有其他上位机占用连接,重启PLC通讯模块 |
| 连接成功但读取超时 | 读取的地址超出范围或写入参数异常 | 核对点位表,先用PLC软件在线监视确认地址 |
连接类的坑基本都是配置问题,和时间无关,按表格顺序排查一般都能解决。
5.2 读到的数值不对:三个容易踩的数据坑
有一类问题特别隐蔽,就是能连上也能读到数据,但数值就是不对。常见的三个坑:
第一个坑是无符号和有符号。PLC程序里D0被当作有符号整数写了,比如实际值是-1,你读出来却得到65535,这是因为16位的二进制表示本身没有符号概念。解决办法是在Python端做转换:
def u16_to_i16(value): if value >= 32768: return value - 65536 return value第二个坑是32位数据的高低字顺序。前面已经反复强调过,三菱的低字在前,如果用struct的'>'大端格式解析,数据就会完全错乱。
第三个坑是浮点数不要看字面。你PLC里显示1.25,上位机读出来可能是一个很大的乱数,大概率是高低字组合错了,而不是PLC数据坏了。
5.3 批量读取报错或超时怎么办
批量读取最容易遇到“一次读太多”的问题。MC协议对单帧读取的点数有限制,不同系列、不同协议版本上限不一样。我的建议是单次读取控制在100个字以内,位软元件控制在256位以内,这在实际项目中基本不会踩协议帧长的雷。
如果你需要连续读一整块区域,比如D0到D2000,正确的做法是分多次读取:
all_data = [] for start in range(0, 2000, 100): all_data.extend(plc.batchread("D", start, 100))这样每帧报文都在合理范围内,即使是通讯不太稳定的现场也不容易超时。这个方法看起来简单,但在现场救过我很多次。
5.4 轮询频率与稳定运行的一些实操建议
关于轮询频率,我见过有人写while True里不sleep直接拼命读的,结果把PLC以太网模块的CPU占用拉满,导致PLC程序扫描周期都被拖长了。轮询周期最低建议不要低于200毫秒,一般场景1秒一次足够了,毕竟工业数据的变化通常不会快到毫秒级。
长时间运行的脚本,还需要考虑连接断线恢复的问题。现场网络偶尔抖动、PLC断电重启,都会让已有连接失效。比较稳妥的做法是给读取操作加上重连机制:
import time import pymcprotocol PLC_IP = "192.168.3.10" PLC_PORT = 2000 plc = pymcprotocol.Type3E() def ensure_connected(): try: # 用一个简单的读取测试连接是否正常 plc.batchread("D", 0, 1) except Exception: print("连接断开,尝试重连...") try: plc.close() except Exception: pass plc.connect(PLC_IP, PLC_PORT, timeout=3) while True: ensure_connected() data = plc.batchread("D", 0, 10) print(data) time.sleep(1)这段代码的try块里调用batchread,如果抛出异常就重连,重连成功后继续下一轮。对于无人值守的数据采集端,这种自愈能力几乎是必需的。
最后再分享一个习惯:现场调试时,我一般会在PLC编程软件里开一个在线监视窗口,把要读的寄存器值直接标出来。Python端读出来的数据能实时和PLC软件的值对应上,就说明通讯链路和地址翻译都正确了。这种“双端对照”的调试方式,比盲目看打印日志高效太多。等做过一两个项目,这套流程就变成标准动作了,后面再接新设备,基本半小时内就能跑通数据链路。