三菱FX3U ModbusRTU数据采集:Python pymodbus实战指南
1. 工控数据采集的起点为什么选FX3U和ModbusRTU车间里跑了五六年的三菱FX3U稳定得像头老黄牛但一到要做数据采集、对接MES或者做看板很多人就卡住了。PLC本身不带网口加以太网模块成本又上去了这时候FX3U自带的RS-485通讯口就成了最经济的突破口。我做过好几个注塑机、包装线的数据采集项目最后落地最稳的方案就是走ModbusRTU上位机用Python的pymodbus库来读写。这套组合的好处很直接硬件不用动一根屏蔽双绞线接上去代码几十行就能跑通成本几乎为零。这篇文章面向的是有一定PLC基础、但Python刚入门的工控朋友。我会把FX3U的通讯参数设置、寄存器地址映射、Python代码实现、以及实际调试中踩过的坑全部讲清楚。你不需要是Python高手只要能看懂基本的语法跟着做就能把PLC里的D寄存器、M继电器数据读到电脑上。整套方案的核心关键词就是三菱FX3U、ModbusRTU、Python、pymodbus我会围绕这几个点展开把每个环节的“为什么”讲透。先说清楚一个前提FX3U原生是不支持ModbusRTU的它自带的是三菱自己的专用协议也就是编程口协议。所以要用ModbusRTU必须加一块FX3U-485ADP或者FX3U-485BD通讯板。485ADP是适配器走的是通讯口485BD是扩展板插在PLC本体上。两者都能用但485ADP在抗干扰和传输距离上更稳一些长距离布线我一般推荐ADP。这个细节很多教程不讲导致新手买错模块白白折腾好几天。2. 硬件接线与通讯参数配置把物理层先打通2.1 485通讯板的选型与接线要点FX3U-485ADP和FX3U-485BD的区别不只是安装方式。485ADP占用的是PLC的通道1CH1通讯格式通过D8120设置485BD占用的是通道2CH2用D8120和D8121配合设置。实际项目里我优先选485ADP原因是它自带隔离在变频器多的场合抗干扰明显更好。接线就三根线SDA和SDB是一对差分信号SG是信号地。屏蔽层单端接地接在PLC侧的SG端子上另一端悬空这个细节能避免大部分通讯不稳定的问题。接线的时候有个坑要特别注意A接A、B接B但不同厂家的标注可能反过来。三菱的SDA对应其他设备的A或者叫DSDB对应B或者叫D-。如果你接上去通讯不上先把A、B对调试试这是最常见的接线错误。我见过太多人因为这个问题查了一下午代码结果发现是线接反了。2.2 D8120参数设置一个寄存器决定通讯格式FX3U的通讯格式全部由D8120这个16位寄存器控制。每一位的含义不同设置错了就通讯不上。走ModbusRTU的话典型配置是波特率9600、数据位8、停止位1、无校验。对应的D8120值是H0C81十六进制。如果你要用19200的波特率值就是H0C89。这个值怎么来的我拆解一下bit0是数据长度07位18位bit1-2是校验00无01奇10偶bit3是停止位01位12位bit4-7是波特率00119600010019200bit8-11是通讯协议1000无协议通讯bit12-15是其他控制位。在GX Works2里设置的话可以在PLC参数里的“PLC系统设置”中找到“通讯设置”直接勾选对应的选项软件会自动算出D8120的值。但我建议你手动确认一下D8120的最终值因为有时候软件版本不同算出来的值会有差异。设置完D8120后必须断电重启PLC参数才会生效。这个“断电重启”很多人会忽略改完就直接测试结果一直通讯不上。2.3 从站地址与寄存器映射搞清楚Modbus地址和PLC地址的对应关系FX3U作为ModbusRTU从站时站号通过D8121设置用485ADP的话。比如设成1那上位机就要用站号1去请求。寄存器映射是新手最容易晕的地方。Modbus的寄存器地址和PLC的软元件地址不是一一对应的需要转换。FX3U的D寄存器数据寄存器对应Modbus的保持寄存器Holding Register功能码03/06/16起始地址是0x0000对应D00x0001对应D1以此类推。M继电器对应的是线圈Coil功能码01/05/15M0对应0x0000M1对应0x0001。但这里有个陷阱FX3U的Modbus地址映射不是从D0开始的而是有一个偏移。具体来说D0对应的是Modbus地址0x0000但有些文档会写成40001这是Modicon格式的表示法4代表保持寄存器0001是地址。你在pymodbus里写代码时用的是0基地址所以读D0就是读地址0。我建议你拿一个已知值的D寄存器做测试比如在PLC里给D100写个1234然后用Python读地址100看能不能读到1234这样就能确认映射关系对不对。3. Python端环境搭建与pymodbus实战3.1 Python环境准备从安装到pymodbus库如果你电脑上还没装Python直接去官网下载最新版安装时勾选“Add Python to PATH”这一步很重要不然命令行里找不到python命令。装完之后打开命令行输入python --version确认版本。然后安装pymodbus库命令是pip install pymodbus。我建议用pymodbus 2.x版本因为3.x版本API变动比较大网上大部分教程还是基于2.x的。指定版本安装pip install pymodbus2.5.3。开发工具我推荐VS Code轻量而且插件丰富。装个Python插件再装个Serial Monitor插件调试串口的时候很方便。如果你习惯用PyCharm也可以但VS Code启动更快改代码测试更顺手。环境配置这块核心就是Python本体加pymodbus库不需要额外的串口库因为pymodbus内部已经处理了串口通讯。3.2 串口参数匹配波特率、校验位、停止位一个都不能错Python端打开串口时参数必须和PLC的D8120设置完全一致。波特率9600、数据位8、停止位1、无校验对应的代码是serial.EIGHTBITS、serial.STOPBITS_ONE、serial.PARITY_NONE。串口号在Windows上是COMx在Linux上是/dev/ttyUSBx或/dev/ttySx。怎么确认串口号Windows设备管理器里看“端口”那一栏Linux下用ls /dev/tty*查看。这里有个经验USB转485转换器的芯片很关键。CH340芯片便宜但稳定性一般FTDI芯片贵但稳如老狗。如果通讯经常断先换个FTDI的转换器试试。另外转换器的A、B端子要和PLC的SDA、SDB对应接好别接反了。我自己的测试台上常年备着两个转换器一个CH340一个FTDI出问题的时候交叉验证一下能快速定位是硬件问题还是代码问题。3.3 完整代码实现读D寄存器、写D寄存器、读M继电器下面这段代码是我在实际项目中反复打磨过的直接可以拿去用。先看读保持寄存器D寄存器的部分from pymodbus.client.sync import ModbusSerialClient as ModbusClient from pymodbus.payload import BinaryPayloadDecoder from pymodbus.constants import Endian client ModbusClient( methodrtu, portCOM3, baudrate9600, bytesize8, parityN, stopbits1, timeout1 ) if client.connect(): # 读D100开始的10个寄存器 result client.read_holding_registers(address100, count10, unit1) if not result.isError(): print(D100-D109的值, result.registers) else: print(读取失败) client.close() else: print(串口连接失败)这段代码的核心是read_holding_registersaddress参数是起始地址count是读取数量unit是从站号。注意address用的是0基地址所以D100就是100。读回来的result.registers是一个列表按顺序对应D100到D109。写单个寄存器的代码# 把D200的值写成1234 client.write_register(address200, value1234, unit1)写多个寄存器# 把D300-D302写成[11, 22, 33] client.write_registers(address300, values[11, 22, 33], unit1)读线圈M继电器的代码# 读M0开始的8个线圈状态 result client.read_coils(address0, count8, unit1) if not result.isError(): print(M0-M7的状态, result.bits)写单个线圈# 把M10置为ON client.write_coil(address10, valueTrue, unit1)这些代码看起来简单但实际跑起来可能会遇到各种问题。比如读回来的数据是乱的或者偶尔超时。接下来我讲几个调试技巧。4. 调试实战从通讯失败到稳定读取的排查过程4.1 通讯超时的常见原因与排查顺序第一次测试通讯不上按这个顺序排查先确认接线A、B有没有接反SG有没有接再确认D8120的值对不对改完有没有断电重启然后确认串口号对不对转换器驱动有没有装好最后确认从站号对不对代码里的unit参数和D8121设置是否一致。这个顺序是从物理层到应用层能避免你一开始就陷入代码细节里。我遇到过一个案例客户现场用的是笔记本加USB转485代码没问题但就是通讯不上。后来发现是笔记本的USB口供电不足换了个带外接电源的USB Hub就好了。所以如果你用的是笔记本尤其是轻薄本USB口供电能力有限带不动485转换器的情况是存在的。4.2 数据错乱与字节序问题读回来的数据不对比如PLC里D100是1234读回来是53764或者别的数这通常是字节序问题。ModbusRTU传输时一个16位寄存器分两个字节发送高字节在前还是低字节在前不同设备可能不一样。pymodbus默认是大端模式高字节在前但有些设备是小端。如果数据不对可以用BinaryPayloadDecoder来转换from pymodbus.payload import BinaryPayloadDecoder from pymodbus.constants import Endian decoder BinaryPayloadDecoder.fromRegisters( result.registers, byteorderEndian.Big, wordorderEndian.Big ) value decoder.decode_16bit_uint()如果读32位数据比如两个连续的D寄存器组成一个32位整数wordorder的设置就很关键。我一般先用已知值测试比如在PLC里给D100和D101分别写0和1234然后读回来试不同的字节序组合看哪种能还原出1234。4.3 通讯稳定性优化轮询间隔与超时设置ModbusRTU是主从轮询机制上位机发一个请求PLC回一个响应。如果轮询太快PLC可能来不及响应导致超时。我的经验是读10个寄存器的请求轮询间隔至少100ms。如果读的点数多间隔要相应加大。timeout参数设1秒比较稳妥设太短容易误判超时设太长出问题时等待时间过久。另外如果总线上有多个从站轮询的时候要一个一个来不能同时发请求。pymodbus的同步客户端是阻塞式的天然就是顺序执行所以不用担心并发问题。但如果你用异步客户端就要自己控制好请求节奏。5. 进阶应用从单点读取到批量采集与数据可视化5.1 批量采集与数据存储实际项目中往往要采集几十个甚至上百个寄存器。我的做法是分批读每批读20到30个寄存器中间加50ms延时。读回来的数据存到SQLite数据库里方便后续查询和分析。SQLite的好处是不用装额外的数据库服务一个文件搞定。建表的时候时间戳、寄存器地址、值这三个字段就够了。import sqlite3 import time conn sqlite3.connect(plc_data.db) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS registers (timestamp TEXT, address INTEGER, value INTEGER)) # 假设result.registers是读回来的数据起始地址是100 for i, val in enumerate(result.registers): c.execute(INSERT INTO registers VALUES (?, ?, ?), (time.strftime(%Y-%m-%d %H:%M:%S), 100i, val)) conn.commit()这个方案简单可靠跑几个月都没问题。如果数据量特别大可以考虑用InfluxDB或者TDengine但对于大多数工控场景SQLite足够了。5.2 用Python做实时数据看板数据存下来之后可以用Flask或者Streamlit做个简单的Web看板。Streamlit特别适合这种场景几行代码就能出一个实时刷新的图表。我做过一个注塑机监控看板用Streamlit加Plotly每2秒刷新一次显示温度、压力、周期时间等关键参数。车间主任在办公室电脑上打开浏览器就能看比传统的组态软件灵活多了。import streamlit as st import plotly.graph_objects as go import time st.title(PLC实时数据监控) placeholder st.empty() while True: # 这里调用读取PLC数据的函数 data read_plc_data() fig go.Figure(datago.Scatter(ydata, modelines)) placeholder.plotly_chart(fig) time.sleep(2)这个看板方案的好处是开发快、部署简单一台普通工控机就能跑。而且Python生态丰富想加什么功能都方便。5.3 多台PLC的采集方案如果车间里有好几台FX3U每台都走485的话可以用一个多串口卡或者用485集线器。但更推荐的做法是每台PLC加一个485转以太网的模块然后Python端用ModbusTCP去读。这样布线简单而且可以同时采集多台设备。不过这就超出了本文的范围以后有机会再展开讲。6. 常见问题速查与避坑经验6.1 问题排查速查表现象可能原因解决方法串口连接失败串口号错误或驱动未装检查设备管理器重装驱动通讯超时接线错误或D8120设置不对检查A/B线序确认D8120值并断电重启读回数据全为0从站号错误或地址映射错误确认unit参数和D8121一致用已知值测试地址数据错乱字节序或字序问题用BinaryPayloadDecoder调整byteorder和wordorder偶尔超时轮询太快或干扰加大轮询间隔检查屏蔽层接地写寄存器无效PLC处于RUN模式但写了只读区确认目标寄存器可写检查PLC是否在RUN6.2 几个容易忽略的细节第一个细节FX3U的D寄存器掉电不保持如果你写进去的数据需要断电保存要用EEPROM写入指令或者改用掉电保持寄存器。第二个细节ModbusRTU的帧间隔至少3.5个字符时间pymodbus会自动处理但如果你自己写串口通讯这个间隔必须遵守。第三个细节485总线上如果接了多个从站终端电阻要接在总线两端阻值120欧姆中间节点不接。还有一个我踩过的坑用USB转485转换器的时候如果电脑休眠再唤醒串口可能会失效需要重新插拔转换器。所以做长期采集的话建议用工控机或者树莓派别用普通笔记本。6.3 代码调试的小技巧调试的时候我习惯先用Modbus Poll或者QModMaster这类工具测试通讯确认硬件和参数没问题了再写Python代码。这样能把问题范围缩小避免在代码和硬件之间来回猜。另外pymodbus有个logging模块打开debug日志能看到收发的原始数据帧对排查问题很有帮助import logging logging.basicConfig() log logging.getLogger() log.setLevel(logging.DEBUG)打开之后控制台会打印出类似SEND: 0x01 0x03 0x00 0x64 0x00 0x0a 0xc5 0xcd这样的原始帧你可以对照Modbus协议手册分析每一段的意思。这个技能在排查复杂问题时特别有用。7. 从采集到控制把Python变成PLC的上位机数据采集只是第一步更进一步是用Python做控制逻辑。比如根据采集到的温度值自动调整PLC里的设定值。这时候要注意写操作比读操作风险大写错了可能导致设备误动作。我的做法是写之前先读一遍确认当前值写之后立即读回来验证确认写入成功。另外关键的控制逻辑最好还是在PLC里实现Python只做参数下发和状态监控这样即使上位机挂了PLC也能独立运行。Python和PLC的配合核心思路是“PLC管实时控制Python管数据分析和人机交互”。这个分工明确之后整个系统的稳定性和可维护性都会好很多。我做过的一个项目PLC负责设备的逻辑控制和联锁保护Python负责采集数据、生成报表、推送到企业微信运行了两年多没出过问题。最后分享一个实用技巧如果你需要采集的寄存器地址不连续可以写一个配置文件把需要采集的地址和名称列出来Python读取配置后自动生成请求。这样以后增加或修改采集点不用改代码改配置文件就行。这个做法在项目后期维护时特别省事。