ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python上位机Modbus通信实战:RTU与TCP从零到跑通

Python上位机Modbus通信实战:RTU与TCP从零到跑通 我给设备供上电把USB转485线插到电脑上打开串口调试助手照着手册里抄来的报文发过去01 03 00 00 00 06 C5 C8。等了五秒没有任何回复变频器像压根没通电一样。后来才发现不是我设备坏了是我把从站地址看成了0而实际地址是1。类似的事做上位机的人基本都经历过。所谓Modbus通信本质上就是上位机向一个或多个从站设备轮流“提问”设备按约定把寄存器里的数据交出来。Python做这件事优势非常明显库成熟、代码短、改动快特别适合调试台架、数据采集站和中小型监控界面。这篇文章我就用实际代码把Modbus RTU和Modbus TCP两条路线都走一遍顺便把新手最容易踩的寄存器地址、数据类型、超时和从站编号这些坑挨个填平。看完你不需要死记协议只需要一条串口线、一个模拟器就能把第一段通信跑通。1. 说好的五分钟先花五分钟看懂Modbus这层关系很多人一上来就复制代码结果换了设备就抓瞎根本原因是没有把Modbus的“角色关系”搞清楚。Modbus协议的模型非常简单所有通信都建立在这两个角色上主站和从站。上位机就是主站负责发起请求PLC、变频器、温控表、电表都是从站负责响应。从站永远不会主动跟主站说话哪怕设备已经炸了温度它也只能等着被问。这个设计很老但可靠做上位机时心里必须有这张图。1.1 主从结构上位机永远在“主动问”被监控的设备不管有多少台逻辑上都是一个个“从站”每个从站有一个唯一地址1到247。上位机拿着地址挨个点名从站听到点名后回话。这里有两个容易忽略的点第一同一组串口总线上的从站地址不能重复否则两个设备同时回话报文直接撞车第二0不是普通从站地址它是广播地址很多设备根本不响应广播你拿0去读数据当然什么都等不到。后面我会专门讲这个坑。这个“一问一答”的模型也决定了RTU通信通常是半双工的同一时刻只能有一方说话。你在设计轮询逻辑时必须等一个请求返回后再发下一个不要像TCP那样并行乱发否则设备根本来不及处理。1.2 寄存器地址不是数组下标而是一格格的抽屉Modbus把设备里的数据分成了四类对应四种功能码线圈、离散输入、输入寄存器、保持寄存器。线圈是位数据可读可写离散输入是位数据只能读输入寄存器是16位只读数据保持寄存器是16位可读可写数据。工业现场最常见的就是保持寄存器电机转速、电流、温度这些参数一般都在里面。这里最大的误解是寄存器地址。设备文档写“转速地址40001”和代码里填的address0可能是一个东西。因为Modbus协议报文里传的是相对偏移量0x0000而很多组态软件和旧手册喜欢用PLC地址体系40001表示第一个保持寄存器。这中间往往要减1。我在现场见过有人把40001直接填进去然后抱怨读数差一位。你做上位机时一定先确认文档写的是协议偏移地址还是PLC风格的寄存器编号否则差之毫厘谬以千里。1.3 RTU与TCP先看设备屁股后面长什么接口Modbus有两个主流传输形态。走串口RS232/RS485的一般叫Modbus RTU数据是一字节一字节通过串口发出去的报文末尾带CRC校验波特率、数据位、校验位、停止位这四个参数必须和从站一致走网线的叫Modbus TCP报文外面套一层MBAP头用IP和端口502连接没有CRC因为TCP自己保证可靠传输。我个人的选型建议是如果设备能联网优先用TCP至少不用关心串口被占用、USB转485模块驱动不稳的问题如果只能走串口那就老老实实做RTU。如果遇到一个本身是串口设备、却想通过网关转成TCP上报的场景记住那只是“包装”变了底层寄存器地址和功能码逻辑完全一样代码里换个Client就行。2. 环境搭建选对库版本比写代码更省时间Modbus在Python里最常用的库就是pymodbus。但有些教程是两三年前的你用最新版本照抄会报错因为库的API改过好几轮。我建议新项目直接用当前稳定版同时学会看版本号的差异而不是永远靠“粘贴-报错-搜索”的循环。2.1 安装pymodbus别一上来就装最新版在终端里执行pip install pymodbus这个会装当前最新版比如3.x。如果你是Windows环境再顺手装pyserialRTU模式会用到它pip install pyserial如果你在网上搜到类似from pymodbus.client.sync import ModbusSerialClient的写法那基本是pymodbus 2.x的代码。2.x和3.x最大的区别在于导入路径和参数名2.x喜欢写methodrtu3.x里直接用framer或者干脆不写。为了避免项目里出现“上午能跑下午装个依赖就崩”的灵异事件建议把版本固定下来pip install pymodbus3.6.6 pyserial3.5这样至少别人复现你的代码时不会因为库版本不一致而怀疑人生。2.2 没有设备时用Modbus从站模拟器顶上去调试上位机最尴尬的是设备还没到位或者设备在车间里你人在办公室。这时候可以用Modbus从站模拟器。业界常用的两个工具是Modbus Slave和Modbus Poll前者模拟从站设备后者模拟主站调试工具。做上位机调试时我一般是开一个Modbus Slave往里填寄存器值再用自己写的Python脚本去读两边能对上就说明代码基本没问题。需要注意这类商业软件有免费试用期很多人找破解版真没必要。调试用途的试用完全够用而且更安全。你把从站地址设为1协议选RTU或TCP端口填502或者串口参数再往保持寄存器区域写几个数值Python脚本就能当成真实设备来读。2.3 串口权限、驱动和端口占用排查Linux用户连USB转485设备后如果提示没有权限大概率是当前用户不在dialout组里sudo usermod -a -G dialout $USER然后重新登录生效。Windows用户反而相对省心但要注意设备管理器里看到的串口号可能和实际对不上。如果Python报“could not open port”先看看是不是有两个软件同时占用了同一个串口尤其是串口调试助手没关时上位机是打不开端口的。我习惯在代码里把端口号用参数暴露出来而不是写死省得每次换USB口都要改代码。3. 核心通信代码拆解从连上串口到读回第一个数环境准备好之后就可以写第一段能跑的RTU代码了。我先给一段最精简的读取程序然后逐一解释每个参数的含义。这一段代码你吃透了后面换成TCP只是换类名而已。3.1 串口参数和连接是最容易想当然的一步# modbus_rtu_demo.py from pymodbus.client import ModbusSerialClient client ModbusSerialClient( portCOM3, # Windows下是COM口Linux下是/dev/ttyUSB0 baudrate9600, bytesize8, parityN, stopbits1, timeout1.5, ) if not client.connect(): raise SystemExit(串口打开失败请检查设备连接和端口占用)这里有个细节baudrate9600必须跟设备一致。很多仪表出厂默认9600但你不知道它是否被人改成19200或115200。第一次连不上别急着怀疑代码先把设备说明书翻出来确认这四个串口参数。这几个参数加起来其实就是在定义一个“频道”频道对不上设备根本听不懂你在说什么。3.2 读保持寄存器、读输入寄存器与写入操作连接成功后读取保持寄存器只需要两行核心代码resp client.read_holding_registers(address0, count6, slave1) if resp.isError(): print(读取失败, resp) else: for i, val in enumerate(resp.registers): print(f寄存器 {i}: {val})address0是协议偏移地址count6表示连续读6个寄存器slave1是从站地址。resp.registers返回一个整数列表每一位对应一个16位寄存器的原始数值。如果读的是只读型数据用read_input_registers参数完全一样。写操作也一样简单# 写单个保持寄存器把从站1的寄存器100写成1500 client.write_register(address100, value1500, slave1) # 写单个线圈把从站1的线圈0置为True client.write_coil(address0, valueTrue, slave1) # 连续写多个保持寄存器 client.write_registers(address200, values[1, 2, 3, 4], slave1)这里要特别提醒写操作是把数据真正写进设备里的调试时务必确认寄存器地址不是设备内部的关键参数地址更不要拿生产设备乱写。我一般会在代码里加一层“写保护开关”只有显式打开后写接口才允许执行防止误操作。3.3 超时与异常的三种处理姿势Modbus是请求-响应模型只要网络没通、设备没电、地址错了上位机就会一直等。所以timeout参数非常关键。它表示“发出请求之后最多等多少秒”超过时间还没收到响应就放弃。千万不要设为0或一个很小的值不然慢速设备明明在线也会被判超时。常见工业设备的响应时间在几十到几百毫秒不等我通常设1~3秒。pymodbus的响应对象可以判断错误类型。最基本的写法是resp.isError()但如果想看得更细可以捕获异常try: resp client.read_holding_registers(address0, count6, slave1) if resp.isError(): print(设备返回异常, resp) else: print(成功, resp.registers) except Exception as e: print(通信异常, type(e).__name__, e) finally: client.close()这里finally里的client.close()是很多新手容易漏掉的。串口是独占资源脚本异常退出后如果不主动关闭下次运行很可能会报“端口被占用”。4. 从RTU到TCPPLC联网通信的差异远不止换一个Client很多人以为把ModbusSerialClient换成ModbusTcpClient就完事了表面上看确实如此但TCP模式的连接管理思路跟串口完全不一样。串口是物理链路一根线接上就得TCP是网络连接上位机和PLC之间是“会话”你得先建立会话再发请求还要考虑断开重连。4.1 就几行代码但“连接管理”才是重头戏from pymodbus.client import ModbusTcpClient client ModbusTcpClient( host192.168.1.100, port502, timeout2, ) if not client.connect(): raise SystemExit(连不上PLC请先检查IP和网线) resp client.read_holding_registers(address0, count10, slave1) if not resp.isError(): print(resp.registers) client.close()TCP模式下slave这个参数在协议里叫“单元地址”Unit ID很多PLC在TCP模式下也认它但未必会校验。我的经验是如果只是一台PLC、一个连接Unit ID填0或1都行如果前面挂了Modbus网关下面带很多RTU从站那Unit ID就必须填真正要读的那个从站地址网关会把它转成串口报文发下去。连接管理上TCP和串口的最大区别是“断线重连”。网线被踢了、PLC重启了、交换机短暂断流都会把连接弄没。一个健壮的上位机应该定期检测连接状态断线后自动重连而不是直接崩掉。简单做法是每次轮询前判断client.connected如果为False就重新connect()。4.2 多从站轮询与“报文不要太密”如果你的系统里有好几台设备最简单的轮询模型是写一个循环每个周期把所有设备地址都扫一遍import time DEVICES [1, 2, 3, 4] while True: for dev in DEVICES: resp client.read_holding_registers(address0, count10, slavedev) if resp.isError(): print(f设备{dev}失败) else: print(f设备{dev}:, resp.registers) time.sleep(0.05) # 每一次请求之间留一点间隔 time.sleep(1) # 每轮周期之间的间隔加time.sleep(0.05)不是无意义的拖延。虽然TCP是全双工但底层设备往往是一个单片机在循环处理收发连续高频请求会造成缓冲区溢出或设备复位。RTU更不用说485总线半双工报文太密直接把总线拉垮。轮询周期到底设多少取决于设备数量和从站响应速度我习惯从100ms起步稳定后再慢慢往下压。4.3 功能码与寄存器地址映射别被设备文档绕晕上面代码里读保持寄存器会走Modbus功能码0x03写单个寄存器走0x06这个映射是协议内定的不需要你在代码里写功能码。但很多设备文档会直接写“支持功能码03/06”你要对应到“可读保持寄存器、可写单个保持寄存器”这一层逻辑上来。另外多台设备挂在同一个网关后面时寄存器偏移不一定从0开始。有些设备为了兼容PLC的地址区块文档会把寄存器写成40021之类。我遇到这种情况会在代码里做一个“设备点表”显式记录每个参数的寄存器偏移和数据类型POINT_TABLE { device1_speed: {addr: 0, type: uint16}, device1_temp: {addr: 1, type: int16}, device2_status: {addr: 0, type: bit}, }这样做的好处是代码可读性高而且当现场设备点表改版时只需改配置不用改逻辑。5. 三个让我从下午查到半夜的实战坑写上位机通信代码本身通常半小时能搞定真正耗时间的全都是细节坑。我把自己踩过最深的三个坑完整复盘一遍你好歹能少走一半弯路。5.1 从站地址写0明明代码没问题设备就是不回话第一次现场调试时我拿着设备手册看到开头写着“本机地址范围0~247”想当然地把从站地址设成了0结果程序一直超时。我查了波特率、串口线、接线端子甚至怀疑设备坏了最后把Modbus Poll打开随手填1一下就读出来了。原因就是Modbus协议里0地址是广播地址常规从站设备收到地址0的报文根本不响应只有极少数设备会特殊处理。这个坑之所以隐蔽是因为代码层面完全不会报错只表现为“超时”。排查链路其实很固定先用Modbus Poll工具验证通信再回头查代码。如果你连Modbus Poll都读不到那是物理接线和参数问题如果Modbus Poll能读到而你的代码读不到那就要怀疑代码里的从站地址、寄存器地址换算、超时设置。不能让代码问题掩盖物理问题也不能反过来。5.2 32位浮点数读回来一堆乱码其实是两个寄存器拼出来的有一次读温控器的温度寄存器里读回来两个数字16908和16544每一个看起来都像无意义的整数。后来看设备手册才知道这个温度是32位浮点数保存在两个连续的16位寄存器里也就是需要把两个寄存器拼起来按照IEEE 754解析。拼的时候还有顺序问题设备可能先传高16位也可能先传低16位取决于厂家实现。我最后用struct解决import struct high resp.registers[0] # 假设第一个是高位 low resp.registers[1] # 第二个是低位 raw struct.pack(HH, high, low) # 两个16位拼成4字节大端序 value struct.unpack(f, raw)[0] print(f温度: {value:.2f}℃)如果读出来乱码把HH换成HH试试也就是“高字在前”还是“低字在前”的差异。很多新手在这里容易崩溃其实这才是Modbus工程项目的常态你读出来的不是漂亮的数据而是一堆需要按设备手册二次解析的二进制。5.3 超时时间设太短设备明明在线程序却总在报错还有一次是在现场调试一个比较老旧的仪表我把timeout设成了0.3秒。在电脑上测试时一切正常因为模拟器响应特别快一接真实仪表就频繁超时因为那个仪表在处理数据时要花好几百毫秒。后来我把超时改成2秒问题立刻消失。超时不是越小越好它只代表“我最多能接受多慢”一个健壮的上位机应该容忍慢速设备而不是用超时去惩罚它。排查这一类问题时我会先用串口监控工具抓原始报文确认报文是否发出去、返回报文是否回来。如果返回在报文已经到了但代码还是判超时那通常是pymodbus的framer解析出错或者串口缓冲区被干扰如果报文根本没回来那就是设备真的没响应重点查地址、寄存器、物理接线。6. 可直接改用的上位机通信模板和进阶方向到现在为止RTU能读能写TCP能读能写已经可以处理80%的上位机数据采集需求了。但真正常规项目还需要把代码结构稍微整理一下方便切换协议、扩展设备、记录日志。6.1 RTU与TCP二合一模板按配置切换我习惯把连接参数放到一个配置文件或者字典里用一个工厂函数统一创建客户端from pymodbus.client import ModbusSerialClient, ModbusTcpClient def create_client(cfg): if cfg[type] rtu: return ModbusSerialClient( portcfg[port], baudratecfg.get(baudrate, 9600), timeoutcfg.get(timeout, 2), ) elif cfg[type] tcp: return ModbusTcpClient( hostcfg[host], portcfg.get(port, 502), timeoutcfg.get(timeout, 2), ) raise ValueError(未知的连接类型) config { type: rtu, port: COM3, baudrate: 9600, timeout: 1.5, } client create_client(config) if not client.connect(): raise SystemExit(连接失败) # 统一调用读取函数不管是RTU还是TCP resp client.read_holding_registers(address0, count10, slave1) ... client.close()这个模板的实际意义在于项目早期你只有串口测试台后来要接厂内局域网PLC只需要改配置字典业务代码一行不动。我靠着这个模式把好几个项目从“USB连仪表”平滑升级成了“网口连网关”省掉了很多不必要的返工。6.2 再往前走界面、日志、自动重连和分布采集当轮询代码稳定之后你大概率会想要一个界面。Python上位机界面的主流选择是PySide6或PyQt。但我不建议一上来就搞大而全的界面更稳妥的路径是先把通信逻辑封装成一个独立模块界面只是把模块的返回值显示出来。日志同样重要。Modbus通信最怕的是“偶发失败”没有日志根本无从排查。我的做法是给每次读写加上日志记录包括时间、从站地址、功能码、寄存器地址、返回值和耗时。等项目跑一段时间后偶发超时的规律基本都能从日志里看出来。另外如果你需要同时采集几十台设备还要考虑把RTU轮询放到线程里避免界面卡死如果是TCP多设备可以考虑异步版本的AsyncModbusTcpClient它可以并发请求多个设备吞吐量比同步循环高得多。但异步代码调试难度也上去了非必要不引入。我实际操作中的体会是上位机开发从来不是“能不能通”的问题而是“稳定几天、出现异常怎么定位、设备重启后能不能自动恢复”的问题。把上面这些通信、重连、日志和点表管理做好一套小小的Python上位机完全可以在生产环境里踏实跑很久。
返回列表