用Python和pymodbus库模拟Modbus RTU主从通信(附完整代码和报文分析)

用Python和pymodbus库模拟Modbus RTU主从通信(附完整代码和报文分析) Python实战用pymodbus模拟Modbus RTU主从通信全流程解析工业自动化领域的数据采集离不开设备间的可靠通信。Modbus RTU作为最常用的串行通信协议之一其简洁高效的特性使其在PLC、传感器等设备中广泛应用。本文将带您用Python的pymodbus库从零搭建一个完整的Modbus RTU通信实验环境通过代码实例深入解析协议细节。1. 实验环境搭建在开始编码前我们需要准备以下工具链Python 3.8推荐使用Anaconda管理环境pymodbus 3.0支持同步/异步通信的Modbus库虚拟串口工具如com0comWindows或socatLinux/Mac串口调试助手如AccessPort或PuttyWireshark用于抓包分析需安装串口捕获插件安装核心依赖pip install pymodbus3.1.3 pyserial3.5虚拟串口配置示例Windows使用com0com安装后创建配对的虚拟串口如COM3-COM4设置波特率96008数据位无校验1停止位8N1在设备管理器中确认端口可用性注意Linux/Mac用户可使用socat -d -d pty,raw,echo0 pty,raw,echo0创建虚拟终端2. Modbus从站模拟实现我们先构建一个支持多功能的Modbus从站服务。以下代码模拟了包含线圈、寄存器的完整设备from pymodbus.server import StartSerialServer from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext def setup_slave(): # 初始化数据存储 coil_block ModbusSequentialDataBlock(0x00, [False]*100) # 100个线圈 input_block ModbusSequentialDataBlock(0x00, [False]*100) # 离散输入 holding_regs ModbusSequentialDataBlock(0x00, [0]*100) # 保持寄存器 input_regs ModbusSequentialDataBlock(0x00, [0]*100) # 输入寄存器 # 创建从站上下文 slave_context ModbusSlaveContext( diinput_block, cocoil_block, hrholding_regs, irinput_regs ) # 多从站支持地址1-10 slaves {i: slave_context for i in range(1, 11)} context ModbusServerContext(slavesslaves, singleFalse) # 启动RTU服务 StartSerialServer( contextcontext, portCOM3, # 对应虚拟串口 framerModbusRtuFramer, baudrate9600, timeout0.005 ) if __name__ __main__: setup_slave()关键参数说明参数说明典型值port虚拟串口名称COM3/COM4baudrate波特率9600/19200timeout帧间隔超时秒0.003-0.01parity校验位N无校验3. Modbus主站请求实现主站通过功能码与从站交互。以下是读取保持寄存器的完整示例from pymodbus.client import ModbusSerialClient import struct class ModbusMaster: def __init__(self, port): self.client ModbusSerialClient( methodrtu, portport, baudrate9600, timeout1 ) self.client.connect() def read_holding_registers(self, slave_id, address, count): response self.client.read_holding_registers( addressaddress, countcount, slaveslave_id ) if response.isError(): print(fError: {response}) return None # 解析寄存器值为不同数据类型 raw_data response.registers return { raw: raw_data, int16: [x if x 32768 else x-65536 for x in raw_data], uint32: [struct.unpack(I, bytes([x8, x0xFF, y8, y0xFF]))[0] for x,y in zip(raw_data[::2], raw_data[1::2])], float32: [struct.unpack(f, bytes([x8, x0xFF, y8, y0xFF]))[0] for x,y in zip(raw_data[::2], raw_data[1::2])] } def write_single_register(self, slave_id, address, value): response self.client.write_register( addressaddress, valuevalue, slaveslave_id ) return not response.isError() # 使用示例 if __name__ __main__: master ModbusMaster(COM4) # 读取从站1的保持寄存器0-2 result master.read_holding_registers(slave_id1, address0, count3) print(f读取结果: {result}) # 写入单个寄存器 success master.write_single_register(slave_id1, address0, value1234) print(f写入状态: {成功 if success else 失败})4. 通信数据帧深度解析通过串口捕获工具获取的原始报文十六进制请求帧主→从01 03 00 00 00 03 05 CB逐字节解析字节位置值含义00x01从站地址10x03功能码读保持寄存器2-30x0000起始寄存器地址4-50x0003寄存器数量6-70x05CBCRC校验响应帧从→主01 03 06 00 21 00 00 00 00 9D 72响应帧结构字节位置值含义00x01从站地址10x03功能码20x06数据字节数3寄存器×2字节3-80x002100000000寄存器数据9-100x9D72CRC校验常见功能码操作对照表功能码名称请求示例响应示例0x01读线圈01 01 00 00 00 08 3D CC01 01 01 01 90 480x05写单个线圈01 05 00 00 FF 00 8C 3A回显请求帧0x10写多寄存器01 10 00 00 00 01 02 00 0F E6 5401 10 00 00 00 01 01 C95. 高级应用与故障排查5.1 自定义数据块处理通过继承ModbusDatastore实现动态数据class DynamicDataBlock(ModbusBaseDatastore): def __init__(self): self._data {} def setValues(self, address, values): for idx, val in enumerate(values): self._data[address idx] val def getValues(self, address, count1): return [self._data.get(address i, 0) for i in range(count)]5.2 典型错误处理常见错误码及解决方案0x01非法功能码 → 检查从站是否支持该功能0x02非法数据地址 → 验证寄存器地址范围0x03非法数据值 → 检查写入值是否越界0x04从站设备故障 → 检查从站运行状态调试建议使用logging.basicConfig(levellogging.DEBUG)开启详细日志在关键操作后添加CRC校验验证from pymodbus.utilities import checkCRC, computeCRC def validate_crc(frame): crc computeCRC(frame[:-2]) return frame[-2:] bytes([crc 8, crc 0xFF])5.3 性能优化技巧批量读写替代单次操作适当调整timeout避免帧不完整对高频读取数据实现本地缓存使用异步客户端AsyncModbusSerialClient提升吞吐量实际项目中我们发现当波特率≥115200时需要精确计算帧间隔时间。一个实用的经验公式最小帧间隔 (11 bits/byte × 字节数) / 波特率 0.001安全余量