上周朋友拉我去处理一个生产看板的需求,现场是一台三菱FX5U的PLC,每天靠老师傅抄D寄存器的数值做日报,想着能不能把温度和压力数据自动送到车间大屏上。我第一反应就是拿Python写个小服务,用网线把PLC和电脑接起来,定时把数据读出来推到看板。这件事听起来高大上,其实核心就一层窗户纸:三菱PLC支持MC协议(新一点的文档里叫SLMP协议),走以太网时所谓“通讯”就是往PLC发一段符合协议规定的报文,再把返回值拆开。这篇文章适合自动化工程师、会一点Python又想碰PLC数据采集的朋友,内容是我在实际项目里踩过路的完整记录:从通信帧结构、PLC参数设置、Python代码,到排障经验和优化技巧。看完之后,你至少能自己实现一个“定时把三菱PLC的D、M、X/Y数据抓回来存进数据库或CSV”的小工具。
1. 先理清整体方案:读三菱PLC到底是怎么回事
1.1 项目要解决的痛点
工厂里大量设备用的还是三菱PLC,常见的有FX5U、Q系列、L系列,这些设备本身运行稳定,但“数据看不到”是一线工程师普遍头疼的事。工艺员想知道实时温度,品管想知道每批次的压力曲线,生产主管想统计开机率,这些数据都在PLC的软元件里,比如D100存了当前温度,D101存了当前压力。没有上位机的时候,只能靠人工定时去触摸屏抄数,费时费力还容易记错。
把PLC数据读上来之后能干什么?往小里说是做个实时看板,往大里说是给MES、SCADA提供数据源,甚至可以做设备预测性维护。我做过一个比较典型的场景:把PLC里的PID参数和运行状态D寄存器定时抓回来,配合前端画趋势曲线,工艺员在整定PID参数时就不用来回跑现场,直接在网页上看响应曲线。还有朋友用FX5U通过CC-Link IE Basic带伺服,表面上是在总线上控制伺服,但上位机要拿伺服的位置、电流、报警码,最终还是要从PLC的软元件映射区去读,底层还是同一套软元件读取逻辑。
1.2 为什么用Python而不是组态软件或C#
很多工厂现在的做法是用组态软件,比如组态王、WinCC或者直接用触摸屏。组态软件的优势是开发快,拖拖拽拽就出一个画面,但问题也很明显:一套正版授权不便宜,需要跑在Windows工控机上,关键逻辑一复杂就受限制,而且数据要导出给其他系统用往往比较别扭。C#和VB是工控行业的老牌选择,资料多、成熟稳定,但开发环境重,部署的时候还得装.NET运行时,改个小逻辑就要重新编译。
Python在这件事上的优点恰好补了短板。第一,标准库里的socket就能直接和PLC通信,不需要安装任何厂商的组件包,这对很多“电脑上不让你乱装软件”的工厂环境特别友好。第二,Python处理数据太方便了,读回来的数值可以直接用pandas做统计,用matplotlib画趋势,甚至接Flask做个内网网页。第三,脚本改起来快,现场发现点位地址不对,改一行代码重新跑就行。当然Python也不是没有缺点,做高实时性、高频率的运动控制不行,但做秒级、百毫秒级的数据采集完全够用。
1.3 通信选型:MC协议加以太网,目前最通用的一条路
三菱PLC和上位机通信有几种常见路子。
一种是走串口,RS232或者RS485,老设备常用,但波特率有限、距离受限、而且一台上位机想同时接多台PLC很麻烦。另一种是走以太网,用三菱的MC协议,也叫SLMP协议,新老型号基本都支持,速度快、部署灵活,也是目前主流的选择。MC协议本质上就是一套“应用层报文规范”,PLC作为TCP服务端(或者UDP服务端)监听一个端口,上位机作为客户端发起连接、发请求帧、收响应帧。
这里有个很关键的点:只要你选的是“网口+MC协议”这条路,不管对面是FX5U、Q系列还是L系列,帧结构都是同一套逻辑,代码可以复用。唯一的区别是PLC型号不同,参数设置入口不同、默认端口号可能不同,但协议本身是一致的。所以我把方案定成了“Python标准库socket,走TCP,使用MC协议的3E帧二进制模式”,这样不依赖任何第三方PLC库,部署最干净,也方便你用Wireshark抓包去核对报文。
2. 通信帧结构:几乎所有异常都出在这几个字节上
2.1 一次读D寄存器的请求报文,逐字节拆给你看
我用Python读三菱PLC,踩过最大的坑就是帧结构。网上能找到的资料很多互相矛盾,有的用ASCII模式,有的用Qna-3E帧,有的用3E帧,一个字节对不上,PLC就是不理你。所以这部分我建议所有人都耐下心看懂,宁可在这里花半小时,也不要盲写代码后抓瞎。
下面是一次“读取D100开始的10个字软元件”的完整请求帧,3E帧二进制模式(兼容新版FX5U、Q系列SLMP):
| 字段 | 字节数 | 示例值 | 说明 |
|---|---|---|---|
| 帧头 | 2 | D0 00 | 3E帧二进制模式固定头;Qna-3E老帧是50 00 |
| 网络号 | 1 | 00 | 一般填0 |
| PC号 | 1 | FF | 一般填FF(255),表示请求来源 |
| IO编号 | 2 | FF 03 | 目标模块IO号,常见默认03FF小端排列 |
| 站号 | 1 | 00 | 模块站号,一般填0 |
| 请求数据长度 | 2 | 0C 00 | 从监视定时器开始到报文末尾的总字节数,这里12字节 |
| 监视定时器 | 2 | 10 00 | 0x0010,单位250ms,也就是4秒超时 |
| 命令 | 2 | 01 04 | 0x0401小端排列,表示批量读取字软元件 |
| 子命令 | 2 | 00 00 | 3E帧必须带子命令,批量读取时为0 |
| 起始软元件地址 | 3 | 64 00 00 | D100的地址100,3字节小端排列 |
| 软元件代码 | 1 | A8 | 0xA8表示D寄存器 |
| 软元件点数 | 2 | 0A 00 | 连续读取10个点 |
把这串字节连起来就是:
D0 00 00 FF FF 03 00 0C 00 10 00 01 04 00 00 64 00 00 A8 0A 00我当时第一次看到这个报文时觉得挺繁琐,但把它拆开看就不难理解。它其实就是一个“信封”:前面7个字节是固定地址信息,告诉PLC这封信从哪来、发给谁;接着2个字节写这封信有多长;再往后才是正事,我要读哪个寄存器、从哪个地址开始、读多少个。
2.2 帧头、命令、软元件代码:三个最容易出错的地方
我见过太多人卡在帧头选择上。三菱PLC的协议文档里有几种帧格式,按通信模式分有ASCII模式和二进制模式,按应用场景分有Qna-3E帧、3E帧、A兼容1E帧等等。如果你的目标是“用网口读FX5U或Q系列数据”,用3E帧二进制模式最省事,头固定是D0 00。网上很多老教程用的Qna-3E帧,头是50 00,而且请求数据区里没有子命令字段,两者差出来的两个字节足以让你调试到怀疑人生。所以写代码前先确认你参考的示例到底用的是哪种帧。
命令代码也容易混。批量读取字软元件的命令是0x0401,但注意三菱报文在二进制模式下是小端排列,所以正式拼帧时你得写成01 04。如果看别人贴的报文里写的是“0401”,要明白那只是人看的写法,socket发出去的字节必须按底层的字节序来。批量读取位软元件时命令是0x1401,同理拼帧时写01 14。
软元件代码是另一个高频翻车点。我常用的几个代码列在下面,单位都是16进制:
| 软元件 | 含义 | MC二进制代码 |
|---|---|---|
| X | 输入继电器 | 9C |
| Y | 输出继电器 | 9D |
| M | 内部继电器 | 90 |
| L | 锁存继电器 | 92 |
| B | 链接继电器 | A0 |
| D | 数据寄存器 | A8 |
| W | 链接寄存器 | B4 |
| R | 文件寄存器 | AF |
| Z | 变址寄存器 | CC |
D寄存器我基本天天用,代码是A8这个我闭着眼都记得。如果你是读计数器C的当前值、定时器T的当前值这类特殊软元件,代码就不能凭猜了,一定要去对应型号PLC的通信手册里查“软元件代码一览表”,不同系列会有差异,这也是我踩过坑之后养成的习惯:凡是拿不准的代码,先查手册再拼包。
2.3 响应报文的解析套路
发完请求帧,PLC会回一个响应帧。响应帧的前7个字节和请求帧结构相同,也是D0 00 00 FF FF 03 00开头,接着2个字节是响应数据长度,由结束代码和数据区组成。成功时的结束代码是00 00,如果PLC返回别的值,就要去手册里查对应的含义了,常见的有软元件地址越界、点数超出范围等。
读取10个D寄存器(10个字)成功时,响应帧主体就是:
D0 00 00 FF FF 03 00 16 00 00 00 [20字节数据]其中16 00换算成十进制是22,表示后面有22个字节(2字节结束代码+20字节数据)。20字节数据对应10个16位寄存器,每个寄存器按小端排列。比如D100的值是123,那数据区里对应位置的2个字节就是7B 00。
我调试时最喜欢干的一件事,就是把请求帧和响应帧都打印成hex串,肉眼对着看。这比任何高大上的调试器都好使,因为帧一旦对了,剩下就是纯数据处理问题。
3. 环境搭建与PLC连接配置
3.1 Python环境:别在最基础的地方翻车
很多人看Python采集PLC的教程,上来就写socket,结果卡在环境配置上一两个小时。Windows下装Python,最稳妥的办法是去python.org下载对应版本的安装包,安装时一定要勾选“Add Python to PATH”。装完之后打开命令行,输入python --version,能正常打印版本号就说明成了。如果输入python提示“python was not found; run without arguments to install from the microsoft st”,多半是没加PATH,要么重启一下命令行,要么用py命令试试,要么去系统环境变量里手动把Python安装目录加进去。
Linux服务器上一般自带python3,如果没装,Ubuntu系用sudo apt install python3,CentOS系用sudo yum install python3。本文的采集脚本只需要标准库,socket、struct、time都是内置的,所以不装任何第三方包也能跑。后面如果要写CSV、连数据库,再按需pip install pandas、pymysql这些,随用随装,没必要一上来就配一大堆依赖。
3.2 PLC的IP、端口和MC协议开关
PLC网口默认不是随插随用的。以太网模块或内置以太网口都需要设置IP地址,你要先确认PLC和电脑在同一个局域网。现场最简单的做法是拿一根网线直接连PLC和电脑,把电脑网卡的IP手动改成和PLC同网段,比如PLC是192.168.1.10,电脑就设192.168.1.50,掩码255.255.255.0,然后在命令行ping一下PLC的IP,能通再往下走。
接下来要确认PLC的以太网端口开了MC协议服务器功能。FX5U的话,用GX Works3在“以太网端口设置”里找到SLMP/MC协议相关选项,启用服务,并记下端口号。Q系列如果用了以太网模块,比如QJ71E71,要用GX Works2或者模块配置工具设置通信协议为MC协议,并指定端口。这里最容易踩的坑是:不同型号、不同固件版本的PLC,默认端口可能不同,有人说是5000,有人说是6000,还有的场合是4950,其实这些都可以在PLC参数里自定义。所以不要拿着别人的代码里的端口号硬套,以你的PLC里实际看到为准。
还要注意一点,GX Works软件如果在PLC上在线监控,有些型号会占用通信资源,这时候你再开Python去连,可能连不上或者响应很慢。调试阶段最好把GX Works的在线监控断开,或者两台电脑分开用,一台专门跑采集,一台维护PLC。
3.3 地址规划:想清楚读什么,再动手写代码
代码是最后一步,先把点位表整理出来才是正经事。我通常用Excel建一张表,列清楚要读的软元件类型、起始地址、点数、数据类型、换算系数、用途备注。比如:
- D100:温度,16位有符号,实际值=原始值/10
- D101:压力,16位有符号,实际值=原始值/100
- D110-D111:流量累计值,32位无符号
- M50:设备运行状态,0停止1运行
- X10:急停按钮状态
为什么强调先做地址规划?因为批量读取追求的是连续地址,如果点位东一个西一个,你就只能分成好多次读,网络开销大。现场很多PLC程序是多年前别人写的,数据分布并没有为上位机优化,这时就可以和工艺或电气同事协商,把需要采集的数据尽量连续整理到一段空闲的D区,上位机读起来会轻松很多。三菱的计数器C当前值、PID自整定过程中的各种参数,本质上也都是存在软元件里的,读法和D寄存器完全一样,只是软元件代码不同。FX5U通过CC-Link IE Basic挂伺服时,伺服的状态和控制字也是映射到PLC内部软元件区的,你只要知道映射到了哪里,照样用这套代码去读。
4. 核心代码实现:把D寄存器数据读回来
4.1 最简版本:socket直接读取D100开始的10个字
先把最简单的demo贴上,照着用就能跑通。这里我用的是3E帧二进制模式,命令0x0401读取字软元件,软元件代码0xA8对应D寄存器。
import socket import struct PLC_IP = "192.168.1.10" PLC_PORT = 6000 FRAME_HEAD = bytes([0xD0, 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00]) def build_read_word_frame(start, count): monitor = struct.pack("<H", 0x0010) # 监视定时器,4秒 cmd = struct.pack("<H", 0x0401) # 批量读取字软元件 subcmd = struct.pack("<H", 0x0000) # 子命令 addr = struct.pack("<I", start)[:3] # 起始地址,3字节小端 code = bytes([0xA8]) # 软元件代码:D points = struct.pack("<H", count) # 读取点数 data = monitor + cmd + subcmd + addr + code + points length = struct.pack("<H", len(data)) return FRAME_HEAD + length + data def recv_exact(sock, n): buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("连接被断开") buf += chunk return buf def read_d(start, count): frame = build_read_word_frame(start, count) with socket.create_connection((PLC_IP, PLC_PORT), timeout=3) as s: s.sendall(frame) head = recv_exact(s, 9) # 固定头7字节 + 响应长度2字节 body_len = struct.unpack("<H", head[7:9])[0] body = recv_exact(s, body_len) end_code = struct.unpack("<H", body[0:2])[0] if end_code != 0: raise RuntimeError(f"PLC返回异常结束代码: 0x{end_code:04X}") raw = body[2:2 + count * 2] return list(struct.unpack("<" + "h" * count, raw)) if __name__ == "__main__": values = read_d(100, 10) print(values)这段代码的逻辑不复杂:先拼帧,发出去,再按“先收9个字节,解析出响应长度,再收完剩余内容”的方式收响应,最后按16位有符号整数解析数据区。用recv_exact而不是单纯recv一次,是因为TCP是流协议,一次recv不一定能收到完整响应帧,必须循环读到指定长度为止。很多初学者在socket这一层翻车,就是默认“发一次就能收一次收全”,这在局域网里大部分时候行得通,但遇到网络波动就会偶发丢数据。
4.2 封装成可复用的采集客户端
项目里不可能只读一次就完事,通常要一个“能保持连接、反复调用、异常断线自动重连”的类。把上面的逻辑封一层,现场加点位、改功能都方便。
class McClient: def __init__(self, ip, port, timeout=3): self.ip = ip self.port = port self.timeout = timeout self.sock = None def connect(self): self.close() self.sock = socket.create_connection((self.ip, self.port), timeout=self.timeout) def close(self): if self.sock: try: self.sock.close() except Exception: pass self.sock = None def _transact(self, payload): if not self.sock: self.connect() try: self.sock.sendall(payload) head = recv_exact(self.sock, 9) body_len = struct.unpack("<H", head[7:9])[0] body = recv_exact(self.sock, body_len) except Exception: self.close() raise return body def read_words(self, dev_code, start, count): monitor = struct.pack("<H", 0x0010) cmd = struct.pack("<H", 0x0401) subcmd = struct.pack("<H", 0x0000) addr = struct.pack("<I", start)[:3] points = struct.pack("<H", count) data = monitor + cmd + subcmd + addr + bytes([dev_code]) + points frame = FRAME_HEAD + struct.pack("<H", len(data)) + data body = self._transact(frame) end_code = struct.unpack("<H", body[0:2])[0] if end_code != 0: raise RuntimeError(f"PLC返回异常结束代码: 0x{end_code:04X}") raw = body[2:2 + count * 2] return list(struct.unpack("<" + "h" * count, raw))这样调用时只需要几行:
plc = McClient("192.168.1.10", 6000) temp = plc.read_words(0xA8, 100, 1)[0] / 10.0 plc.close()只要记住软元件代码和地址,每个点位的读取都是同一套逻辑。封装之后,脚本主流程可以专注在处理数据上,不用每次纠结拼帧。
4.3 位软元件和32位浮点数怎么读
D寄存器是字软元件,读起来简单,但现场还需要读M、X、Y这些开关量。批量读取位软元件的命令是0x1401,帧结构和读字类似,只是命令码不同,数据区解析方式也不同。PLC返回的数据区是位打包的,第一个字节的最低位对应第一个软元件,第二个比特位对应第二个软元件,以此类推。比如读M100开始的8个点,返回1个字节,bit0是M100的值,bit1是M101的值。
def read_bits(self, dev_code, start, count): monitor = struct.pack("<H", 0x0010) cmd = struct.pack("<H", 0x1401) # 批量读取位软元件 subcmd = struct.pack("<H", 0x0000) addr = struct.pack("<I", start)[:3] points = struct.pack("<H", count) data = monitor + cmd + subcmd + addr + bytes([dev_code]) + points frame = FRAME_HEAD + struct.pack("<H", len(data)) + data body = self._transact(frame) end_code = struct.unpack("<H", body[0:2])[0] if end_code != 0: raise RuntimeError(f"PLC返回异常结束代码: 0x{end_code:04X}") raw = body[2:] return [(raw[i // 8] >> (i % 8)) & 1 for i in range(count)]32位浮点数是另一个常见需求。PLC里一个32位浮点占两个D寄存器,比如D110和D111拼成一个浮点。三菱的字节序是低字在前,也就是D110是低16位,D111是高16位。拼装代码很简单:
def read_float32(self, start): words = self.read_words(0xA8, start, 2) value = (words[1] << 16) | (words[0] & 0xFFFF) return struct.unpack("<f", struct.pack("<I", value))[0]这里有个血泪教训:不要想当然把两个16位整数直接除以65536再相加,用位运算和struct打包最稳。同时,如果你的PLC数据是用32位整数存储的,比如计数器的双字累计值,把最后一行的unpack格式从f改成i就行,或者直接words[0] | (words[1] << 16)无符号版本。
5. 定时采集与数据落地:从“读一次”到“一直读”
5.1 一个简单的轮询采集脚本
实际项目里通常不是手动执行一次脚本,而是要一个常驻的采集程序,每隔一定时间把点位读一遍,记录到文件或者数据库。最朴素的框架就是while True加sleep,加上异常处理和断线重连。
import time import csv from mc_client import McClient def collect_once(plc): row = { "time": time.strftime("%Y-%m-%d %H:%M:%S"), "temp": plc.read_words(0xA8, 100, 1)[0] / 10.0, "pressure": plc.read_words(0xA8, 101, 1)[0] / 100.0, "running": plc.read_bits(0x90, 50, 1)[0], } return row def main(): plc = McClient("192.168.1.10", 6000) while True: try: row = collect_once(plc) with open("data.csv", "a", newline="") as f: writer = csv.DictWriter(f, fieldnames=list(row.keys())) if f.tell() == 0: writer.writeheader() writer.writerow(row) time.sleep(1) except Exception as e: print("采集异常:", e) time.sleep(3) try: plc.connect() except Exception: pass if __name__ == "__main__": main()这个脚本能跑,但有几个工程上的细节要提醒:第一,采集频率要根据PLC的负载来定,别上来就设100ms一次,PLC扫描周期可能会受影响,一般秒级足够了。第二,写文件的异常不能影响主循环,比如磁盘满了、CSV被占用,要有独立的错误处理。第三,生产环境建议把脚本做成Windows服务或者Linux systemd服务,保证断电重启后能自己拉起来。
5.2 性能优化:批量读取远比你想象的重要
刚开始做采集的同学特别容易写出这样的代码:循环100次,每次读1个D寄存器。这在点位少的时候没问题,但点位一多,问题就来了。每次读取都是一次完整的TCP请求响应,网络往返时间在小规模局域网里大概是零点几毫秒到几毫秒,看着不多,但100个点就是100次往返,再加上PLC内部处理时间,整体就慢了。
批量读取一次可以拿连续地址的数据,比如D100到D199,一次读取100个字,响应帧也就200多字节,速度能快一个数量级。我测试过在普通工厂局域网上,循环读100个单点和一次性批量读100个字,耗时差距大概有5到10倍。所以做点位规划的时候,尽量把要采集的数据整理到连续的地址段,一次读回来到内存里再按索引取用。三菱协议对批量读取的点数有上限,不同型号不太一样,常见的是不超过960个字,保险起见一次别超过512个点,实在需要更多就分段。
5.3 数据写CSV、数据库与可视化
采集上来的数据,落地方式取决于你的用途。只是想导出来做报表,CSV最简单,Excel直接能开。要长期存储、多人查询,那就得进数据库,MySQL或者SQLite都行。Python连MySQL用pymysql,写起来很直接,但注意写入失败不能拖垮采集主循环,可以把插入操作放到单独的队列里,或者定期批量插入。我常用的做法是:采集线程只负责把数据放进一个队列,消费者线程负责批量写库,这样即使数据库卡了一下,采集线程也不会丢数据。
数据可视化的选择更多,简单的可以直接用matplotlib画曲线,适合事后分析。要做实时看板,轻量方案是Flask加前端图表库,把最近一次读取的数据通过接口暴露出去,前端定时拉取刷新。工业环境里如果已经有InfluxDB加Grafana这套时序数据库方案,效果会更好,Python这边只需要把数据推送进去就行。核心问题始终是“读得稳、存得下”,界面反而是最不着急的部分。
6. 高频问题和我的排查心得
6.1 连不上PLC的排查顺序
“连不上”这个问题我被问过无数次,排查顺序很重要,别一上来就怀疑代码。先ping PLC的IP,不通就去查网线、网卡、IP是否同网段。ping通了再确认端口,Windows下可以用telnet,也可以用Python的socket试着connect一下,看端口通不通。端口不通就去PLC侧查MC协议服务有没有启用、端口号是不是写错了。端口通了但代码读不到数据,这时候才需要怀疑帧格式,把请求帧和响应帧的hex打出来,和2.1节的示例逐字节对。我之前遇到过一次很隐蔽的问题:PLC参数里开放了多个通信端口,但某个端口只允许特定来源IP访问,电脑换了IP之后就被拒绝连接,查了半天才发现是PLC侧的访问限制设置。
6.2 返回结束代码非0与数据不对
PLC返回的结束代码非0,说明请求被拒绝了。最常见的两类原因:一类是地址越界,比如你的PLC程序里D区只用到D500,你非要去读D1000,自然会被拒绝;另一类是指定的软元件代码和地址不匹配,比如把D寄存器当成了M寄存器来读。处理办法就是把错误代码记下来,去对应型号通信手册附录里查结束代码一览表,能看到具体原因。要注意的是,不同系列PLC的结束代码含义大体一致但不完全一样,别拿着FX系列的说明去套Q系列。
数据读回来了但明显不对,一般也逃不出几个原因。第一是符号问题,D寄存器里可能是无符号整数,你按有符号解析了,负数的表现就会很奇怪。第二是字节序问题,三菱走的是小端,解析时struct格式串里必须是<前缀。第三是32位数据高低字顺序反了,两个D寄存器拼32位时,记得先读的是低字。还有一种很磨人的情况:PLC程序里做了量纲换算,比如实际温度是23.5度,但PLC里存的是235,上位机没除以10直接就显示了。做点位表的时候最好把每个点位的数值类型和换算关系都标注清楚,写代码时严格按表来。
6.3 现场实施的一些保命建议
最后说几条我自己的经验。第一,先在办公室搭测试环境,PLC模拟器或者一台闲置PLC都行,不要在正在生产的设备上第一次跑通就完事,万一写命令写错了位置,后果不是开玩笑。第二,如果只读不写,一定要把代码限定在读取命令范围内,不要顺手把写命令也封装上去,防止有人误调用。第三,采集脚本一定要记日志,至少把请求帧、响应帧、错误码都记录下来,出问题的时候复盘会快很多。第四,考虑一下PLC的通信资源,有些三菱PLC同时只允许几个客户端连接,你一会儿用GX Works连,一会儿用Python连,新手阶段很容易遇到“刚才还能读,怎么突然连不上了”的情况,基本是通信连接没释放。第五,如果采集脚本打算长年累月跑,最好加个看门狗机制,Windows下可以用计划任务定时检查进程,Linux下用systemd管理,简单可靠。
我在实际项目里最深的体会是:用Python读三菱PLC这件事,难点完全不在Python语法,而在“你能不能把一个字节一个字节的报文拼对”。一旦把帧结构吃透了,后面不管是读D寄存器、M继电器,还是扩展成完整的SCADA采集服务,都只是锦上添花。希望这篇文章能帮你少走点弯路,尽快把第一个点位读出来。