02 · 常见问题排查 FAQ(附录)
本篇是「工业设备数据采集」知识体系第 07 篇(附录)的第 2 篇。
定位:现场出问题时翻这一篇。每条按「现象 → 原因 → 排查步骤 → 处置」组织,给出可直接复制的命令和代码。
共 30 条,按五大类分组。凡涉及具体数值需要现场确认的,标注 (需实测确认)。
本篇目录
| 分类 | 条目 | 条数 |
|---|---|---|
| A. 连接类 | FAQ-01 ~ FAQ-09 | 9 |
| B. 数据类 | FAQ-10 ~ FAQ-16 | 7 |
| C. 通信质量类 | FAQ-17 ~ FAQ-22 | 6 |
| D. RTU 串口类 | FAQ-23 ~ FAQ-27 | 5 |
| E. 工程类 | FAQ-28 ~ FAQ-30+ | 3+ |
A. 连接类
FAQ-01 Connection refused(连接被拒绝)
现象:java.net.ConnectException: Connection refused: connect
原因:目标 IP 上没有进程监听该端口。可能是:IP 写错、PLC 未上电、网口没插、通信模块未启用、端口不匹配。
排查步骤:
# 1. 先确认 IP 可达
ping 192.168.1.110
# 2. 确认端口在监听
# Linux
ss -lntp | grep 502
# Windows
netstat -ano | findstr :502
# 3. 直接测端口(最直观)
telnet 192.168.1.110 502
# 或 Windows PowerShell
Test-NetConnection 192.168.1.110 -Port 502
# 4. 确认本机网段
ip addr # Linux
ipconfig # Windows
处置清单:
□ IP 地址与子网掩码是否在同一网段(192.168.1.x / 255.255.255.0)
□ PLC 是否上电、网口灯是否亮
□ 通信模块/以太网口是否在硬件配置里启用并已下载
□ 端口号:Modbus 502 / S7 102 / MC 6000(以 GX Works 配置为准)
□ 如果是虚拟机/容器,网卡模式是否为桥接
FAQ-02 能 ping 通,但 502 / 102 端口不通
现象:ping 通,但业务连不上;telnet 超时(注意:是超时,不是 refused)。
原因:防火墙拦截(与 FAQ-01 的关键区别)。refused = 端口没开;超时 = 被防火墙丢包。
排查步骤:
# 1. 确认是 refused 还是 timeout
telnet 192.168.1.110 502
# "Connection refused" → FAQ-01
# 一直等待/超时 → 防火墙(本条)
# 2. 本机防火墙
# Windows
netsh advfirewall firewall show rule name=all | findstr 502
# Linux
iptables -L -n | grep 502
# 3. 抓包看有没有 SYN 出去、有没有 SYN-ACK 回来
tcpdump -i any host 192.168.1.110 and port 502 -nn
处置:
□ Windows:高级防火墙 → 入站规则放通 502/102/6000
□ 工业现场常见:三层交换机 ACL、网关白名单
□ PLC 侧"访问保护":S7-1200/1500 需设置允许 PUT/GET
□ 三菱:以太网模块"RUN/STOP 时允许写入"、IP 过滤
FAQ-03 S7 连接失败(TSAP / rack-slot 不对)
现象:TCP 能连上,但握手无响应;或抓包看到 COTP 回复 0x50(拒绝)。
原因:远端 TSAP 与目标 CPU 不符。
远端 TSAP = ConnectionType << 8 | (rack << 5 | slot)
S7-1200 / 1500 → 通常 (0, 0) 或 (0, 1),且这两个机型通常忽略 slot 位
S7-300 → (0, 2) CPU 在第 2 槽
S7-400 → (0, 3) CPU 在第 3 槽
S7-200 → "MW" (0x4D57)
S7-200Smart → 特殊值
两库默认 ConnectionType 不同:
HSL = 0x01(PG)
iot = 0x03(S7 单边)
排查步骤:
1. TIA Portal → "在线和诊断" 或 硬件配置 → 确认 CPU 的机架号/槽号
2. 依次换 ConnectionType 试:0x0100 → 0x0200 → 0x0300
3. 抓包看 CC(Connect Confirm):
回 0xD0 → 通过
回 0x50 → 远端 TSAP 被拒
无响应 → TPKT 长度错误或 TSAP 非法
FAQ-04 S7 能连不能读(PUT/GET 未开)
现象:COTP + Setup 握手都成功,但读请求返回错误或直接超时。1200/1500 上 99% 是这个原因。
原因:TIA Portal 未勾选"允许来自远程对象的 PUT/GET 通信访问"。
处置:
1. TIA Portal → CPU 属性 → 防护与安全 → 连接机制
2. 勾选「允许从远程伙伴(PLC、HMI、OPC、...)使用 PUT/GET 通信访问」
3. 重新下载硬件配置到 PLC
iot 仓库里贴心地放了截图:
notice/博图-允许PUT-GET.png。
FAQ-05 读 DB 返回 0x0A(对象不存在)
现象:S7 响应 Data 区 Return Code = 0x0A。
原因(三选一):
□ DB 号写错
□ DB 没有下载到 PLC
□ DB 仍然是「优化的块访问」(optimized block access)
处置:
1. TIA Portal → DB 块 → 属性 → 取消勾选「优化的块访问」
2. 重新下载 DB
3. 确认地址写法(HSL: DB1.0 表示 DB1 字节 0)
截图见
notice/博图-不要勾选DB块优化访问.png。 ⚠️ 关掉优化访问后,DB 内变量的偏移才是你在协议里用的地址;优化访问下偏移由系统管理,外部无法按地址访问。
FAQ-06 MC 连接后无响应
现象:TCP 连上,发帧后石沉大海,或返回 0xC059。
原因:帧类型 / 系列不匹配。
| 检查项 | 说明 |
|---|---|
系列 EMcSeries |
A(1E 帧)/ QnA / Q_L / IQ_R |
| 子命令 | Q/L 用 0x0000(字)、0x0001(位);iQ-R 用 0x0002 / 0x0003 |
| 端口 | iot 默认 6000;现场以 GX Works 配置为准(需实测确认) |
| CPU 监视定时器 | 太短 → 0xC05E |
排查步骤:
# 1. 确认端口
telnet 192.168.1.120 6000
# 2. 抓包确认帧头
tcpdump -i any host 192.168.1.120 -nn -A | head -50
# 二进制 3E 帧头应为:50 00 (副头,小端 → 0x0050)
# ASCII 帧头应以 "5000" 开头
# 3. 用 iot 的 comCallback 打印原始报文
mcPLC.setComCallback((tag, bytes) ->
System.out.printf("%s[%d] %s%n", tag, bytes.length, HexUtil.toHexString(bytes)));
FAQ-07 MC 报错 0xC059(命令/子命令不正确)
现象:一上电/一连上就返回 0xC059。
原因:iQ-R 用了 Q/L 的子命令。
❌ 错误:new McPLC(EMcSeries.IQ_R, ...) 但子命令用了 0x0000
✅ 正确:iQ-R 字单位 → 0x0002;位单位 → 0x0003
源码逻辑:
// iot(McReadDeviceBatchInWordReqData.java:53-54)
this.subcommand = series != EMcSeries.IQ_R ? 0x0000 : 0x0002;
// HSL(MelsecMcRNet.java:286)
command[2] = isBit ? (byte) 0x03 : (byte) 0x02;
FAQ-08 连接数满(S7-1200 只有 8 个连接)
现象:部分设备连不上,或运行一段时间后新设备连不上;PLC 诊断报"连接资源不足"。
原因:S7-1200/1500 的连接资源有限(1200 约 8 个,含 PG/HMI/Web),被采集器占满。
排查步骤:
# 数一下采集器到该 PLC 的 ESTABLISHED 连接数
ss -ant | grep 192.168.1.110 | grep ESTAB | wc -l
# 看是不是短连接高频建连(TIME_WAIT 也占资源)
ss -ant | grep 192.168.1.110 | awk '{print $1}' | sort | uniq -c
处置:
□ 单 IP 连接上限设为 2~3(见 06 篇 02 章 §3.4)
□ HSL 必须 SetPersistentConnection(),避免短连接反复建连
□ 多个逻辑设备共享一条 Channel(配置模型的价值所在)
□ 检查是否有"僵尸连接":代码路径没 close()
□ 必要时换支持更多连接的 CP 模块 / 上 1500
FAQ-09 MC CPU 监视定时器超时(0xC05E)
现象:偶发返回 0xC05E;PLC 忙时更频繁。
原因:请求设置的监视定时器值(单位 250ms)小于 PLC 实际处理时间。
值 × 250ms = 超时时间
0x000A (10) → 2.5s
0x0014 (20) → 5s
0x0028 (40) → 10s
0x00F0 (240) → 60s
推荐配置:
| 场景 | 建议值 | 实际超时 |
|---|---|---|
| 直连 CPU | 0x000A ~ 0x0028 |
2.5 ~ 10s |
| 经网络模块/多层网络 | 0x0014 ~ 0x00F0 |
5 ~ 60s |
❗ 同时必须在驱动层再套一个 socket 超时(SO_TIMEOUT),否则 PLC 不响应时线程会永久阻塞。
B. 数据类
FAQ-10 读到的整数值不对(字节序错)
现象:读出 100 但实际是 25600;或值随量程放大 256 倍。
原因:16/32 位整数的字节顺序错。
S7 → 大端(ABCD),HSL 用 ReverseBytesTransform
Modbus → 寄存器内大端;32 位数据的"字序"因厂商而异
排查步骤:
// 打印原始字节,人工比对
System.out.println(HexUtil.toHexString(raw)); // iot
// 或 HSL:r.Content
// 例:期望 100 (0x0064)
// 收到 64 00 → 小端,应改为大端解析
// 收到 00 64 → 正确
处置:修改配置项 byteOrder(ABCD / BADC / CDAB / DCBA),见 07 篇 01 章 §12。
FAQ-11 float 读出乱码
现象:float 值显示为 -1.2e+38、0.0、NaN,或数量级完全不对。
原因:四个字节的排列顺序错了。这是现场最常见的单个问题。
IEEE754:12.5f = 0x41480000
线上字节顺序 解析结果
─────────────────────────────
41 48 00 00 → 12.5 ✅ ABCD(大端)
00 00 48 41 → 12.5 ✅ DCBA(小端反转)
00 00 41 48 → 荒谬值 ❌ CDAB(字序反了)
48 41 00 00 → 荒谬值 ❌ BADC
排查代码:
byte[] raw = mcPLC.readBytes("D100", 2);
System.out.println(HexUtil.toHexString(raw)); // 看 4 个字节
int bits = ByteReadBuff.newInstance(raw).getInt32(); // 按默认序试
System.out.println(Float.intBitsToFloat(bits));
// 若不对,手工按 4 种顺序重组后再 intBitsToFloat
处置口诀:
浮点乱码 → 先换字序(ABCD↔CDAB 或 DCBA↔BADC)
→ 再换字节序(ABCD↔BADC 或 CDAB↔DCBA)
FAQ-12 bool 全 0 / 全 1(位压缩没解)
现象:读位(0x01/0x02、M 区、X/Y)时,值恒为 0 或恒为 true,与 PLC 实际状态不符。
原因:Modbus 与 MC 的位数据是按位压缩打包的,不是每 bit 一个字节。
8 个位 = 1 个字节,bit0 对应第一个点
byte = 0b0000_0010 = 0x02
bit0 = 0
bit1 = 1 ← 第 2 个点为 true
bit2..7 = 0
排查步骤:
// 打印原始字节
byte[] raw = plc.readCoilAsBytes(0, 8); // 或 readBoolean 后看原始
System.out.println(HexUtil.toHexString(raw));
// 手工解位
for (int i = 0; i < count; i++) {
int byteIdx = i / 8;
int bitIdx = i % 8;
boolean v = ((raw[byteIdx] >> bitIdx) & 0x01) != 0;
System.out.printf("bit[%d]=%b%n", i, v);
}
处置:
□ Modbus 位读取:字节数 = ⌈N/8⌉,不是 N
□ MC 位读取:位压缩编码,每点 4 bit(见 03 篇 MC 例 2)
□ S7 位读取:Return Code 0x03 表示位数据,长度按位、不除 8
□ 地址是否忘了 ×8(S7)
FAQ-13 地址偏移错(1 基 / 0 基混淆)
现象:读 40001 拿到的是 40002 的值;或总是差一个寄存器。
原因:协议里的地址是从 0 开始的,而点表习惯从 1 开始。
点表 40001 → 协议地址 0
点表 40100 → 协议地址 99
两库处理:
| 库 | 机制 |
|---|---|
| HSL | AddressStartWithZero 开关(ModbusTcpNet.java:75)+ ModbusInfo.java:117-122 换算 |
| iot | 直接传数字地址(readHoldRegister(0, 4)),不做换算 |
排查代码:
// 验证:读 40001 一个字,与 PLC 上 40001 显示值比对
byte[] v = plc.readHoldRegister(0, 1);
// 若差一位,说明 0 基/1 基判断反了
处置:统一约定——点表里写 1 基(40001),驱动层统一减 1。不要两处都减或都不减。
FAQ-14 偶发数据错位(粘包 / 并发读写)
现象:大部分时间正确,偶尔出现 A 点的值是 B 点的;或某个点的值突然跳到另一个量级。
原因(两类):
① 粘包处理不完整
→ 第二阶段没读满,剩余字节留到下一帧被误读
② 并发访问同一个连接
→ 两个线程同时从同一 InputStream 做两阶段读,抢字节
排查步骤:
// ① 确认所有读写都经过同一把锁
// (见 06 篇 02 章 ModbusTcpDriver.roundTripLock)
// ② 打开原始报文日志,看错位时的字节流
plc.setComCallback((tag, bytes) ->
System.out.printf("%s[%d] %s%n", tag, bytes.length, HexUtil.toHexString(bytes)));
// ③ 检查事务 ID(Modbus TCP)是否连续且匹配
处置:
□ 所有 read/write 必须走 roundTripLock(本设计已在抽象基类强制)
□ 第二阶段必须 readFully 读满,不能"读短即止"
□ Modbus TCP 校验事务 ID 与单元 ID(HSL 的 IsCheckMessageId / StationCheckMatch)
□ 写操作失败后,丢弃该连接并重建(残留字节无法安全复用)
FAQ-15 值跳变 / 上报刷屏(死区未设)
现象:平台上数据量巨大,模拟量每轮都上报,微小波动也产生一条记录。
原因:未配置死区(deadband),或死区设为 0。
处置:
- key: temperature
address: DB1.0
dataType: FLOAT32
periodMs: 1000
deadband: 0.5 # 变化小于 0.5 不上报
| 量类型 | 建议死区 |
|---|---|
| 温度(0.1 精度) | 0.2 ~ 0.5 |
| 压力 | 量程的 0.5% |
| 流量 | 量程的 1% |
| 开关量 | 0(变化才上报,本身就是最稀疏的) |
| 计数器 | 0(不允许丢) |
FAQ-16 32 位数据高低字反了
现象:INT32 / FLOAT32 读出的值"像是两个数拼反了",比如高 16 位和低 16 位互换。
原因:32 位数据跨两个寄存器,寄存器的先后顺序(字序)与字节序是两个独立维度。
4 个字节 A B C D:
字序(word order) :先发 AB 还是先发 CD
字节序(byte order):字内是 AB 还是 BA
组合出 4 种:
ABCD 大端 + 正常字序
BADC 大端 + 字内交换
CDAB 小端 + 字序交换 ← Modbus 最常见
DCBA 小端 + 正常字序
排查代码:
byte[] raw = plc.readHoldRegister(100, 2); // 读 2 个字组成 1 个 float
// 打印 4 种排列的解析结果,挑对的那个
for (ByteOrder o : ByteOrder.values()) {
byte[] r = o.rearrange(raw);
int bits = java.nio.ByteBuffer.wrap(r).getInt();
System.out.printf("%s -> %f%n", o, Float.intBitsToFloat(bits));
}
C. 通信质量类
FAQ-17 偶发超时(Nagle / 延迟 ACK)
现象:采集 100 次有几次超时,且延迟呈现规律的 ~40ms 或 ~200ms。
原因:Nagle 算法与 TCP 延迟 ACK 相互作用。
Nagle:有未确认的小包时,攒着不立刻发
延迟 ACK:收到数据不立刻 ACK,等 40ms 或有数据要发时捎带
→ 请求-响应模式下两者叠加,产生固定延迟
处置:
socket.setTcpNoDelay(true); // ❗ 工业采集必开
| 场景 | 推荐 |
|---|---|
| 请求-响应(Modbus/S7/MC) | 必须 true |
| 大块连续传输 | 可 false |
⚠️ 注意:HSL 源码里基本没设任何 Socket 选项(04 篇 §6.1 穷尽式 grep 的结论)。用 HSL 做高频采集时,这一点要特别留意。
FAQ-18 大量 CLOSE_WAIT
现象:ss -ant 看到大量 CLOSE-WAIT,数量持续增长不释放。
原因:对端已发 FIN(对端主动关闭),本端没有调用 close()。
正常四次挥手:
对端 FIN → 本端 CLOSE_WAIT → 本端 close() 发 FIN → LAST_ACK → 关闭
卡在 CLOSE_WAIT = 本端代码没关
排查步骤:
# 1. 看数量和 PID
ss -antp | grep CLOSE-WAIT
# 2. jstack 看该 PID 的线程在干什么
jstack <pid> | grep -A 10 "collect-worker"
# 3. 检查代码:是否所有异常路径都有 finally { close(); }
处置:
// ❌ 错误:只关 socket 不关流
socket.close();
// ✅ 正确:先关流再关 socket,且放在 finally
try {
// read / write
} finally {
try { if (out != null) out.close(); } catch (IOException ignored) { }
try { if (in != null) in.close(); } catch (IOException ignored) { }
try { if (socket != null) socket.close(); } catch (IOException ignored) { }
}
FAQ-19 TIME_WAIT 堆积
现象:ss -ant 看到大量 TIME-WAIT;端口耗尽后无法新建连接。
原因:本端主动关闭,进入 2MSL 等待(Linux 通常 60s)。短连接高频采集必然如此。
排查步骤:
# 统计
ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn
# 看是谁主动关的:TIME_WAIT 出现在主动关闭方
ss -antp | grep TIME-WAIT | head
处置(按优先级):
1. ✅ 改长连接 —— 这是根本解法
HSL: SetPersistentConnection()
iot: 默认就是长连接,别每轮 close()
2. 让服务端主动关闭(部分协议栈可配)
3. 内核参数(治标,谨慎)
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
4. ❌ 不要 tcp_tw_recycle(NAT 环境会丢连接,新内核已移除)
FAQ-20 RST 突然出现
现象:抓包看到 RST;或日志出现 Connection reset by peer / Connection reset。
原因:
| 原因 | 说明 |
|---|---|
SO_LINGER=0 |
setSoLinger(true, 0) → close() 直接发 RST,可能丢未发送数据 |
| 对端进程崩溃 | 操作系统代发 RST |
| 读写已关闭的 socket | 本端继续写 → 收到 RST |
| 对端接收缓冲区有未读数据就 close() | 会发 RST 而非 FIN |
| 连接被中间设备(防火墙)强制切断 | 常见于空闲超时 |
排查步骤:
# 抓包看 RST 出现在什么阶段
tcpdump -i any host <ip> -nn -c 200 -w /tmp/rst.pcap
# Wireshark 过滤:tcp.flags.reset == 1
处置:
□ 检查是否设置了 SO_LINGER=0 → 去掉
□ 关闭前先 shutdownOutput() 再读尽对端数据,最后 close()
□ 检查对端(PLC)是否因连接数超限而拒绝
□ 检查中间防火墙的空闲连接超时(常见 30min)
FAQ-21 断电 / 拔网线感知太慢
现象:PLC 掉电后,采集器要几十秒甚至几分钟才发现连接断了。
原因:TCP KeepAlive 默认 7200 秒(2 小时)才发第一个探测包。
# 查看当前值
sysctl net.ipv4.tcp_keepalive_time # 默认 7200
sysctl net.ipv4.tcp_keepalive_intvl # 默认 75
sysctl net.ipv4.tcp_keepalive_probes # 默认 9
处置(推荐:应用层心跳):
// 每 3s 心跳,连续 3 次失败判定 OFFLINE
scheduler.scheduleAtFixedRate(
new HeartbeatTask(connection, driver, 3), 0, 3, TimeUnit.SECONDS);
| 方案 | 检测延迟 | 评价 |
|---|---|---|
| TCP KeepAlive 默认 | 2 小时+ | ❌ 不可用 |
| TCP KeepAlive 调参 | ~10 分钟 | 🟡 粒度粗,且是系统级 |
| 应用层心跳 | 3~15 秒 | ✅ 推荐 |
FAQ-22 重连风暴
现象:全场掉电恢复后,PLC 迟迟连不上;采集器日志刷屏重连失败。
原因:所有设备在同一时刻集体重连,瞬间打满 PLC 连接资源。
处置:指数退避 + 抖动(见 06 篇 02 章 §3.3)。
BackoffPolicy policy = BackoffPolicy.industrialDefaults(); // 1s 起步,60s 封顶,±30% 抖动
long delay = policy.nextDelay(attempt);
不抖动:08:00:00 全部重连 → 打满 → 全部超时 → 08:00:01 再来 → 风暴持续
抖动后:首轮到期时刻随机铺开在 [0, period] 内 → 峰值被削平
D. RTU 串口类
编号续接,本类为 FAQ-23 ~ FAQ-27。
FAQ-23 CRC 校验错误
现象:从站不响应;或主站侧 CRC 校验失败。
原因:
□ CRC 字节顺序反了(Modbus CRC16 是低字节在前)
□ 多项式 / 初值 / 反转配置不对
□ 数据域范围算错(CRC 不含 CRC 本身)
CRC-16/MODBUS 参数:
多项式 0xA001(反转表示)/0x8005(正常表示)
初值 0xFFFF
输入反转 true,输出反转 true
结果异或 0x0000
线上发送:低字节在前
验证用例(可直接抄去自测):
// 官方示例:01 03 00 00 00 01 → CRC = 84 0A
byte[] frame = {(byte)0x01, (byte)0x03, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x01};
int crc = CRCUtil.crc16(frame);
System.out.printf("CRC=%04X%n", crc); // 期望 0x0A84
// 发送时:先 crc & 0xFF(0x84),再 (crc >> 8) & 0xFF(0x0A)
排查命令:
# 用 modpoll / mbpoll 交叉验证
mbpoll -m tcp -a 1 -r 1 -c 4 192.168.1.130
mbpoll -m rtu -b 9600 -P none -a 1 -r 1 -c 4 /dev/ttyUSB0
FAQ-24 帧间隔(3.5 字符)问题
现象:RTU 通信偶发失败,从站偶尔不识别帧。
原因:RTU 靠帧间静默时间(≥ 3.5 个字符时间)来判定帧边界。
t3.5 = 3.5 × (1 / baudrate) × 每字符位数
11 bit/字符(8N1 + 起始 + 停止):
9600 bps → 3.5 × 11 / 9600 ≈ 4.01 ms
19200 bps → 3.5 × 11 / 19200 ≈ 2.00 ms
115200 bps→ 3.5 × 11 / 115200 ≈ 0.33 ms
三条硬性要求:
1. 帧内字符间隔 ≤ 1.5 字符时间(t1.5)
2. 帧间静默 ≥ 3.5 字符时间(t3.5)
3. 波特率 > 19200 时,可用固定值 1.75ms / 750μs
处置:不要用"读满就好"的逻辑,必须靠超时判定帧结束(详见 03 篇 Modbus §9.4)。
FAQ-25 RS-485 方向切换冲突
现象:多从站时丢包;加终端电阻后更糟;主从距离远时几乎不通。
原因:RS-485 是半双工,收发方向由 DE/RE 引脚控制;切换时序不对会截断帧头或帧尾。
排查清单:
□ USB-485 转换器是否支持自动方向切换(多数廉价转换器不可靠)
□ 是否在 flush 后立刻切回接收(需要等最后一字节发出)
□ 总线两端是否各加 120Ω 终端电阻(只在两端!)
□ 是否加了偏置电阻(A 上拉 / B 下拉),保证空闲态确定
□ 是否单点接地、屏蔽层单端接地
□ 是否菊花链布线(禁止星型)
□ 主从距离是否超过 1200m(9600bps 理论值)
FAQ-26 波特率 / 校验位不匹配
现象:完全无响应,或收到乱码;CRC 恒定失败。
原因:串口参数与从站不一致。
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600 / 19200 / 38400 / 115200 | 双方必须一致 |
| 数据位 | 8(Modbus RTU 固定) | |
| 校验位 | None / Even / Odd | RTU 常用 Even 或 None |
| 停止位 | 1 / 2 | 无校验时常用 2 |
| 流控 | None |
典型组合(8N1 / 8E1 / 8N2)
Modbus RTU 标准:8E1 或 8N2(保证 11 bit/字符)
排查命令:
# Linux 查看串口参数
stty -F /dev/ttyUSB0 -a
# 设置
stty -F /dev/ttyUSB0 9600 cs8 -cstopb -parenb
# 用 mbpoll 试探
mbpoll -m rtu -b 9600 -P even -a 1 -r 1 /dev/ttyUSB0
FAQ-27 串口数据偶发丢帧
现象:偶发超时,重试后成功;干扰强时更频繁。
原因:
□ 电磁干扰(变频器、电机、焊机)
□ 波特率过高 + 线缆过长
□ 终端/偏置电阻缺失
□ 从站响应慢于主站超时
□ 主站发帧间隔 < t3.5
处置:
□ 降波特率(9600 → 4800)或缩短线缆
□ 加磁环、换屏蔽双绞线、屏蔽层单端接地
□ 远离动力线(间距 ≥ 30cm,交叉时垂直)
□ 增大主站超时(按 t3.5 + 帧传输时间 + 从站处理时间计算)
□ 帧间强制 ≥ t3.5 的静默
□ 应用层重试(≤ 2 次,带退避)
超时计算:
T_timeout = t3.5 + (帧字节数 × 每字符时间) + 从站处理余量(≥20ms)
9600bps, 8E1, 响应 25 字节:
= 4.0 + 25 × 1.146 + 20 ≈ 52.6 ms → 建议取 100ms
E. 工程类
FAQ-28 线程数暴涨
现象:jstack / jconsole 看到线程数几百上千;OutOfMemoryError: unable to create new native thread。
原因:
□ 每设备一线程,设备数增长 → 线程线性增长
□ 重连线程没有上限,每次失败都 new Thread()
□ 线程池用了无界队列 + 无限最大线程数
□ 调度任务没关闭,设备反复注册
排查步骤:
# 1. 看线程数
jstack <pid> | grep "^\"" | wc -l
# 2. 按线程名分组统计
jstack <pid> | grep -oP '^\"\K[^"]+' | sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -20
# 3. 看是不是卡在 SocketInputStream.socketRead0
jstack <pid> | grep -B2 -A5 socketRead0 | head -40
处置:
□ 用共享线程池,线程数与设备数解耦(见 06 篇 02 章 §4)
workerThreads ≈ min(ceil(设备数 / 20), 200)
□ 重连交给统一的调度器,不要每次 new Thread
□ 用有界队列 + 合理拒绝策略
□ 设备注销时同步从调度器移除
□ 线程栈:-Xss512k(线程多时)
FAQ-29 GC 压力大(每次 new 大 buffer)
现象:Young GC 频繁;堆内存锯齿;采集延迟抖动。
原因:每帧都 new byte[1024] → 大量短命大对象直接进入老年代或频繁触发 GC。
排查步骤:
# 1. 看 GC 情况
jstat -gcutil <pid> 1000
# 2. 看对象分配热点
jmap -histo:live <pid> | head -30
# 3. 开启分配分析(JFR)
jcmd <pid> JFR.start duration=60s filename=/tmp/alloc.jfr
处置:
// ❌ 错误:每次读写都新建大数组
byte[] buf = new byte[1024];
in.read(buf);
// ✅ 正确:用 ByteBuffer 池 / ThreadLocal 复用
private static final ThreadLocal<byte[]> BUF =
ThreadLocal.withInitial(() -> new byte[1024]);
byte[] buf = BUF.get();
int n = in.read(buf);
// 注意:用完不要把引用传出,防止逃逸
| 手段 | 说明 |
|---|---|
ThreadLocal<byte[]> |
每线程一个缓冲,零分配 |
固定 ByteBuffer |
配合池化 |
避免在热路径 new String(...) |
用 new String(bytes, charset, offset, len) 也要小心 |
| 采集结果对象复用 | 批量模式下复用 DTO |
FAQ-30 采集周期抖动
现象:配置 1s 周期,实际有时 1.05s、有时 1.5s;多设备同时上报形成尖峰。
原因:
□ 用 scheduleAtFixedRate → 任务执行慢时叠加
□ 所有设备初始相位相同 → 同一时刻齐发
□ 单轮耗时接近周期 → 排队
□ GC / 系统负载抖动
处置:
□ 改成"执行完再排下一轮"(本设计采用)
□ 注册时加随机初始延迟(抖动),见 §4.3 的 Task 构造
□ 单轮耗时控制在周期的 50% 以内
□ 设备数多时分桶错峰
FAQ-31 断网后数据丢失
现象:网络中断期间采集到的数据,恢复后平台上查不到。
原因:上报走的是"发完即忘",失败就丢了。
处置:环形缓冲 + WAL(见 06 篇 02 章 §6)。
采集成功 → ① 入内存环形缓冲 → ② 异步刷盘 WAL
↓ MQTT 恢复
③ 回放 → 上报 → 确认 → 推进 checkpoint
| 参数 | 建议 |
|---|---|
| 环形缓冲容量 | 4096 条 |
| WAL 单文件 | 64 MB 滚动 |
| WAL 配额 | 1~2 GB,超了丢最老 |
| fsync | 每 1s 或每 1000 条 |
FAQ-32 采集任务互相拖累(慢设备拖垮全场)
现象:一台设备响应慢,导致其它设备的采集也跟着延后。
原因:所有设备共用一个小线程池,慢设备占着线程不放。
处置:
□ 单设备串行(tryLock),拿不到就跳过本轮 —— 慢设备自我限流
□ worker 池足够大,或按频率分池
□ 严格 SO_TIMEOUT,避免无限等待
□ 连续失败 N 次的设备降级(降低采集频率)
通用排查工具箱
第一梯队:先确认通路
ping <ip> # IP 层
telnet <ip> <port> # TCP 层(最直观)
Test-NetConnection <ip> -Port <port> # Windows
第二梯队:看连接状态
ss -antp | grep <ip> # 详细连接
ss -ant | awk 'NR>1{print $1}' | sort | uniq -c # 状态统计
netstat -s | grep -i retransmit # 重传统计
lsof -i:<port> # 谁占用
第三梯队:抓包
tcpdump -i any host <ip> and port <port> -w /tmp/cap.pcap
# Wireshark 过滤:
# modbus —— Modbus
# s7comm —— 西门子
# slmp —— 三菱(需实测确认)
# tcp.flags.reset == 1
# tcp.analysis.retransmission
第四梯队:看进程
jps -l # 找 PID
jstack <pid> > /tmp/thread.txt # 线程栈
jstat -gcutil <pid> 1000 # GC
jmap -histo:live <pid> | head -30 # 对象分布
排查决策树
现场出问题
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
完全连不上 能连但数据不对 时好时坏
│ │ │
▼ ▼ ▼
FAQ-01/02 FAQ-10/11/13 FAQ-14/17/27
refused/超时 字节序/偏移 粘包/Nagle
│ │ │
▼ ▼ ▼
防火墙/端口 浮点乱码/地址 连接数/CLOSE_WAIT
FAQ-03 TSAP FAQ-12 位压缩 FAQ-18/19
FAQ-04 PUT/GET FAQ-16 死区 FAQ-22 风暴
小结
30+ 条清单索引
| 分类 | 条目 | 关键词 |
|---|---|---|
| A 连接 | 01~09 | refused、超时、TSAP、PUT/GET、优化块、帧类型、0xC059、连接数、0xC05E |
| B 数据 | 10~16 | 整数字节序、float 乱码、bool 位压缩、1基/0基、偶发错位、死区、高低字 |
| C 质量 | 17~22 | Nagle/延迟ACK、CLOSE_WAIT、TIME_WAIT、RST、断电感知、重连风暴 |
| D 串口 | 23~27 | CRC、3.5字符、485方向、波特率、干扰丢帧 |
| E 工程 | 28~32 | 线程暴涨、GC、周期抖动、断网丢数、慢设备拖累 |
三条最高频根因
| # | 根因 | 占比感受 |
|---|---|---|
| 1 | S7 没勾 PUT/GET 或 DB 仍是优化块 | 1200/1500 上"能连不能读"的 99% |
| 2 | 字节序错(尤其 32 位浮点) | "值不对"类问题的一半以上 |
| 3 | 并发访问同一连接导致粘包错位 | "偶发"类问题的主要来源 |
一条建议
先分层定位,再动手改:
第 1 层:ping / telnet —— 通路问题
第 2 层:抓包看有没有响应 —— 协议/参数问题
第 3 层:打印原始字节 —— 解析/字节序问题
第 4 层:看线程栈 / GC —— 工程问题
大多数现场问题在第 1、2 层就能定位。习惯性地"先 telnet 再抓包",能省掉大量猜测时间。
本文是《工业通信与数据采集》栏目第 22/22 篇 · 上一篇:速查手册(附录) | 栏目总览