
rs-485 半双工通信中,modbus rtu 响应读取失败的根本原因常是 rts(request to send)信号未及时切换至接收模式,导致从机返回数据时主控仍处于发送态,从而丢失或截断响应。本文详解问题成因与可靠解决方案。
rs-485 半双工通信中,modbus rtu 响应读取失败的根本原因常是 rts(request to send)信号未及时切换至接收模式,导致从机返回数据时主控仍处于发送态,从而丢失或截断响应。本文详解问题成因与可靠解决方案。
在使用 pyserial 与支持 Modbus RTU 的 RS-485 设备(如 I-7000 模块)通信时,你可能遇到“能发不能稳收”的典型现象:偶尔读到完整 6 字节响应(含设备地址、功能码、数据和 CRC),更多时候只收到末尾的 CRC 字节(如 b'Q\xcc')或部分字节。正如你在多次调试中发现的——反复关闭/重开串口后第二次通信成功率显著提升——这并非巧合,而是暴露了 RS-485 硬件层的关键控制缺失。
? 根本原因:RTS 电平未动态管理
RS-485 是半双工总线,依赖方向控制信号(通常为 RTS 引脚)切换收发状态:
-
发送时:RTS =
True→ 驱动器使能(TX 模式),总线被占用; -
接收时:RTS =
False→ 驱动器禁用(RX 模式),收发器监听总线。
若未显式控制 RTS,默认行为取决于硬件/驱动:许多 USB-RS485 转换器或 SoC UART(如树莓派的 /dev/ttyS0)在串口打开时将 RTS 置高并保持,导致发送完成后仍锁定在 TX 模式——此时从机发出的响应直接被主控忽略,造成数据截断。
你观察到“关闭再打开串口后第二次成功”,正是因为 ser.close() 通常会将 RTS 拉低(进入默认 RX 态),而 ser.open() 初始化时可能触发一次 RTS 复位,形成“隐式切换”。但这不可靠,且效率低下。
✅ 正确解法:显式 RTS 控制 + 合理时序
以下为生产级推荐方案,无需重启串口,一次连接即可稳定通信:
import serial
import time
import crcmod.predefined
def calculate_modbus_crc(data: bytes) -> int:
crc16 = crcmod.predefined.mkCrcFun("modbus")
return crc16(data)
# 配置串口(关键:启用 RTS 控制)
ser = serial.Serial(
port="/dev/ttyS0",
baudrate=115200,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0.5, # 响应超时,非发送等待
rtscts=False, # 不使用硬件流控
dsrdtr=False,
)
try:
# 构造 Modbus RTU 请求:读线圈 (0x01),地址 0x0006,数量 1
message = bytes.fromhex("020100060001")
crc = calculate_modbus_crc(message)
command = message + crc.to_bytes(2, "little")
print(f"Sent: {command.hex()}")
# Step 1: 拉高 RTS 进入发送模式
ser.rts = True
time.sleep(0.001) # 确保电平稳定(可选,部分芯片需)
# Step 2: 发送命令
ser.write(command)
# Step 3: 立即拉低 RTS 切换至接收模式
ser.rts = False
time.sleep(0.001) # 确保切换完成
# Step 4: 等待响应(Modbus RTU 帧间最小间隔 3.5T)
# 对于 115200bps,1 字节 ≈ 87μs,3.5 字节 ≈ 305μs → 建议至少 1ms
time.sleep(0.002)
# Step 5: 读取预期长度(6 字节:addr+func+bytecnt+data+CRC_L+CRC_H)
response = ser.read(6)
print(f"Received: {response.hex() if response else 'None'}")
# 可选:验证 CRC(前 len-2 字节计算 CRC 应等于后 2 字节)
if len(response) == 6:
expected_crc = calculate_modbus_crc(response[:-2])
actual_crc = int.from_bytes(response[-2:], "little")
if expected_crc == actual_crc:
print("✅ CRC verified.")
else:
print("❌ CRC mismatch!")
finally:
ser.close()
⚠️ 关键注意事项
-
不要依赖
timeout参数等待响应:timeout是单字节读取超时,而非整帧等待。RS-485 响应有严格时序要求(帧间最小 3.5 字符时间),必须用time.sleep()主动延时确保接收窗口开启。 -
避免
read_until()或readline():Modbus RTU 无终止符,b"\xcc"只是 CRC 低位,不具唯一性;且响应中可能含0xCC数据,导致提前截断。 -
RTS 切换时机至关重要:必须在
write()后立即设ser.rts = False,延迟过长会导致响应丢失;切换过早(如 write 前)则发送失败。 - 硬件兼容性:确认你的 RS-485 转换器支持 RTS 方向控制(绝大多数工业级模块支持)。若使用 GPIO 模拟 RTS,请确保电平匹配(通常为 TTL 电平)。
-
Linux 系统注意:某些内核驱动(如
sc16is7xx)可能需要额外配置 RTS 行为,可通过stty -F /dev/ttyS0 crtscts检查是否启用了 RTS/CTS 流控(此处应禁用)。
? 总结
RS-485 Modbus RTU 通信不稳定,90% 源于方向控制失当。显式管理 RTS 电平 + 精确的发送/接收时序切换,是解决“只能读到 CRC”或“响应不全”问题的唯一可靠路径。摒弃“重启串口碰运气”的临时方案,采用上述结构化流程,即可实现单次连接、稳定全帧读取,大幅提升工业现场通信鲁棒性。










