如何用Python搭建Modbus RTU Slave?现有脚本超时问题排查
问题:搭建Modbus RTU Slave时QModMaster超时的排查与修复
问题描述
需要编写脚本让控制器充当Modbus RTU Slave,响应其他设备请求:控制器需将10个布尔值存储在地址1至10的离散输入中,仅支持通过功能码2读取。尝试用pymodbus实现,但搭建方式存疑,运行脚本时QModMaster出现超时错误,需排查问题所在。
原测试脚本:
from pymodbus.datastore import ModbusSequentialDataBlock from serial import Serial import time import sys import random # Create a serial port serial = Serial(port='COM1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0.5) # Create a Modbus server context store = ModbusSlaveContext( di=ModbusSequentialDataBlock.create(), co=ModbusSequentialDataBlock.create(), hr=ModbusSequentialDataBlock.create(), ir=ModbusSequentialDataBlock.create(), zero_mode=True ) # Create a loop to change the discrete input from adress 1 to 10 every 2 seconds while True: try: # Change the discrete input randomly for i in range(1, 11): store.setValues(1, i, [random.randint(0, 1)]) time.sleep(2) except KeyboardInterrupt: server.stop() sys.exit()
环境说明:控制器运行Windows系统,支持Python执行,配备RS-232(DB9)端口;测试用安装QModMaster的电脑,读取离散输入时触发超时。
原脚本的核心错误
脚本存在多个关键问题,导致Modbus Slave无法正常响应请求:
- 未启动Modbus RTU Server:仅创建了串口和数据存储对象,但未将两者关联、启动Slave服务,串口从未处理任何Modbus请求,必然超时。
- 缺失类导入:使用
ModbusSlaveContext但未从pymodbus.datastore导入,运行时会直接抛出NameError中断脚本。 - 数据块初始化无效:
ModbusSequentialDataBlock.create()默认生成空数据块,未预分配地址1-10的离散输入空间,读取时会返回无效地址错误。 - 数据更新参数错误:
store.setValues(1, i, [...])的第一个参数是数据类型代码,离散输入(DI)对应代码为2,而非1(1是线圈CO的代码),原代码实际在修改线圈而非离散输入。 - 未定义
server对象:异常处理中的server.stop()会抛出NameError,因为server从未初始化。
修正后的脚本
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext from pymodbus.server import StartSerialServer from serial import Serial import time import sys import random # 初始化串口(参数需与QModMaster完全匹配) serial_port = Serial( port='COM1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=0.5 ) # 初始化离散输入数据块:起始地址1,共10个点位,初始值设为0 di_block = ModbusSequentialDataBlock(1, [0]*10) # 创建Slave上下文,仅启用离散输入(DI),关闭其他不需要的存储区 store = ModbusSlaveContext( di=di_block, co=None, # 禁用线圈 hr=None, # 禁用保持寄存器 ir=None, # 禁用输入寄存器 zero_mode=True ) def update_discrete_inputs(): """后台更新离散输入值的线程函数""" while True: # 更新地址1-10的离散输入值 for addr in range(1, 11): # 离散输入对应的数据类型代码是2 store.setValues(2, addr, [random.randint(0, 1)]) time.sleep(2) if __name__ == "__main__": try: # 启动Modbus RTU Slave服务器,同时运行数据更新线程 StartSerialServer( context=store, port=serial_port, timeout=1, custom_processes=[update_discrete_inputs] ) except KeyboardInterrupt: serial_port.close() sys.exit()
关键修改说明
- 补全必要导入:添加
ModbusSlaveContext和StartSerialServer的导入,确保类可用。 - 规范数据块初始化:明确指定离散输入的起始地址和点位数量,保证地址1-10有效。
- 禁用无用存储区:将不需要的线圈、寄存器设为
None,减少资源占用。 - 启动Modbus Server:通过
StartSerialServer关联串口与数据存储,正式启动Slave服务处理请求。 - 线程化数据更新:将数据更新逻辑放在独立线程中,避免阻塞主线程的Modbus请求处理(原脚本的while循环会完全占用主线程,无法响应串口数据)。
- 修正数据类型代码:用
2作为setValues的第一个参数,确保修改的是离散输入。 - 完善异常处理:确保串口正常关闭,避免资源泄漏。
额外排查点
- 确认COM端口号正确:检查Windows设备管理器,确认控制器RS-232端口的实际COM编号。
- 串口接线正确:使用RS-232交叉线(TX-RX交叉连接),确保控制器与测试电脑的串口通信通路正常。
- QModMaster参数匹配:确保软件中的波特率、奇偶校验、停止位、Slave地址(默认1)与脚本完全一致,读取时选择功能码2,起始地址1,数量10。
内容的提问来源于stack exchange,提问作者Sara Delahaie
相关产品推荐
相关产品推荐

