资讯动态

modbus-tk 实战:Python 实现 Modbus RTU/TCP 主从站通信与避坑指南

发布时间:2026/10/16 15:20:51 来源:尧图企业网站定制
简介modbus-tk 是一套基于 Python 的 Modbus 全栈实现库面向需要编写 Modbus TCP 与 RTU 主站、从站的开发者尤其适合测试验证与工业通信类应用开发。它刻意精简依赖仅 RTU 模式需 pyserial与 pymodbus 定位不同可用于数据库日志、HMI、报表生成等实际场景并自带从站模拟器与基于 Web 的 HMI 主站示例。资源包共 48 个文件约 109KB以 28 个 py 源码为核心辅以 tpl 模板、txt 说明、md 文档及 css、js、cfg 等配置与前端资源涵盖 modbus_tk 核心模块、examples 示例、tests 测试与 hmi 演示目录结构清晰便于按模块查阅。目前已有 4427 人学习下载。读者可借此快速理解 Modbus 协议栈的分层设计参考 TCP/RTU 主从示例搭建通信链路并复用模拟器与测试脚本完成功能验证与性能压测是入门与工程落地兼顾的实用工具包。1. 从一根 RS485 线说起modbus-tk 到底解决了谁的痛点车间里一台老 PLC 只认 Modbus RTU上位机却要跑在 Linux 工控机上中间还夹着几台自研的采集板。这种场景下很多人第一反应是找个组态软件拖控件但一旦要批量部署、要嵌进自己的调度逻辑、要做自动化测试组态软件就成了黑匣子。modbus-tk 就是在这个缝隙里活下来的它是一个纯 Python 实现的 Modbus 协议栈主站和从站都能写RTU 和 TCP 都支持不依赖任何商业库。你可以在树莓派上跑一个从站模拟器也可以在服务器上跑一个主站轮询几十个设备。它不解决“设备为什么不通”这种玄学问题但它把协议层的收发、CRC 校验、异常码解析都封装好了让你把精力放在业务逻辑上。适合谁适合那些需要把 Modbus 通信嵌进 Python 脚本、测试框架或数据采集服务里的工程师尤其是手头设备杂、协议文档不全、又不想被某个平台绑死的场景。2. 把 modbus-tk 跑起来环境、最小主站与从站2.1 安装与串口权限的坑modbus-tk 本身是纯 Python 包安装没有编译依赖但串口通信依赖 pyserial。常见做法是直接 pip 安装然后在 Linux 下把当前用户加入 dialout 组否则打开串口会报权限错误。# 安装 modbus-tk 和串口依赖 pip install modbus-tk pyserial # Linux 下查看串口设备 ls /dev/ttyUSB* /dev/ttyS* # 将当前用户加入 dialout 组避免每次 sudo sudo usermod -aG dialout $USER # 重新登录后生效或者临时用 newgrp dialout逻辑说明modbus-tk 的 RTU 主站底层调用 pyserial 打开串口如果用户不在 dialout 组open() 会直接抛 PermissionError。参数上/dev/ttyUSB0是常见 USB 转 485 设备名但不同芯片方案可能枚举成 ttyACM0插拔后编号也可能变生产环境建议用 udev 规则固定别名。2.2 写一个最小 RTU 主站轮询下面这段代码是我在采集项目里最常用的骨架打开串口建主站读保持寄存器异常时打印错误码而不是直接崩掉。import serial import modbus_tk import modbus_tk.defines as cst from modbus_tk import modbus_rtu # 打开串口端口、波特率、数据位、校验、停止位 ser serial.Serial( port/dev/ttyUSB0, baudrate9600, bytesize8, parityN, stopbits1, timeout1.0 # 读超时单位秒 ) # 创建 RTU 主站slave1 是默认从站地址实际轮询时可动态改 master modbus_rtu.RtuMaster(ser) master.set_timeout(1.0) master.set_verbose(True) # 打开后能看到收发原始帧调试期很有用 try: # 读从站地址 1 的保持寄存器起始地址 0读 4 个 data master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 4) print(寄存器值:, data) except modbus_tk.modbus.ModbusError as e: # 异常码2 非法地址3 非法数据值4 从站故障 print(Modbus 异常错误码:, e.get_exception_code()) except Exception as e: print(通信失败:, e) finally: master.close() ser.close()逻辑说明execute的第一个参数是从站地址第二个是功能码后面是起始地址和数量。set_verbose(True)会把发送和接收的字节以十六进制打印出来排查接线和波特率问题时比猜快得多。超时设 1 秒是折中太短容易误判太长会拖慢轮询周期。注意读寄存器数量不要超过从站实际范围否则会返回异常码 2。2.3 用从站模拟器验证主站逻辑没有真实设备时可以在同一台机器上开两个进程一个跑从站模拟一个跑主站轮询。从站模拟器把寄存器映射到内存主站读到的值就是预设值。import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu # 从站模拟监听 /dev/ttyUSB1地址 1 ser serial.Serial(port/dev/ttyUSB1, baudrate9600, bytesize8, parityN, stopbits1, timeout1.0) slave modbus_rtu.RtuSlave(ser, slave1) # 添加一个保持寄存器块起始地址 0长度 16 slave.add_block(holdings, cst.HOLDING_REGISTERS, 0, 16) # 预设几个值模拟传感器数据 slave.set_values(holdings, 0, [100, 200, 300, 400]) slave.start() # 阻塞式运行实际项目里可放到线程中逻辑说明add_block定义了从站暴露的寄存器区域set_values写入初始值。主站读到的就是[100, 200, 300, 400]。如果主站读不到先确认两个串口设备是否真实存在或者用虚拟串口对如 socat在本地模拟一对 tty。参数上从站地址必须和主站execute里的地址一致波特率、校验位、停止位也必须完全一致否则收到的就是乱码。3. 从 RTU 到 TCP协议切换与多从站轮询3.1 TCP 主站的写法差异Modbus TCP 去掉了 CRC 校验多了 MBAP 头modbus-tk 里用TcpMaster替代RtuMaster连接对象从串口变成 socket。下面是最小 TCP 主站示例。import modbus_tk.defines as cst from modbus_tk import modbus_tcp # 连接 TCP 从站host 和 port 按实际填 master modbus_tcp.TcpMaster(host192.168.1.10, port502) master.set_timeout(2.0) try: # 读输入寄存器起始 0读 8 个 data master.execute(1, cst.READ_INPUT_REGISTERS, 0, 8) print(输入寄存器:, data) except Exception as e: print(TCP 通信失败:, e) finally: master.close()逻辑说明TCP 模式下从站地址仍然要填虽然很多网关忽略它但协议帧里保留了这个字段。端口 502 是 Modbus TCP 默认端口如果设备改过端口这里要同步改。超时设 2 秒是因为 TCP 重传机制下太短容易在丢包时误断。3.2 多从站轮询的节奏控制一条 485 总线上挂多个从站时不能并发发请求必须串行轮询且每个请求之间留间隔。下面是一个轮询多个从站的片段用字典维护地址和寄存器映射。import time import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu ser serial.Serial(port/dev/ttyUSB0, baudrate19200, bytesize8, parityE, stopbits1, timeout0.5) master modbus_rtu.RtuMaster(ser) master.set_timeout(0.5) # 从站地址 - 起始地址, 数量 slaves { 1: (0, 4), 2: (0, 4), 3: (10, 2), } while True: for addr, (start, count) in slaves.items(): try: data master.execute(addr, cst.READ_HOLDING_REGISTERS, start, count) print(f从站 {addr}: {data}) except Exception as e: print(f从站 {addr} 异常: {e}) time.sleep(0.05) # 帧间间隔给从站切换时间 time.sleep(1.0) # 一轮结束后等待逻辑说明time.sleep(0.05)是帧间最小间隔485 收发切换需要时间太快会导致从站还没准备好就收到下一帧。波特率 19200 且校验位为偶校验E时一帧 8 字节大约 5 毫秒50 毫秒间隔足够。如果从站响应慢可以适当加大。异常时不要直接退出循环记录后继续轮询其他从站否则一个设备掉线会拖垮整个采集。3.3 寄存器地址映射的常见误区Modbus 文档里的地址有 0 基和 1 基两种写法PLC 手册常写 40001 表示保持寄存器第一个而 modbus-tk 的execute里起始地址填 0。这个偏移差一是新手最容易翻车的地方。文档写法功能码modbus-tk 起始地址说明40001030保持寄存器1 基文档减 130001040输入寄存器1 基文档减 110001020离散输入1 基文档减 100001010线圈1 基文档减 1注意如果读到的值明显不对但通信正常先检查地址偏移再检查数据类型16 位有符号/无符号、32 位高低字顺序。4. 避坑与排查那些让通信时通时断的细节4.1 现象偶尔超时重试又正常原因485 总线终端电阻缺失或线缆过长信号反射导致部分帧校验失败。解决在总线两端各加 120 欧姆终端电阻缩短线缆或降低波特率。软件层面可以加重试机制但重试次数不要超过 3 次否则会掩盖硬件问题。4.2 现象读到的寄存器值跳变或全为 0原因从站地址冲突或者主站轮询间隔太短从站还没处理完上一帧。解决确认总线上没有两个相同地址的从站加大帧间间隔用set_verbose(True)看发出的帧和收到的帧是否对应。4.3 现象TCP 连接正常但读不到数据原因单元标识符从站地址填错或者网关做了地址映射。解决很多 Modbus TCP 网关把单元标识符当作路由地址填 0 或 255 可能被忽略。先试 1再试 0同时确认网关的映射表。4.4 现象程序跑一段时间后串口报错原因串口被其他进程占用或者 USB 转 485 设备热插拔后设备名变了。解决用lsof /dev/ttyUSB0检查占用生产环境用 udev 规则固定设备名捕获异常后重新打开串口而不是让进程退出。4.5 现象浮点数读出来是乱码原因32 位浮点数在 Modbus 里占两个寄存器高低字顺序因设备而异。解决先读两个寄存器按设备手册确定字序再用 struct 拼接。import struct # 假设读到两个寄存器 regs [0x4248, 0x0000] regs [0x4248, 0x0000] # 高字在前 raw struct.pack(HH, regs[0], regs[1]) value struct.unpack(f, raw)[0] print(value) # 50.0 # 如果设备是低字在前交换 regs[0] 和 regs[1] 再解逻辑说明HH表示大端两个无符号短整型f表示大端浮点。字序不对时结果会完全离谱先用已知值如 50.0验证字序再批量解析。5. 进阶把 modbus-tk 嵌进异步采集与自动化测试5.1 用线程池隔离慢设备轮询几十个从站时某个设备响应慢会拖累整轮。常见做法是把每个从站放到独立线程但 485 总线本身是串行的多线程反而增加冲突。更稳的方式是单线程轮询但给每个从站单独设超时超时后跳过下一轮再试。下面是一个带超时隔离的轮询骨架。import time import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu ser serial.Serial(port/dev/ttyUSB0, baudrate9600, bytesize8, parityN, stopbits1, timeout0.3) master modbus_rtu.RtuMaster(ser) master.set_timeout(0.3) slaves [1, 2, 3, 4, 5] fail_count {addr: 0 for addr in slaves} while True: for addr in slaves: try: data master.execute(addr, cst.READ_HOLDING_REGISTERS, 0, 2) fail_count[addr] 0 print(f从站 {addr}: {data}) except Exception: fail_count[addr] 1 if fail_count[addr] 3: print(f从站 {addr} 连续失败标记离线) time.sleep(0.03) time.sleep(0.5)逻辑说明fail_count记录连续失败次数达到阈值后标记离线避免每轮都等超时。超时设 0.3 秒是因为 9600 波特率下一帧很短正常从站应在几十毫秒内响应。如果某个从站经常超时先查接线和地址再考虑降低波特率。5.2 用从站模拟器做自动化回归在 CI 里跑 Modbus 相关代码时不可能接真实设备。用 modbus-tk 的从站模拟器加虚拟串口可以在容器里跑完整的读写回归。下面是一个用 pytest 验证读写一致性的例子。import threading import time import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu def start_slave(port): ser serial.Serial(portport, baudrate9600, bytesize8, parityN, stopbits1, timeout1.0) slave modbus_rtu.RtuSlave(ser, slave1) slave.add_block(holdings, cst.HOLDING_REGISTERS, 0, 16) slave.set_values(holdings, 0, [11, 22, 33, 44]) slave.start() # 在测试夹具里启动从站线程主站连接另一个虚拟串口 # 然后断言 master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 4) (11, 22, 33, 44)逻辑说明虚拟串口对可以用 socat 创建例如socat -d -d pty,raw,echo0 pty,raw,echo0得到两个设备路径一个给从站一个给主站。这样测试不依赖硬件跑在 CI 里也稳定。注意从站线程要设成守护线程测试结束能自动退出。5.3 一个容易被忽略的参数帧间延迟modbus-tk 的 RTU 主站没有内置帧间延迟连续调用execute时帧与帧之间只隔了 Python 解释器执行时间可能只有几毫秒。对于老式从站这个间隔不够。我一般会在每次execute后手动time.sleep(0.02)到0.05具体值用示波器看总线空闲时间来确定。TCP 模式下不需要这个延迟因为网络栈本身有间隔。提示如果从站手册里写了“帧间最小间隔 3.5 字符时间”按波特率算一下。9600 波特率下 3.5 字符约 4 毫秒但实际留 20 毫秒更保险。5.4 验证方法用已知值反推字序和地址调试新设备时我习惯先写一个已知值到从站如果支持写再读回来确认地址和字序。对于只读设备用两个相邻寄存器拼一个已知浮点数看解析结果是否合理。如果读到的值在合理范围内但小数点不对多半是字序问题如果值完全离谱先查地址偏移。这套方法比对着手册猜快得多也是我这些年少有的后悔药——早些年因为没做这一步在客户现场耗了一整天。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑