modbus-tk实战:用Python实现Modbus主从站与数据采集

📅 发布时间:2026/9/2 12:37:37
modbus-tk实战:用Python实现Modbus主从站与数据采集
简介modbus-tk 是一套基于 Python 的 Modbus 协议栈实现面向需要快速搭建 Modbus TCP/RTU 主站或从站的开发者尤其适合工业自动化测试、SCADA 集成和通信原型验证。包内共包含 48 个文件核心以 28 个 py 源码文件为主覆盖协议定义、RTU/TCP 处理、模拟器与主站逻辑另有 5 个模板文件用于基于 Web 的 HMI 界面以及 txt/cfg 配置和说明文档整体约 109KB结构紧凑便于阅读和二次开发。目前已有 4422 人学习下载。压缩包除完整库源码外还提供多个可直接运行的 RTU/TCP 主从站示例脚本、单元与功能测试模块并附带依赖说明和开发配置能帮助开发者快速理解 Modbus 通信机制搭建测试从站、数据采集或日志记录等实际应用。 去年做产线数据采集的小改造PLC那边只肯把数据扔进寄存器上位机要用Python去读。原以为这是几分钟的事结果设备厂家给的Demo是Windows下跑的VB程序根本没法直接搬到Linux服务器想先抓包看看报文长什么样手里连个能正常响应的Modbus从站模拟器都没有。折腾到半夜翻到modbus-tk这个库问题才真正收场。今天就把这条路上踩过的坑和能直接抄的代码都写出来。modbus-tk是一个纯Python实现的Modbus协议工具包既能当主站也能当从站TCP和RTU都支持。对做工业数据采集、设备联调、协议仿真的人来说它的价值在于不用再求着设备厂家发测试软件也不用在工控机上装一堆破解版模拟器一个Python脚本就能把从站和主站都演出来。这篇文章适合刚接触Modbus协议的人也适合已经在用pymodbus但觉得文档啰嗦、想换个轻量方案的人。1. 为什么工业通信里我会用modbus-tk1.1 这个库到底解决了什么问题Modbus大概是工业现场最皮实的通信协议了PLC、仪表、变频器、温控器几乎标配。但皮实归皮实开发联调时有个很现实的问题设备不一定随时在场或者说设备的寄存器表是PDF文档你没法对着纸面写代码。modbus-tk能让你直接在电脑上把一个虚拟设备跑起来把文档里的寄存器区域先模拟出来。这个虚拟设备响应真实的Modbus报文你的采集程序连它跟连真机是一模一样的路径。我之前做采集网关时就是先用modbus-tk模拟了三个从站把整个采集调度逻辑跑通再到现场换真机只改了IP和寄存器地址表就上线了。另一个用处是抓报文。modbus-tk内置了日志功能把报文按十六进制打出来。遇到为什么设备不回包这类问题先在日志里看请求帧长什么样再判断是地址问题、CRC问题还是功能码问题比盲猜高效得多。1.2 modbus-tk和pymodbus、minimalmodbus怎么选Python生态里做Modbus的库不少常见的就是modbus-tk、pymodbus、minimalmodbus。我自己的选型结论是TCP调试和从站仿真首选modbus-tk纯RTU小工具可以考虑minimalmodbus需要异步高并发再去看pymodbus。三者的差别我用一个表说清楚对比项modbus-tkpymodbusminimalmodbus从站Slave/Server支持支持且写法简单支持但异步写法绕不支持RTU主站支持支持支持TCP主站支持支持不支持调试日志内置开箱即用需要配logging几乎没有依赖只需pyserial较重只需pyserial学习成本低中高最低modbus-tk在代码风格上很老派没有一堆抽象类核心概念就三个server或master、slave、block。理解这三样东西整个库就通了。而pymodbus近年主推client/server的异步架构功能是很强但对只想快速读几个寄存器的场景写上多线程异步反而累赘。提示如果你只是给树莓派接个传感器模块读几个寄存器minimalmodbus可能更省事但只要你需要模拟从站、或者以后要扩展成多设备采集modbus-tk的性价比更高。2. 先跑起来Modbus TCP从站的完整实现2.1 环境准备与端口权限安装非常简单pip install modbus-tk pyserial版本方面我一直在用它的老版本也没有问题因为Modbus协议几十年没变过这类库的接口相对稳定。需要提醒的是端口权限Modbus TCP默认走502端口Linux下普通用户没权限绑定1024以下的端口。我在Ubuntu上第一次跑从站直接遇到PermissionError解决办法要么用sudo要么改到5020之类的测试端口。联调阶段我建议直接用5020因为现场如果还有其他软件占着502排查起来很烦。2.2 最小编码一个能响应的从站下面这段代码是我每次测试都会用到的种子代码定义了一个从站地址为1的设备里面有一块保持寄存器起始地址0长度10import time import modbus_tk.defines as cst import modbus_tk.tcp as modbus_tcp server modbus_tcp.TcpServer(address127.0.0.1, port5020) server.start() slave server.add_slave(1) slave.add_block(holding, cst.HOLDING_REGISTERS, 0, 10) slave.set_values(holding, 0, [100, 200, 300, 400, 500]) print(slave is running at 127.0.0.1:5020, slave id1) while True: server.process_request() time.sleep(0.01)这段代码里有个非常容易忽略的细节server.start()只启动了后台线程真正去处理请求的是process_request()。如果不写这个循环从站进程虽然活了但谁连都没反应。很多第一次用modbus-tk的人从站跑起来却连不上八成是漏了这个循环。add_block是核心方法三个参数分别是块名、寄存器类型、起始地址、长度。寄存器类型就四种cst.COILS线圈、cst.DISCRETE_INPUTS离散输入、cst.INPUT_REGISTERS输入寄存器、cst.HOLDING_REGISTERS保持寄存器。这个块定义就决定了从站对外暴露的地址空间边界。2.3 用日志验证从站是否正常modbus-tk内置了日志功能我建议在调试阶段直接开启import modbus_tk.utils as utils logger utils.create_logger(console, levelDEBUG)开启后终端会打印出收到的帧和返回的帧比如DEBUG:modbus_tk:1: 00 01 00 00 00 06 01 03 00 00 00 0A DEBUG:modbus_tk:1: 00 01 00 00 00 17 01 03 14 00 64 ...从这些裸报文里能直观地看到MBAP头、从站地址、功能码、地址和长度。这一步对理解协议特别有帮助。我后来看现场问题也养成了先开日志的习惯比抓包工具更轻量。3. 主站侧读写execute函数是真正的主心骨3.1 主站最简用法主站侧代码更短核心就是一个execute方法import modbus_tk.defines as cst import modbus_tk.tcp as modbus_tcp master modbus_tcp.TcpMaster(127.0.0.1, 5020) master.set_timeout(3.0) # 读保持寄存器从站1起始地址0读5个 values master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 5) print(values) # 写单个寄存器从站1地址3写值666 master.execute(1, cst.WRITE_SINGLE_REGISTER, 0, output_value666) # 写多个寄存器从站1地址5覆盖写 master.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 5, output_value[11, 22, 33])execute的参数顺序是从站ID、功能码、起始地址、数量、关键字参数output_value。读操作时第四位是读取数量写操作时第四位在有些版本里也要求填我通常统一填和写入长度一致的数字避免某些设备较真。3.2 主站能做的全部操作modbus-tk把常用的Modbus功能码都定义在defines里主站能操作的类型基本全覆盖。下面是我整理的一份速查表操作意图功能码定义说明读线圈cst.READ_COILS读0/1开关量读离散输入cst.READ_DISCRETE_INPUTS读只读开关量读保持寄存器cst.READ_HOLDING_REGISTERS读写寄存器最常用读输入寄存器cst.READ_INPUT_REGISTERS只读寄存器常用于模拟量写单个线圈cst.WRITE_SINGLE_COIL操作单个开关写单个寄存器cst.WRITE_SINGLE_REGISTER操作单个寄存器写多个线圈cst.WRITE_MULTIPLE_COILS批量写开关写多个寄存器cst.WRITE_MULTIPLE_REGISTERS批量写寄存器主站写线圈时output_value传布尔值或0/1都可以。写寄存器时注意Modbus寄存器是16位无符号数范围0到65535。如果设备用有符号数表示负值写入前要自己做转换比如-1在寄存器里实际是65535。3.3 超时设置别太贪心set_timeout(3.0)这个参数我吃过亏。一开始为了让系统稳定把超时设成10秒结果现场有一个从站掉线整个采集线程卡在execute里10秒才报错后面的设备全被堵住。Modbus TCP本身是请求-响应模式超时时间应该反映设备最坏情况下的响应时间而不是我有多大的耐心。对PLC这类设备1到3秒足够对某些老仪表可能要到5秒。建议超时设置单独抽成配置项不同设备给不同值不要一刀切。注意主站的TcpMaster是线程安全的吗实测下来并非完全安全如果多个线程同时用同一个master实例去轮询不同从站偶尔会出现响应错乱。稳妥做法是每个线程各自创建master或者用一个线程做串行轮询。4. 串口现场RTU模式的部署要点4.1 RtuMaster读串口设备TCP是调试时的舒适区但工业现场大量设备还是走RS485。modbus-tk对RTU的支持同样到位代码结构和TCP类似只是底层换成了串口import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu master modbus_rtu.RtuMaster( serial.Serial( portCOM3, baudrate9600, bytesize8, parityN, stopbits1, timeout0.5, ) ) master.set_timeout(3.0) values master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 10) print(values)串口参数必须和设备侧完全一致特别是校验位和停止位。很多设备默认8-N-1但部分老仪表用Even校验这个错了连帧都对齐不了。4.2 RtuServer模拟串口从站modbus-tk也可以把电脑变成一个串口从站这在用真实触摸屏或组态软件调试时特别有用import serial import modbus_tk.defines as cst from modbus_tk import modbus_rtu server modbus_rtu.RtuServer( serial.Serial(portCOM3, baudrate9600, bytesize8, parityN, stopbits1) ) server.start() slave server.add_slave(1) slave.add_block(coil, cst.COILS, 0, 8) slave.set_values(coil, 0, [True, False, True]) while True: server.process_request()串口从站和TCP从站有个重要区别RS485是半双工总线同一时刻只能有一个节点发送。如果电脑上挂着好几个串口工具同时打开同一个COM口或者主站在报文中设置了广播地址却要求从站回包都会造成总线冲突。联调时务必保证串口只被一个程序占用。4.3 RTU的时序敏感性问题RTU协议的帧靠空闲间隔来分割标准规定两个帧之间至少要有3.5个字符时间的静默。这个时序直接影响主站的轮询速度。我之前用9600波特率测试一条读请求加上响应大概70毫秒左右如果线程里还有其他耗时操作轮询周期会被拖长。modbus-tk在RTU模式下对串口超时比较敏感串口的timeout参数不能设太短。实测下来串口timeout设0.5秒配合set_timeout(3.0)在大多数设备上表现稳定。如果你发现读数据时偶发超时先把串口timeout放到1秒试试而不是立刻怀疑设备有问题。5. 实战踩坑02异常、地址错位这些坑是怎么炸的5.1 02 Illegal Data Address的根因Modbus调试中最常见的报错就是主站收到异常码02Illegal Data Address。我用modbus-tk时就经常接到这种反馈尤其是配合Modbus Poll这类工具时界面上直接红字提示。这个异常的含义是请求里带的寄存器地址超出了从站定义的block范围。modbus-tk从站侧如果收到这种请求也会返回这个异常码。根因几乎都是同一个地址理解错了。Modbus协议里的地址从0开始但很多设备说明书喜欢把地址写成40001这种五位数。40001对应的实际地址是040002对应1以此类推。如果你在add_block里定义起始地址0长度10然后主站去请求地址40001换算成协议地址就是0正常但如果直接往execute里传40001那从站会认为你请求的是地址40001远远超出block范围直接给你02异常。解决方案很简单所有地址换算统一在配置层做不要散落在业务代码里。我习惯在配置表里直接写协议地址后面对应好block定义减少脑子里的换算。5.2 block的起始地址和长度定义错了整个表全废add_block(holding, cst.HOLDING_REGISTERS, 100, 20)的意思是这个块覆盖地址100到119一共20个寄存器。很多人以为第三个参数是第几个块结果定义成0又去读100只返回一片异常。调试时可以用一个笨办法验证block定义从站启动后用主站去读整个块范围能正常返回就说明定义没问题再去缩小范围读业务地址。如果连块范围都读不出来先检查从站日志里打印的block参数。5.3 串口CRC不对专坑新手RTU模式每帧末尾有2字节CRC16校验。modbus-tk在发送时自动计算接收时自动校验。如果设备完全不响应打开从站日志如果显示CRC校验失败基本可以断定串口参数不对或者总线上有信号干扰。现场RS485布线经常是两根线拧在一起走很长的线干扰大时CRC错误率会直线上升。软件层面唯一能做的就是把波特率降下来以及把轮询间隔放宽。硬件层面务必检查A/B线有没有接反接地是否共地。我遇到过最离谱的一次是485转换器的地线没接一开变频器就CRC错接上地线后整个世界清净了。5.4 功能码选错设备不理你Modbus的四种数据区域各有对应功能码读线圈是01读离散输入是02读保持寄存器是03读输入寄存器是04。新手最常犯的错是设备文档说模拟量在输入寄存器却用读取保持寄存器的功能码去读。在modbus-tk从站里如果你只定义了cst.INPUT_REGISTERS块主站却用READ_HOLDING_REGISTERS功能码去请求从站会返回异常。很多设备固件也会直接忽略这类请求。判断方法是先看设备文档确认数据区域类型再选择对应的功能码定义。我见过不少同事查了半天的设备不通信问题最后就是功能码用错。6. 数据解析从寄存器里读出一个像样的浮点数6.1 寄存器里装的不是数是字节Modbus寄存器是16位无符号数但现场设备经常用多个寄存器拼一个32位浮点数或者32位整数。modbus-tk本身只负责搬运不负责解释业务数据所以这部分得自己写。比如某台仪表的温度值是32位浮点数占用寄存器地址0和1读取和解析的代码是这样的import struct data master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 2) # data [reg0, reg1] raw struct.pack(HH, data[0], data[1]) temperature struct.unpack(f, raw)[0] print(temperature)HH表示两个16位无符号整数按大端字节序打包f表示按大端解析成浮点数。6.2 字节序和寄存器顺序没有标准答案这里是最容易掉头发的地方。不同厂家对浮点数的存储方式不一样常见的有大端寄存器序 大端字节序地址0放高16位地址1放低16位也就是上面示例的写法小端寄存器序 小端字节序地址0放低16位地址1放高16位如果解析出来是个天文数字或者是个乱七八糟的负数通常就是字节序不对。我实测过的几类设备PLC多是上述第一类但某些台湾和国产仪表反而喜欢第一类个别仪表用第二类。没有文档的话就只能挨个试。有个技巧如果设备文档里给了已知值的示例比如温度25度时寄存器值是0x41C80000那你可以用这个值去反推字节序。把两个寄存器的十六进制打出来对比就能确定顺序。6.3 位打包和字符串也要掌握除了浮点数设备还经常把多个布尔量塞进一个寄存器用位来表示。比如寄存器第0位表示运行状态第1位表示故障。解析方法就是把读取的数值和掩码做与运算reg_value master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 1)[0] running bool(reg_value 0x0001) fault bool(reg_value 0x0002)字符串在Modbus里一般一个寄存器存两个字符或一个字符看设备定义。解析时要把每个寄存器的值转成字节再拼接最后decode。这类数据我建议单独写一个解析函数别堆在主流程里否则后期维护会想骂人。6.4 写浮点数的反向操作写浮点数和读取是对称的先把Python浮点数转成4字节再拆成两个16位寄存器def float_to_registers(value): raw struct.pack(f, value) regs struct.unpack(HH, raw) return list(regs) regs float_to_registers(25.6) master.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 0, output_valueregs)实测下来给PLC写设定值、给变频器写频率基本都是这么一套流程。只要确认了设备的字节序读写就是同一套逻辑反过来而已。最后再说一个我踩过的小坑无论读还是写一定先拿modbus-tk的从站侧配合日志验证一遍字节序再上真机。用虚拟从站把整个收发闭环调通了现场就算出了问题也知道问题一定出在设备寄存器定义侧排查范围能缩小一大半。本文还有配套的精品资源点击获取