Modbus RTU/TCP 故障处理案例
案例一:RS-485 通信间歇性失败
故障现象
储能系统中,BMS 通过 RS-485 连接到网关,通信成功率约 85%,每隔几分钟出现一次超时错误,重试后恢复正常。
排查过程
bash
# 1. 使用示波器或逻辑分析仪抓取 RS-485 信号
# 发现信号边沿不干净,有振铃现象
# 2. 检查终端电阻
# 用万用表测量总线两端电阻:
# A-B 之间:测量到 ~240Ω(正常应为 60Ω,即两个 120Ω 并联)
# 实际只有一端接了终端电阻!
# 3. 检查总线长度
# 实测总线长度:450m,波特率:19200bps
# 查表:19200bps 最大推荐距离 300m → 超出范围!
# 4. 检查接地
# 发现多个从站的屏蔽层都接了地 → 形成地环路,引入干扰解决方案
1. 在总线另一端补接 120Ω 终端电阻
2. 将波特率降低到 9600bps(支持 1200m)
3. 屏蔽层改为单端接地(仅主站侧)
4. 在干扰严重的区段更换为铠装屏蔽双绞线
修改后通信成功率提升至 99.9%案例二:Modbus TCP 连接数耗尽
故障现象
SCADA 系统连接多台 PLC(每台 Modbus TCP),运行 2-3 天后,部分 PLC 无法建立新连接,重启 PLC 后恢复。
排查过程
bash
# 在 PLC 侧(Linux 工控机)查看连接状态
netstat -an | grep :502 | wc -l
# 输出:128 ← 已达到 PLC 最大连接数限制
netstat -an | grep :502 | grep TIME_WAIT | wc -l
# 输出:115 ← 大量 TIME_WAIT 状态连接!
# 查看 SCADA 侧代码
# 发现每次读取都新建连接,读完后调用 close()
# 但 TCP TIME_WAIT 状态持续 2*MSL(约 60-120 秒)
# 高频轮询导致 TIME_WAIT 连接堆积解决方案
python
# 错误做法:每次读取都新建连接
def bad_read(host, address):
client = ModbusTcpClient(host)
client.connect()
result = client.read_holding_registers(address, 10)
client.close() # 产生 TIME_WAIT
return result
# 正确做法1:长连接复用
class PersistentModbusClient:
def __init__(self, host, port=502):
self.client = ModbusTcpClient(host, port=port, timeout=3)
self._connect()
def _connect(self):
retry = 0
while retry < 3:
if self.client.connect():
return
retry += 1
time.sleep(1)
raise ConnectionError(f"Cannot connect to Modbus device")
def read(self, address, count, unit_id=1):
if not self.client.is_socket_open():
self._connect()
return self.client.read_holding_registers(address, count, slave=unit_id)
# 正确做法2:调整 TCP 参数减少 TIME_WAIT
# Linux 系统级配置
import subprocess
subprocess.run(['sysctl', '-w', 'net.ipv4.tcp_tw_reuse=1'])
subprocess.run(['sysctl', '-w', 'net.ipv4.tcp_fin_timeout=15'])案例三:浮点数读取值异常
故障现象
读取变频器的输出频率寄存器,得到的值完全错误(如应为 50.0Hz,读到 -1.08e+38)。
排查过程
python
# 读取两个寄存器
result = client.read_holding_registers(100, 2, slave=1)
print(result.registers) # [0x4248, 0x0000]
# 尝试大端序解析
import struct
raw_be = struct.pack('>HH', 0x4248, 0x0000)
value_be = struct.unpack('>f', raw_be)[0]
print(f"Big-endian: {value_be}") # 50.0 ✓
# 但实际读到的是 [0x0000, 0x4248](字节序相反)
raw_wrong = struct.pack('>HH', 0x0000, 0x4248)
value_wrong = struct.unpack('>f', raw_wrong)[0]
print(f"Wrong order: {value_wrong}") # -1.08e+38 ✗
# 结论:该变频器使用小端字序(Low Word First)
raw_le = struct.pack('>HH', 0x0000, 0x4248) # 低字在前
# 需要交换两个寄存器再解析
raw_fixed = struct.pack('>HH', 0x4248, 0x0000)
value_fixed = struct.unpack('>f', raw_fixed)[0]
print(f"Fixed: {value_fixed}") # 50.0 ✓解决方案
python
class FlexibleDecoder:
"""支持多种字节序的 Modbus 数据解码器"""
@staticmethod
def decode_float32(registers, word_order='big'):
"""
word_order: 'big' = 高字在前(ABCD)
'little' = 低字在前(CDAB)
'big_swap' = 字节交换大端(BADC)
'little_swap' = 字节交换小端(DCBA)
"""
if word_order == 'big':
raw = struct.pack('>HH', registers[0], registers[1])
elif word_order == 'little':
raw = struct.pack('>HH', registers[1], registers[0])
elif word_order == 'big_swap':
raw = struct.pack('<HH', registers[0], registers[1])
elif word_order == 'little_swap':
raw = struct.pack('<HH', registers[1], registers[0])
else:
raise ValueError(f"Unknown word_order: {word_order}")
return struct.unpack('>f', raw)[0]
# 不同厂商的字节序习惯:
# Schneider/Modicon:大端(ABCD)
# Siemens S7:大端(ABCD)
# ABB 变频器:小端(CDAB)
# 部分中国品牌:需查手册确认案例四:多主站冲突
故障现象
同一 RS-485 总线上,SCADA 和本地 HMI 同时轮询从站,导致大量 CRC 错误和超时。
根本原因
RS-485 是半双工总线,同一时刻只能有一个主站发送。两个主站同时发送导致总线冲突。
解决方案
方案1:硬件隔离(推荐)
将 HMI 连接到独立的 RS-485 端口(从站支持多端口时)
方案2:Modbus TCP 网关
RS-485 总线 → Modbus RTU/TCP 网关 → 以太网
SCADA 和 HMI 都通过以太网访问网关,网关串行化请求
方案3:时分复用(软件协调)
SCADA 和 HMI 通过共享锁协调访问时间窗口python
# 方案2:使用 Modbus RTU/TCP 网关(如 Moxa NPort)
# 网关将 RS-485 总线暴露为 Modbus TCP 服务
# 内部串行化所有 TCP 请求,避免总线冲突
# 方案3:分布式锁(Redis)
import redis
import time
class ModbusBusLock:
def __init__(self, redis_client, bus_id: str, timeout: float = 5.0):
self.redis = redis_client
self.lock_key = f"modbus_bus_lock:{bus_id}"
self.timeout = timeout
def __enter__(self):
deadline = time.time() + self.timeout
while time.time() < deadline:
# SET NX PX:原子加锁
if self.redis.set(self.lock_key, "1",
nx=True, px=int(self.timeout * 1000)):
return self
time.sleep(0.01)
raise TimeoutError(f"Cannot acquire Modbus bus lock for {self.lock_key}")
def __exit__(self, *args):
self.redis.delete(self.lock_key)
# 使用
redis_client = redis.Redis()
with ModbusBusLock(redis_client, "rs485-line1"):
result = client.read_holding_registers(0, 10, slave=1)常见错误码
| 异常码 | 名称 | 含义 | 处理方法 |
|---|---|---|---|
| 0x01 | Illegal Function | 从站不支持该功能码 | 检查功能码是否正确 |
| 0x02 | Illegal Data Address | 寄存器地址不存在 | 检查地址映射表 |
| 0x03 | Illegal Data Value | 数据值超出范围 | 检查写入值范围 |
| 0x04 | Slave Device Failure | 从站内部错误 | 检查从站设备状态 |
| 0x05 | Acknowledge | 从站正在处理,稍后重试 | 等待后重试 |
| 0x06 | Slave Device Busy | 从站忙 | 降低轮询频率 |
| 0x0A | Gateway Path Unavailable | 网关路径不可用 | 检查网关配置 |
| 0x0B | Gateway Target Device Failed | 目标设备无响应 | 检查设备连接 |
诊断工具使用
bash
# 使用 modpoll 命令行工具测试
# 读取从站 1 的保持寄存器 40001-40010
modpoll -m tcp -a 1 -r 1 -c 10 192.168.1.100
# RTU 模式
modpoll -m rtu -b 9600 -d 8 -s 1 -p none -a 1 -r 1 -c 10 /dev/ttyS1
# 写入单个寄存器
modpoll -m tcp -a 1 -r 100 -1 -t 4 192.168.1.100 1234
# Wireshark 过滤 Modbus TCP
# 过滤器:modbus
# 查看 Function Code、Exception Code、数据内容