
1. 背景Modbus RTU 走串口半双工一问一答。正常情况下主机轮询从站从站回复物理层上不会有第三台设备抢话。但如果使用 Modbus TCP 转 RS485 网关网关内部是网口转串口就不同了局域网里任何广播帧ARP、DHCP、NetBIOS都会被网关接收然后原封不动地往串口里塞。这时候就不在是串口点对点了实际是贡献总线只要网关配置了“透明传输”模式广播帧就成了不断被重复发送的废话包占满了 9600 波特率的带宽。从站收到一堆无法解析的垃圾字节忙着处理异常帧导致没时间响应主机轮询遇到这种该问题如何处理呢2.如何查找并处理问题2.1. 先定位是不是纯物理层故障用串口助手挂到网关的 RS485 输出端直线上有没有持续不断的无意义数据流,代码如下// 用 QSerialPort 写个快速监听工具不主动发包只收数据 QSerialPort port; port.setPortName(COM3); port.setBaudRate(9600); port.setDataBits(QSerialPort::Data8); port.setParity(QSerialPort::NoParity); port.setStopBits(QSerialPort::OneStop); port.open(QIODevice::ReadOnly); // 300ms 内收到的字节数 200 且无法组成有效 Modbus 帧基本就是广播风暴 QByteArray buffer port.readAll(); qDebug() Bytes received in one poll: buffer.size(); // 正常工况下应该是接近 0只有主机轮询时才有一帧 8 字节2.2. 抓包确认广播源头确认线上有垃圾数据后切到网口侧抓包。用 Wireshark 过滤 other broadcast查看。临时解决方案把网关的“广播帧过滤”选项打开或者直接切换到 Modbus TCP 模式让上位机走网口直连 PLC。如果没有备用网关只能先在交换机上把工控机隔离到独立 VLAN不让广播帧进到现场设备网段。// 如果网关支持命令配置可用 QProcess 调用厂家工具远程修改 QProcess p; p.start(netconfig, {-i, eth0, -f, broadcast_filteron}); p.waitForFinished(); qDebug() p.readAllStandardOutput();2.3. 从站崩溃后如何恢复即使广播断开了从站已经被垃圾帧打挂处于 Busy 状态。必须等它内部看门狗超时一般 3~5 秒否则永远不回包。此时上位机侧要加“快速重连 自动恢复”机制// 在 QModbusClient 的响应超时回调里做自动重试 void OnTimeout(QModbusReply* reply) { static int retryCount 0; if (retryCount 3) { retryCount; QTimer::singleShot(500, this, [this]() { // 先发一个 03 功能码的广播识别帧让从站复位收包状态 SendBroadcastReset(); RequestHoldingRegisters(); }); } else { // 3 次不成功彻底置故障标志不再占用总线 SetFaultFlag(true); retryCount 0; } }这里有个关键操作SendBroadcastReset是往地址 0 发一帧 06 写命令Modbus 协议里地址 0 代表所有从站都接收但不回复。这一帧能清掉从站接收缓冲区的残留垃圾状态让它恢复正常监听。但千万别在总线上有多台从站时发——有的设备广播复位会导致内部寄存器被清零必须和厂家确认。3. 治本方案上位机侧加通信链路看门狗加三层看门狗物理层硬件手拉手保底重启、协议层超时重试带退避、应用层心跳检测 故障分段隔离。下面是针对 Modbus 轮询任务的核心逻辑// 轮询任务里不再是一个 while 不断发请求而是用状态机控制 class ModbusPollTask : public QObject { Q_OBJECT enum State { IDLE, POLLING, RETRY, RECOVER }; State m_state IDLE; int m_pollInterval 100; // ms int m_timeout 300; // ms void OnTimer() { switch (m_state) { case IDLE: if (m_faultCount 0) m_state POLLING; break; case POLLING: SendReadRequest(m_deviceAddr); m_state RETRY; break; case RETRY: if (m_replyReceived) { m_faultCount 0; m_state IDLE; } else if (m_retries 3) { // 这里不要继续发防止加重从站负担 m_state RECOVER; m_retries 0; } break; case RECOVER: SendResetFrame(m_deviceAddr); m_state POLLING; // 试探恢复 break; } } };日志记录很关键不要在崩溃后才去查把每一次超时、重试、恢复动作都带时间戳写入环形缓存void WriteLog(const QString msg) { QFile f(通信日志.txt); f.open(QIODevice::Append); f.write(qPrintable(QDateTime::currentDateTime().toString(yyyy-MM-dd hh:mm:ss.zzz ) msg \n)); // 循环滚动只保留最近 10000 行避免磁盘写满 }