Python pyserial读取Modbus RTU设备RS485响应不稳定/不完整的问题求助
大家好,我最近在做和I-7000 IO模块的Modbus RTU通信,用的是RS485接口,pyserial库。目前写线圈、发起读线圈命令这些操作都能成功,但读取设备返回的响应时遇到了大问题:响应总是不完整或者不稳定——大多数时候只能读到末尾的CRC字节,偶尔能读到部分内容,极少能拿到预期的完整6字节响应(包含设备ID、功能码、线圈地址、值和CRC)。
我先贴一段简化的调试代码,和主代码遇到的问题完全一致:
import serial import time import crcmod.predefined # 计算Modbus RTU CRC的函数 def calculate_rs_485_crc(message): crc16 = crcmod.predefined.mkCrcFun("modbus") return crc16(message) port = "/dev/ttyS0" baudrate = 115200 timeout = 1 ser = serial.Serial( port, baudrate, timeout=timeout, parity=serial.PARITY_NONE, ) # 构造Modbus RTU命令 message = "020100060001" command = bytes.fromhex(message) crc = calculate_rs_485_crc(command) command_with_crc = command + crc.to_bytes(2, "little") print(f"Sent: {command_with_crc.hex()}") ser.write(command_with_crc) byteData = ser.read_until(b"\xcc") print(f"byteData: {byteData}") ser.close()
波特率和端口我都确认过是对的,不管设置多少超时时间,结果都一样。试过在写和读之间加time.sleep(0.5),结果更糟——完全收不到任何数据;不加sleep至少还能拿到部分内容。
终端输出的例子:
- 大多数时候只收到CRC相关字节:
Sent: 0201000600011df8 byteData: b'Q\xcc' - 偶尔能拿到完整预期响应,或者中间状态:
Sent: 0201000600011df8 byteData: b'\x02\x01\x01\x00Q\xcc'
我试过read_until、readline、指定字节数的read(6),结果都一样。网上查了很多资料,大多说是时序或者硬件问题,但硬件肯定没问题——厂商提供的工具能和设备正常通信。如果是时序问题,我加sleep等响应完全到达反而收不到,这就很矛盾了。
EDIT1:补充mbpoll测试结果
我用mbpoll工具测试了一下,居然超时了:
user@localhost:~$ sudo mbpoll -m rtu -b 115200 -d 8 -s 1 -p none /dev/ttyS0 -a 2 -r 1 -1 -t 0 mbpoll 1.0-0 - FieldTalk(tm) Modbus(R) Master Simulator Copyright © 2015-2019 Pascal JEAN, https://github.com/epsilonrt/mbpoll This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions; type 'mbpoll -w' for details. Protocol configuration: Modbus RTU Slave configuration...: address = [2] start reference = 1, count = 1 Communication.........: /dev/ttyS0, 115200-8E1 t/o 1.00 s, poll rate 1000 ms Data type.............: discrete output (coil) -- Polling slave 2... Read discrete output (coil) failed: Connection timed out
EDIT2:发现一个奇怪的现象
我有个封装好的RS485发送函数,功能和主代码差不多,只是在函数里创建串口实例、写命令、读响应然后关闭。我在主代码里先调用这个函数,然后再执行原来的读写逻辑,居然就能拿到完整响应了!但这个方法其实是创建了两个串口实例,感觉不是合理的解决方案。
封装的函数代码:
import serial import time import logging import os import crcmod.predefined logger = logging.getLogger(__name__) def send_over_rs485(message: str, max_attempts=3): attempts = 0 while attempts < max_attempts: try: ser = serial.Serial("/dev/ttyS0", 115200, timeout=0.3) command = bytes.fromhex(message) crc = calculate_rs_485_crc(command) command_with_crc = command + crc.to_bytes(2, "little") ser.write(command_with_crc) time.sleep(0.2) if ser.in_waiting: response = ser.readline() return response.hex() else: logging.debug("No response available rs-485 in serial buffer.") except serial.SerialException as e: logging.error(f"Serial communication error: {e}") except OSError as e: logging.error(f"OS-level communication error: {e}") except Exception as e: logging.error(f"Unexpected error: {e}") finally: if "ser" in locals() and ser.is_open: ser.close() attempts += 1 logging.warning(f"Reached maximum attempts without success: {attempts}") return None # 计算Modbus RTU CRC的函数 def calculate_rs_485_crc(message): crc16 = crcmod.predefined.mkCrcFun("modbus") return crc16(message)
调用后的主代码:
import serial import time import crcmod.predefined # 导入上面的函数 from serial_protocol import send_over_rs485 # 计算Modbus RTU CRC的函数 def calculate_rs_485_crc(message): crc16 = crcmod.predefined.mkCrcFun("modbus") return crc16(message) port = "/dev/ttyS0" baudrate = 115200 timeout = 1 ser = serial.Serial( port, baudrate, timeout=timeout, parity=serial.PARITY_NONE, ) # 构造Modbus RTU命令 message = "020100060001" # 先调用这个函数 send_over_rs485(message) command = bytes.fromhex(message) crc = calculate_rs_485_crc(command) command_with_crc = command + crc.to_bytes(2, "little") print(f"Sent: {command_with_crc.hex()}") ser.write(command_with_crc) byteData = ser.read_until(b"\xcc") print(f"byteData: {byteData}") ser.close()
执行后的响应(正常了):
Sent: 0201000600011df8 in waiting false 0 byteData: b'\x02\x01\x01\x00Q\xcc'
EDIT3:进一步定位问题
后来我发现不是那个函数的问题,而是第一次打开串口写读会失败,关闭后重新打开串口再写读,第二次就能正常拿到完整响应。我写了这段测试代码验证:
import serial import time import crcmod.predefined # 计算Modbus RTU CRC的函数 def calculate_rs_485_crc(message): crc16 = crcmod.predefined.mkCrcFun("modbus") return crc16(message) def setup_serial_port() -> serial.Serial: port = "/dev/ttyS0" baudrate = 115200 timeout = 0.3 ser = serial.Serial( port=port, baudrate=baudrate , bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=timeout ) return ser # 构造Modbus RTU命令 message = "020100060001" command = bytes.fromhex(message) crc = calculate_rs_485_crc(command) command_with_crc = command + crc.to_bytes(2, "little") # 第一次打开串口读写 ser = setup_serial_port() print(f"Sent: {command_with_crc}") ser.write(command_with_crc) time.sleep(0.2) byteData = ser.read(6) print(f"byteData: {byteData}") ser.close() # 第二次打开串口读写 ser = setup_serial_port() print(f"Sent: {command_with_crc}") ser.write(command_with_crc) time.sleep(0.5) # 短延迟确保响应已到达 if ser.in_waiting: print(f"in waiting {ser.in_waiting}") else: print(f"in waiting false {ser.in_waiting}") byteData = ser.read(6) print(f"byteData: {byteData}") ser.close()
现在我完全搞不懂为什么会这样,第一次打开串口总是读不到完整响应,第二次就正常了?希望有大佬能帮我分析下原因,或者给出正确的解决方法,谢谢!
备注:内容来源于stack exchange,提问作者Angel Hadzhiev

