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 篇 · 上一篇:速查手册(附录) | 栏目总览