简介modbus-tk 是一套基于 Python 的 Modbus 协议全栈实现面向需要快速搭建 Modbus TCP/RTU 主站与从站的开发者、测试工程师及自动化项目人员。它既能用于协议测试也适合构建数据库日志、HMI、报表生成等实际通信应用且依赖精简与 pymodbus 定位不同名称中的 tk 指 testkit 而非 tkInter。资源包共 48 个文件约 109KB以 28 个 py 源码为核心辅以 tpl 模板、txt 说明、md 文档、css 与 js 前端资源、cfg 配置及 db 数据文件覆盖库本体、示例与测试目录。内容包含 RTU/TCP 主从示例、从站模拟器、基于 Web 的 HMI 主控、单元测试与性能测试脚本以及 setup.py 安装配置便于读者直接运行验证并二次开发。目前已有 4427 人学习下载适合希望低成本掌握 Modbus 通信、快速复用现成模块的 Python 使用者。1. 用 modbus-tk 把 Modbus 从协议文档变成能跑的 Python 代码很多人第一次接触 Modbus是从一份几十页的协议手册开始的功能码、寄存器地址、线圈、CRC 校验、RTU 帧间隔……看完一圈脑子里全是名词手上却一行代码都跑不起来。modbus-tk 这个库的价值就在这儿——它把 Modbus 的帧结构、主从角色、超时重试这些底层细节封装成 Python 对象让你用几行代码就能起一个从站、发一条读保持寄存器的请求。它适合两类人一类是要在工控现场快速验证设备通信的工程师另一类是想把 Modbus 集成进自己 Python 数据采集脚本的开发者。这一章先把「modbus-tk 到底解决什么问题」讲清楚后面几章再落到安装、建站、读写、排错和进阶技巧上。Modbus 本身是主从架构一个主站发请求从站应答从站不会主动说话。modbus-tk 把这件事拆成两个核心类——主站侧用ModbusMaster从站侧用ModbusSlave再配合ModbusServer管理串口或 TCP 连接。你不需要自己拼字节、算 CRC只要告诉它「读哪个从站、哪个功能码、起始地址、读几个」它就把请求帧发出去把应答解析成整数列表返回。常见做法是先用 modbus-tk 在本地起一个模拟从站再用主站去读它这样在没有真实硬件的情况下也能把逻辑跑通。我一般会建议新手先走这条路因为现场设备一旦接错线、波特率对不上排查成本比在本地高得多。2. 环境准备与最小可运行的主从通信2.1 安装 modbus-tk 与串口依赖modbus-tk 本身是纯 Python 实现但它依赖 pyserial 来做串口通信。如果你只用 Modbus TCPpyserial 不是必须的但装了也不影响。安装命令很直接pip install modbus-tk pyserial这里有个细节modbus-tk 在 PyPI 上的包名就是modbus-tk导入时写import modbus_tk注意下划线和连字符的区别。如果你在虚拟环境里装确认 pip 对应的 Python 版本和你运行脚本的版本一致否则会出现「装了但 import 不到」的玄学问题。我一般会先跑一句python -c import modbus_tk; print(modbus_tk.__version__)确认版本再往下走。2.2 用 Modbus TCP 起一个最小从站先从 TCP 入手因为它不依赖串口硬件最容易复现。下面这段代码起了一个从站监听 5020 端口从站地址为 1并在保持寄存器地址 0 到 9 上写入初始值import modbus_tk import modbus_tk.defines as cst from modbus_tk import modbus_tcp # 创建 TCP 从站监听本地 5020 端口 slave modbus_tcp.TcpServer(port5020) slave.start() # 添加从站设备地址为 1 slave_1 slave.add_slave(1) # 添加保持寄存器块起始地址 0长度 10 slave_1.add_block(holdings, cst.HOLDING_REGISTERS, 0, 10) # 给寄存器写入初始值方便主站读取验证 slave_1.set_values(holdings, 0, [100, 200, 300, 400, 500]) print(从站已启动监听 5020 端口)逻辑说明TcpServer负责网络监听add_slave(1)注册了一个从站地址为 1 的设备add_block定义了一块可被访问的寄存器区域。set_values的第三个参数是起始地址第四个参数是值列表。参数上要注意add_block的地址范围一旦设定主站访问超出这个范围的地址会直接返回异常码不会自动扩展。端口选 5020 而不是标准的 502是为了避免在本地测试时和系统服务冲突也避免权限问题。2.3 主站读取从站寄存器的完整脚本从站跑起来后另开一个终端跑主站import modbus_tk.defines as cst from modbus_tk import modbus_tcp # 连接从站指定 IP 和端口 master modbus_tcp.TcpMaster(host127.0.0.1, port5020) master.set_timeout(2.0) # 读保持寄存器从站地址 1起始地址 0读 5 个 values master.execute( slave1, function_codecst.READ_HOLDING_REGISTERS, starting_address0, quantity_of_x5 ) print(读取到的寄存器值:, values)逻辑说明execute是主站的核心方法slave是从站地址function_code用cst里的常量starting_address和quantity_of_x分别对应起始地址和读取数量。set_timeout设的是秒不是毫秒这一点和很多工控库不一样容易踩坑。如果读不到先确认从站进程还在跑再确认端口没被防火墙拦。常见做法是把主站和从站放在同一台机器上先跑通再拆到两台机器验证网络。3. 串口 RTU 模式下的参数配置与读写3.1 RTU 从站的串口参数怎么设Modbus RTU 跑在串口上参数比 TCP 多波特率、数据位、停止位、校验位、串口号。下面是一个 RTU 从站的例子假设你用的是虚拟串口对或者真实串口import modbus_tk.defines as cst from modbus_tk import modbus_rtu import serial # 打开串口参数要和主站完全一致 slave modbus_rtu.RtuServer( serial.Serial( port/dev/ttyUSB0, # Windows 下换成 COM3 之类 baudrate9600, bytesize8, parityN, stopbits1, xonxoff0 ) ) slave.start() slave_1 slave.add_slave(1) slave_1.add_block(coils, cst.COILS, 0, 16) slave_1.set_values(coils, 0, [1, 0, 1, 1, 0, 0, 1, 0]) print(RTU 从站已启动)逻辑说明RtuServer接收一个已经打开的serial.Serial对象所以串口参数是在serial.Serial里设的不是 modbus-tk 设的。parityN表示无校验对应 Modbus 里常说的 8N1。bytesize8、stopbits1也是 8N1 的组成部分。参数上最容易翻车的是波特率和校验位主站设 9600 8N1从站设 19200 8E1两边都收不到正确数据但报错信息往往只是超时不会直接告诉你参数不匹配。3.2 主站写线圈和写寄存器的区别读操作之外写操作分两类写单个和写多个。线圈用WRITE_SINGLE_COIL或WRITE_MULTIPLE_COILS保持寄存器用WRITE_SINGLE_REGISTER或WRITE_MULTIPLE_REGISTERS。下面演示写多个保持寄存器import modbus_tk.defines as cst from modbus_tk import modbus_rtu import serial master modbus_rtu.RtuMaster( serial.Serial(port/dev/ttyUSB0, baudrate9600, bytesize8, parityN, stopbits1) ) master.set_timeout(1.0) master.set_verbose(True) # 打开调试输出能看到收发的原始帧 # 写多个保持寄存器起始地址 0写入 3 个值 master.execute( slave1, function_codecst.WRITE_MULTIPLE_REGISTERS, starting_address0, quantity_of_x3, output_value[11, 22, 33] ) # 回读验证 values master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 3) print(回读结果:, values)逻辑说明set_verbose(True)会打印收发的十六进制帧排查通信问题时非常有用。output_value是写入的值列表长度要和quantity_of_x一致否则会抛异常。写单个和写多个的区别不只是数量有些老设备只支持写单个你用写多个它会返回非法功能码异常。我一般会先查设备手册确认支持哪些功能码再决定用哪个。3.3 地址偏移PLC 地址和协议地址差 1 的问题这是 Modbus 里最经典的坑。设备手册上写「保持寄存器 40001」但协议里的起始地址是 0。也就是说手册地址 40001 对应协议地址 040002 对应 1以此类推。modbus-tk 用的是协议地址不是手册地址。如果你直接拿手册上的 40001 去读会读到偏移一位的数据或者直接报非法地址。常见做法是看到 4xxxx 就减 40001看到 3xxxx 就减 30001得到协议地址再传给 modbus-tk。这个规则不是 modbus-tk 特有的而是 Modbus 协议本身的约定但新手很容易在这里翻车。4. 避坑与排查那些让通信失败的小细节4.1 超时设成毫秒导致一直超时现象主站调用execute后立刻抛超时异常但从站日志显示请求已经收到。原因set_timeout的单位是秒有人按毫秒习惯写了set_timeout(1000)结果变成 1000 秒看起来像卡死也有人写了set_timeout(0.001)1 毫秒对串口来说太短正常应答还没回来就超时了。解决串口 RTU 一般设 0.5 到 2 秒TCP 设 1 到 3 秒先用set_verbose(True)看帧有没有发出去、有没有回来。4.2 从站地址 0 被当成广播现象主站用从站地址 0 发请求从站不回应。原因Modbus 里地址 0 是广播地址从站收到广播请求后执行但不应答。解决单点通信时从站地址用 1 到 247 之间的值不要用 0。如果你确实要做广播写那就要接受「没有应答」这个事实不能再用execute等返回值。4.3 寄存器块没添加就访问现象主站读地址 100从站返回非法数据地址异常。原因从站只add_block了 0 到 9 的范围地址 100 不在块内。解决add_block时把地址范围设得比实际需要略大一点或者按设备手册的地址范围完整添加。注意add_block的地址范围是左闭右开的区间语义起始地址加长度就是上界别算错。4.4 串口被占用导致打不开现象serial.Serial抛PermissionError或SerialException。原因串口已经被另一个进程打开或者上一次脚本异常退出没释放。解决确认没有其他程序占用串口Linux 下可以用lsof /dev/ttyUSB0查Windows 下用设备管理器看。脚本里用try/finally确保master.close()或slave.stop()被调用别让异常路径跳过关闭逻辑。4.5 字节序和寄存器顺序理解错现象读到的值明显不对比如期望 1000 读到 4096。原因Modbus 寄存器是 16 位的32 位数据要占两个寄存器高低字顺序不同设备可能不一样。modbus-tk 返回的是寄存器列表不会自动帮你拼 32 位。解决先读两个寄存器确认设备用的是高字在前还是低字在前再自己用位运算拼接。常见做法是(high 16) | low或(low 16) | high具体看手册。5. 进阶用数据块和自定义功能码把 modbus-tk 用得更顺5.1 用 DataBlock 管理寄存器映射当寄存器数量多起来手动set_values和get_values会很乱。modbus-tk 的DataBlock可以让你按名字访问寄存器适合把设备点表映射成代码里的结构。下面是一个简单示例import modbus_tk.defines as cst from modbus_tk import modbus_tcp from modbus_tk.modbus import DataBlock # 自定义一个数据块把寄存器地址和业务名称对应起来 class MyBlock(DataBlock): def __init__(self): super().__init__(myblock, cst.HOLDING_REGISTERS, 0, 20) slave modbus_tcp.TcpServer(port5020) slave.start() slave_1 slave.add_slave(1) slave_1.add_block(myblock, cst.HOLDING_REGISTERS, 0, 20) # 按业务含义写入地址 0 是温度地址 1 是压力 slave_1.set_values(myblock, 0, [250, 1013]) print(温度:, slave_1.get_values(myblock, 0, 1)) print(压力:, slave_1.get_values(myblock, 1, 1))逻辑说明DataBlock本身不改变协议行为它只是帮你把地址和含义对应起来。实际项目里我一般会再包一层函数把「读温度」翻译成「读地址 0」这样业务代码里不出现裸地址。参数上注意add_block的名字要和后续set_values、get_values用的名字一致大小写敏感。5.2 用钩子函数拦截请求做日志和模拟modbus-tk 允许你在从站上挂钩子在请求处理前后插入自己的逻辑。这在做协议分析或模拟异常时很有用import modbus_tk.defines as cst from modbus_tk import modbus_tcp def on_before_request(request): # request 里包含从站地址、功能码、起始地址等信息 print(收到请求:, request) slave modbus_tcp.TcpServer(port5020) slave.start() slave_1 slave.add_slave(1) slave_1.add_block(holdings, cst.HOLDING_REGISTERS, 0, 10) slave_1.set_values(holdings, 0, [1, 2, 3]) # 挂上钩子每个请求进来前都会打印 slave_1.add_hook(on_before_request, cst.READ_HOLDING_REGISTERS)逻辑说明add_hook的第一个参数是回调函数第二个参数是功能码表示只对这个功能码的请求生效。回调函数里可以拿到请求对象用来做日志、鉴权或者故意返回异常来测试主站的容错。参数上注意钩子函数的签名要和 modbus-tk 期望的一致不同版本可能有差异建议先打印request对象看看它有哪些属性。5.3 验证方法用 verbose 和抓包交叉确认调 Modbus 最怕「猜」。我一般会同时开两个验证手段一是主站set_verbose(True)看发出的帧和收到的帧二是用串口调试工具或网络抓包工具看物理层数据。两边对上了说明协议层没问题对不上就看是发送端没发出去还是接收端没收到。对于 RTU还要注意帧间隔Modbus RTU 要求帧之间至少 3.5 个字符时间的静默modbus-tk 内部会处理但如果你自己拼帧发送这个间隔没留够从站会丢帧。TCP 没有这个问题但要注意 TCP 粘包modbus-tk 的 TCP 实现已经处理了长度字段不用自己拆。5.4 一个我常犯的错忘了关从站导致端口占用最后说一个血泪经验。我早期写测试脚本时从站启动后忘了在finally里调slave.stop()脚本异常退出后端口还占着下一次运行直接报端口被占用。后来我养成了习惯只要start()了就一定在finally里stop()主站也一样master.close()不能省。这个习惯看起来小但能省掉很多「为什么昨天能跑今天跑不了」的后悔药时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?