02 协议栈与工业网络

本篇定位:把「发一条 Modbus 报文到 PLC,字节是怎么跑到对面的」这件事从物理层讲到应用层。读完你应当能在 Wireshark 里认出一条 S7comm 报文的每一段、算出一条读指令在网线上占了多少字节、并判断某个现场该走网口还是串口、该用 TCP 还是 UDP。字节怎样被解释成数值不在本篇讨论(详见 03-字节序与数据类型编码.md),本篇只关心"字节怎么到达"。

本篇目录


1. OSI 七层与 TCP/IP 四层:对照与工业对应物

OSI 七层是理论模型,TCP/IP 四层是现实实现。工业协议并不总是规矩地落在某一层——很多工业以太网协议故意跳过中间层,这是理解工业网络的关键。

OSI 七层 功能 TCP/IP 四层 典型协议 工业场景对应物
7 应用层 为应用提供服务 应用层 Modbus、S7comm、MC、OPC UA、MQTT 读保持寄存器、读 DB 块、订阅 NodeId
6 表示层 格式、编码、加密 (并入应用层) TLS、OPC UA 编码 字节序约定、BCD、IEEE754(见 03 篇)
5 会话层 会话建立与维护 (并入应用层) S7 Setup Communication、OPC UA Session S7 的 PDU 协商、UA 的 Subscription
4 传输层 端到端可靠/不可靠 传输层 TCP、UDP Modbus TCP/S7comm/MC 走 TCP;Profinet RT、EtherCAT 不走
3 网络层 寻址与路由 网际层 IP、ICMP 现场一般二层直连,跨厂区才用三层
2 数据链路层 相邻节点帧传输 网络接口层 Ethernet、802.1Q VLAN、ARP 工业交换机、VLAN;Profinet/EtherCAT 直接在这层
1 物理层 比特流与电气信号 (并入网络接口层) 100BASE-TX、RS485、RS232 网线、屏蔽接地、光电转换、串口线

关键认知:工业协议在 OSI 上的三种站位

  站位 A:跑在 TCP/IP 之上(最像 IT,最易接入)
      ┌──────────────────────┐
   7  │ Modbus / S7comm / MC │   ← 应用层自定义
   5  │ (S7 Setup Comm)      │
      ├──────────────────────┤
   4  │ TCP(可靠、面向连接) │
   3  │ IP                   │
   2  │ Ethernet / VLAN      │
   1  │ 100BASE-TX / 光纤     │
      └──────────────────────┘
      代表:Modbus TCP、S7comm、MC/SLMP、EtherNet/IP 显式、OPC UA、FINS/TCP
      特点:可路由、可跨网段、Wireshark 能直接看、延迟不可控(有重传尖刺)

  站位 B:跑在 UDP/IP 之上(要低抖动,不要重传)
      ┌──────────────────────┐
   7  │ MC-UDP / Modbus-UDP  │
   4  │ UDP                  │
   3  │ IP                   │
   2/1│ Ethernet             │
      └──────────────────────┘
      代表:MC 协议 UDP 模式、FINS/UDP、Modbus UDP、EtherNet/IP 隐式 I/O(UDP 2222)
      特点:无连接、无重传、延迟低且抖动小,可能丢包 → 应用层自己兜

  站位 C:跳过 TCP/IP,直接跑在二层(硬实时)
      ┌──────────────────────┐
   7  │ Profinet / EtherCAT  │
   ...│  (全部压缩进二层帧) │
   2  │ Ethernet II          │
      │  0x8892 = Profinet   │
      │  0x88A4 = EtherCAT   │
   1  │ 100BASE-TX           │
      └──────────────────────┘
      代表:Profinet RT/IRT、EtherCAT、Powerlink、SERCOS III
      特点:微秒级确定性、不可路由、需专用主站网卡

为什么站位 C 要跳过 TCP/IP? TCP 的重传、拥塞控制、延迟确认都会引入不可预测的延迟。对于"1ms 周期同步 20 个伺服轴",一次 TCP 重传就是灾难。实时协议宁可丢一帧(下一周期再来),也不能等一个不确定的重传。


2. 工业以太网协议家族全景对比

搞采集时,先在这张表里定位目标设备属于哪一格。

协议 传输层 默认端口 / EtherType 基于 TCP? 开放性 主要厂商 采集难度
Modbus TCP TCP 502 是 完全开放 施耐德发起,事实标准 ★☆☆☆☆
Modbus UDP UDP 502 否 完全开放 同上 ★★☆☆☆
Modbus RTU 串行 RS485/232 — 否 完全开放 同上 ★★☆☆☆
Modbus ASCII 串行 — 否 完全开放 同上 ★☆☆☆☆
S7comm TCP 102 是 逆向工程(事实开放) 西门子 ★★★☆☆
S7comm-Plus TCP 102 是 加密/混淆 西门子 ★★★★★
MC 协议 (SLMP/3E) TCP / UDP 9600 可选 公开文档 三菱电机 ★★★☆☆
FINS UDP / TCP 9600 可选 公开文档 欧姆龙 ★★★☆☆
HostLink (C-Mode) 串行 / TCP 自定义 否 公开文档 欧姆龙 ★★★☆☆
EtherNet/IP (CIP) TCP + UDP 44818(显式)、2222(隐式) 两者都用 开放标准(ODVA) 罗克韦尔 AB、欧姆龙 ★★★★☆
Profinet 二层(部分 UDP) EtherType 0x8892 否(RT/IRT) 开放标准(PI) 西门子等 ★★★★★
EtherCAT 二层 EtherType 0x88A4 否 开放标准(ETG) 倍福发起 ★★★★★
OPC UA TCP 4840 是 完全开放(IEC 62541) 跨厂商 ★★★☆☆
OPC DA (Classic) DCOM/RPC 135 + 动态端口 是 Windows 专有 跨厂商 ★★★★☆
Fatek(永宏) TCP / UDP 常见 500(需实测确认) 可选 部分公开 永宏电机 ★★☆☆☆
IEC 60870-5-104 TCP 2404 是 开放标准 电力行业 ★★★☆☆
DNP3 TCP/UDP/串行 20000 可选 开放标准 电力、水务 ★★★☆☆
BACnet/IP UDP 47808 否 开放标准 楼宇自控 ★★★☆☆
MTConnect HTTP 80 / 5000 是 开放标准 机床行业 ★★☆☆☆
MQTT TCP 1883 / 8883(TLS) 是 开放标准 跨厂商 ★☆☆☆☆

补充判断:Modbus 只有"16 位寄存器"的概念、没有数据类型,类型与字节序全靠点表约定(见 03 篇);S7/MC 有类型但地址体系各异;OPC UA 有完整信息模型但要配置 NodeId。

三种 PLC 协议的"握手前奏"对比

搞采集最容易卡在"TCP 连上了但读不出来",原因就是各协议在应用层还有自己的握手。

  Modbus TCP:
     无握手,TCP 连上就能发功能码。
     unitId(从站地址)填错 → 网关不转发 / 设备不应答

  S7comm(S7-300/400/1200/1500):
     TCP 102 连上后还要两步:
       1) COTP 连接请求(ISO-on-TCP / RFC1006)
          携带 Local/Remote TSAP,由 rack/slot 推导
          (S7-1200/1500 常见 rack=0 slot=1;S7-300 常见 rack=0 slot=2,需实测确认)
       2) S7 Setup Communication(ROSCTR=0xF0)
          协商 PDU 大小、MaxAmqCaller/Callee(并发请求数)
     两步都成功才能发 Read(0x04) / Write(0x05)

  MC 协议(3E 帧,二进制):
     TCP 9600 连上后直接发命令帧,无显式握手,但必须填对:
       网络号(常 0x00)、PC 号(常 0xFF)、请求目标模块 I/O 号(常 0x03FF)、
       站号(常 0x00)、CPU 监视定时器(单位 250ms,常 0x0010 = 4s)
     任一填错 → 返回非 0 结束码(具体含义需查对应手册确认)

3. 现场总线:RS485/RS232、Modbus RTU/ASCII、CAN、Profibus-DP

"现场总线"不是某一个协议,而是车间现场这一层设备互联技术的总称,大多基于串行物理层,今天仍大量存在。

3.1 物理层对比:RS232 vs RS485(重点)

特性 RS232 RS485(2 线半双工) RS485(4 线全双工)
信号方式 单端(对地电压) 差分(A/B 压差) 差分
电平 1:-3~-15V;0:+3~+15V 差分 ≥+200mV 为 1,≤-200mV 为 0 同左
拓扑 点对点 总线型(多点) 总线型(主从)
节点数 2 标准 32 单位负载(1/4、1/8 负载收发器可扩到 128/256) 32
最大距离 约 15 m(标准值,低速可更长,需实测确认) 约 1200 m 约 1200 m
速率-距离 115.2kbps @ 数米 100kbps@1200m;10Mbps@约 10m 同左
抗干扰 弱(共模直接叠加) 强(差分抵消共模) 强
终端电阻 无 必须 120Ω(两端各一) 120Ω
典型用途 老 CNC、仪表调试口、PLC 编程口 Modbus RTU 主力、Profibus-DP 少数全双工场景

RS485 布线铁律(现场八成串口故障都源于此):

  1) 屏蔽双绞线(特性阻抗约 120Ω),禁止平行线
  2) 总线型拓扑,禁止星型分支;必须分支时用 RS485 中继器/集线器
  3) 总线两端各接 120Ω 终端电阻,中间节点不接
  4) 屏蔽层单点接地(通常主站端),多点接地形成地环路引入干扰
  5) 远离动力线(尤其变频器输出线),间距 ≥30cm,交叉时垂直交叉
  6) A/B 极性各家标注不统一(常见 A=DATA- / B=DATA+),反了表现为"完全收不到",需试

3.2 Modbus RTU 帧结构

  ┌────────┬────────┬──────────────┬──────────┐
  │ 从站地址 │ 功能码  │  数据         │ CRC16    │
  │ 1 byte  │ 1 byte │ 0~252 bytes  │ 2 bytes  │
  └────────┴────────┴──────────────┴──────────┘
     从站地址:1~247,0 为广播(广播无应答)
     CRC16  :多项式 0xA001(0x8005 的反转),初值 0xFFFF
              ★ CRC 在帧中是【低字节在前】,与 Modbus 其它字段的大端相反

  帧定界:帧前 ≥3.5 字符时间的总线空闲;帧内字符间隔 ≤1.5 字符时间

  3.5 字符时间(8N1:1 起始 + 8 数据 + 1 停止 = 11 bit/char):
     波特率      1 字符      3.5 字符      1.5 字符
     9600        1.146 ms    4.010 ms      1.719 ms
     19200       0.573 ms    2.005 ms      0.859 ms
     38400       0.286 ms    1.003 ms      0.430 ms
     57600       0.191 ms    0.667 ms      0.286 ms
     115200      0.095 ms    0.334 ms      0.143 ms

3.3 Modbus ASCII 帧结构

  ┌────┬──────┬──────┬─────────┬─────┬────────┐
  │ ':'│ 地址  │ 功能码│ 数据     │ LRC │ CR LF  │
  └────┴──────┴──────┴─────────┴─────┴────────┘
  每字节用 2 个 ASCII 十六进制字符表示 → 帧长约为 RTU 的 2 倍
  优点:可读,可用串口助手手动敲命令调试;缺点:效率减半,现场已很少用(LRC 为纵向冗余校验)

3.4 CAN 与 Profibus-DP

特性 CAN 2.0A/B CAN FD Profibus-DP
物理层 差分双绞线 同左 RS485
速率 最高 1 Mbps 数据段 5~8 Mbps 9.6kbps~12 Mbps
单帧数据 8 字节 64 字节 最多 244 字节
介质访问 CSMA/CD+AMP(非破坏性逐位仲裁,ID 小者优先) 同左 主从轮询 + 令牌(多主)
节点数 理论 110 同左 最多 126
终端电阻 120Ω × 2 同左 有源终端
应用层 CANopen、DeviceNet、J1939 CANopen FD DP-V0/V1/V2
用途 汽车、工程机械、AGV、BMS 同上(数据量大时) 老产线主力,正被 Profinet 取代

CAN 的反直觉点:仲裁是逐位进行的,ID 越小优先级越高,仲裁失败方自动退让并在总线空闲后重发,不会破坏已发出的帧。因此 CAN 不需要"3.5 字符时间"定界——帧本身有 SOF/EOF 位,这是它比 RS485 主从协议更实时的根本原因之一。

3.5 现场总线与工业以太网的关系

  现实:不是替代关系,是长期共存 + 网关桥接

  ┌────────── 工业以太网(新,骨干)──────────┐
  │ Modbus TCP / S7comm / MC / OPC UA / Profinet│
  └──────────────────┬─────────────────────────┘
                     │ 网关 / 串口服务器 / 耦合器
                     │ (协议转换 or 透明传输)
  ┌──────────────────┴─────────────────────────┐
  │ 现场总线(老,设备侧)                       │
  │ Modbus RTU(RS485) / CAN / Profibus-DP / HART│
  └─────────────────────────────────────────────┘

  不会被完全替代的原因:
    · 存量巨大(一台仪表几千块能跑 15 年)· 成本(RS485 收发器几毛钱 vs
      以太网 PHY+协议栈几十块)· 布线(两线制、长距离、可总线供电)
    · 本质安全(防爆)场景用以太网更麻烦

4. 报文封装全过程图解与开销计算

以"Modbus TCP 读保持寄存器,从偏移 100 开始读 10 个"为例。

4.1 逐层封装图

【第 7 层】Modbus PDU(协议数据单元)
    ┌──────────┬───────────────┬───────────────┐
    │ 功能码    │ 起始地址       │ 寄存器数量     │
    │ 0x03     │ 0x0064 (=100) │ 0x000A (=10)  │
    │ 1 byte   │ 2 bytes 大端  │ 2 bytes 大端  │
    └──────────┴───────────────┴───────────────┘
    PDU 长度 = 5 字节
             │ 加 MBAP 头(7 字节)
【第 7 层】Modbus ADU = MBAP + PDU
    ┌────────┬────────┬────────┬────────┬──────────────┐
    │ TID 2B │ PID 2B │ LEN 2B │ UID 1B │ PDU 5B       │
    │ 0x0001 │ 0x0000 │ 0x0006 │ 0x01   │ 03 0064 000A │
    └────────┴────────┴────────┴────────┴──────────────┘
    ★ LEN = 后面剩余字节数(UID 1B + PDU 5B = 6),不含 LEN 自身
    ADU 长度 = 12 字节   ← 这才是有用的数据
             │ 加 TCP 头(20 字节,无选项)
【第 4 层】TCP 段:源端口 2B | 目的端口 2B(=502) | 序列号 4B | 确认号 4B |
                 数据偏移+标志位 2B | 窗口 2B | 校验和 2B | 紧急指针 2B | [选项 0~40B]
    段总长 = 20 + 12 = 32 字节
             │ 加 IP 头(20 字节,无选项)
【第 3 层】IP 包:版本+首部长 1B | ToS 1B | 总长度 2B(=52) | 标识 2B | 标志+片偏移 2B |
                 TTL 1B(=64) | 协议 1B(=6 TCP) | 校验和 2B | 源 IP 4B | 目的 IP 4B
    包总长 = 20 + 32 = 52 字节
             │ 加以太网头尾
【第 2 层】以太网帧
    ┌───────┬──────────┬──────────┬────────┬─────────┬──────┬────────┐
    │前导码 8│ 目的MAC 6 │ 源MAC 6   │ 类型 2  │ 数据 46~│ FCS 4 │ 帧间隙12│
    │7×55+D5│          │          │ 0x0800 │  1500   │ CRC32│ (空闲) │
    └───────┴──────────┴──────────┴────────┴─────────┴──────┴────────┘
    帧长度 = 14 + 52 + 4 = 70;线上实际占用 = 8 + 70 + 12 = 90 字节

4.2 开销汇总(残酷的现实)

层次 本层头/尾 累计长度 有用数据占比
应用数据(PDU) 5 5 100%
+ MBAP 7 12 41.7%
+ TCP 头 20 32 15.6%
+ IP 头 20 52 9.6%
+ 以太网头尾 14 + 4 70 7.1%
+ 前导码 + 帧间隙 8 + 12 90 5.6%
  一次采集请求的字节账本(读 20 个寄存器 = 40 字节数据)
    请求: 8+14+20+20+12(ADU)+4+12 = 90 字节
    应答: 8+14+20+20+49(7+1+1+40)+4+12 = 127 字节
    合计线上 217 字节,换取 40 字节有效数据 → 有效载荷率 18.4%
  改成读 120 个寄存器(240 字节数据):
    请求 90 + 应答 327 = 417 字节,换取 240 字节 → 有效载荷率 57.6%

  → 单次读取数量从 20 提到 120,有效载荷率提升 3 倍以上。
    因为大头是固定头部,不是数据本身。这就是"合批"的价值(见 01 篇第 8 节)。

4.3 应答报文的字节级示例

  请求(TCP payload 12 字节):
    00 01  00 00  00 06  01  03 00 64 00 0A
    └TID┘  └PID┘  └LEN┘  └U┘  └────PDU────┘
                          UID  FC  地址  数量

  应答(TCP payload 29 字节 = 7 + 22):
    00 01  00 00  00 17  01  03  14  <20 字节寄存器数据>
    └TID┘  └PID┘  └LEN┘  └U┘  FC┘  └字节计数 0x14 = 20
                    LEN = 0x17 = 23 = 1(UID) + 1(FC) + 1(BC) + 20(DATA)
    读满 7 字节 MBAP 后,还需按 LEN 再读 23 字节

  异常应答:
    00 01 00 00 00 03 01 83 02
                        └┬┘ └┬┘
                         |   └── 异常码 0x02 = 非法数据地址
                         └────── 功能码 0x83 = 0x03 | 0x80(最高位置 1 = 异常)

LEN 的算法必须严谨:LEN = 1(unitId)+ PDU 长度,PDU = FC + 数据。写代码时用 1 + pdu.length,不要手算。另外 Modbus 保持寄存器单次最多读 125 个(线圈最多 2000 个),超出返回异常码 0x03。


5. MTU 与 MSS

概念 定义 典型值 层
MTU 链路层一次能承载的最大 IP 包(不含以太网头尾) 1500 二层
MSS TCP 一次能承载的最大应用数据(不含 TCP/IP 头) 1460 四层(握手协商)
Jumbo Frame 巨型帧 9000(需全网支持) 二层
PMTU 整条路径上最小的 MTU 动态发现 三层
  标准以太网:  MSS = 1500 - 20(IP) - 20(TCP) = 1460
  带 802.1Q:  帧长上限 1518 + 4 = 1522,但 MTU 仍 1500、MSS 仍 1460
               (VLAN 标签在二层,不占 IP 层的 1500)
  PPPoE:      MTU = 1492 → MSS = 1452
  TCP 时间戳:  TCP 头 = 20 + 12 = 32 → 单段最大载荷 = 1500-20-32 = 1448
  巨型帧:      MTU = 9000 → MSS = 8960

工业采集的现实:工业报文通常只有几十到几百字节,远小于 MSS,几乎不会分片。但 MTU 仍然重要:

  1. 判断"能不能一次读完":Modbus 单次 125 个寄存器 = 250 + 7 = 257 字节,不分片;S7comm 的限制来自应用层 PDU 协商(S7-1200 常见 240 字节,需实测确认),与 MTU 无关。
  2. MTU 不匹配导致诡异故障:交换机配了 Jumbo Frame 而 PLC 网卡只支持 1500,大包会被静默丢弃,表现为"小数据通、大数据不通"。
  3. VPN/4G/隧道:IPsec、GRE、WireGuard 额外占用 20~60 字节,未做 MSS Clamping 会出现"能 ping 通但业务连不上"的经典故障。采集器跨 VPN 上云时尤其要注意。

6. TCP 还是 UDP:采集场景的取舍

维度 TCP UDP
连接 面向连接(三次握手 + 四次挥手) 无连接
可靠性 确认、重传、按序、去重 不保证到达/顺序/不重复
延迟特征 重传时延迟尖刺(RTO 可达秒级) 延迟低且抖动小(丢就丢了)
流量控制 滑动窗口 + 拥塞控制 无,可能压垮接收方
头开销 20 字节(+选项) 8 字节
丢包后 等待重传,队头阻塞 直接丢,下一包继续
编程复杂度 粘包/拆包、保活、重连 丢包、乱序、重复都要自己处理

6.1 哪些工业协议用 UDP(以及为什么)

协议 传输层 为什么选 UDP
Profinet RT/IRT 二层(不走 UDP) 硬实时,周期 ≤1ms,不能容忍重传
EtherCAT 二层 同上;"飞读飞写"环型帧,一帧遍历所有从站
EtherNet/IP 隐式 I/O UDP 2222 周期 I/O,丢一帧下一周期补,比等重传划算
MC 协议 UDP 模式 UDP 9600 三菱提供 UDP 选项,省连接管理,适合高频轮询
Modbus UDP UDP 502 省握手但无重传,现场较少用
FINS/UDP UDP 9600 欧姆龙常用 UDP 模式
BACnet/IP UDP 47808 楼宇自控,广播发现天然适合 UDP
IEC 61850 GOOSE/SV 二层/组播 变电站跳闸信号,亚毫秒

6.2 选型判断

  设备/协议规定了传输层?
    ├─ 是 → 按协议来(Modbus TCP=TCP,FINS 常=UDP)
    └─ 否 → 继续
  需要跨公网 / 跨三层路由?
    ├─ 是 → TCP(UDP 公网丢包高、NAT 会话易老化)
    └─ 否 → 继续
  周期 ≤10ms 且要求确定性?
    ├─ 是 → UDP(接受偶发丢包,应用层用序号检测)
    └─ 否 → TCP
  数据绝对不能丢(写指令、报警、计费)?
    └─ TCP + 应用层确认

  务实结论:工业数据采集(Modbus/S7/MC 轮询)一律 TCP。
    1) 协议本身就规定 TCP;2) 采集是秒级/百毫秒节拍,TCP 重传延迟不影响;
    3) 省掉一整套丢包处理逻辑。只有自定义高频采集(≥100Hz)或运动控制才考虑 UDP。

6.3 TCP 长连接在采集中的三个必做项

  1) SO_KEEPALIVE 不够用:内核默认 2 小时才探测,对工业采集等于没有。
     必须自己做应用层心跳(轻量读指令或协议自带心跳),周期 3~10 秒。

  2) 必须处理"半开连接":网线被拔/PLC 重启后,本地 TCP 仍是 ESTABLISHED,
     但发出的报文永远收不到应答,可能阻塞几分钟。
     对策:设置 SO_TIMEOUT(1~3 秒);超时后主动 close() 重建;
           指数退避重连(1s,2s,4s...上限 60s)+ 随机抖动。

  3) 必须处理粘包/拆包:TCP 是字节流,没有消息边界,
     正确做法:先读固定长度头(MBAP 7 字节)→ 解析长度 → readFully() 读满。见第 11 节。

7. 端口与连接:常用工业端口速查

端口 协议 传输层 说明
502 Modbus TCP / Modbus UDP TCP/UDP 工业领域最常见的端口
102 S7comm(ISO-on-TCP / RFC1006) TCP 西门子 S7 全系列
9600 MC 协议(SLMP/3E)、FINS、HostLink TCP/UDP 三菱与欧姆龙共用,注意别混淆
4840 OPC UA 二进制(opc.tcp://) TCP OPC UA 默认端口
44818 EtherNet/IP 显式报文(CIP) TCP 罗克韦尔/欧姆龙
2222 EtherNet/IP 隐式 I/O UDP 周期性 I/O 数据
1883 / 8883 MQTT / MQTT over TLS TCP 采集数据上行通道
80 / 443 HTTP / HTTPS TCP MTConnect、设备配置页、REST API
2404 IEC 60870-5-104 TCP 电力远动
20000 DNP3 TCP/UDP 电力/水务
47808 BACnet/IP UDP 楼宇自控
500 Fatek(永宏)(需实测确认) TCP/UDP 台湾小型 PLC
135 + 1024~65535 OPC DA (DCOM/RPC) TCP 动态端口,防火墙噩梦

连接数:工业设备的硬限制

  · 西门子 S7-1200/1500:通信连接资源按"连接资源池"分配,
    PG / HMI / S7 通信 / 开放式用户通信共享该池子,
    工程上常按 8 路左右估算(取决于 CPU 型号与固件版本,需实测确认)
  · 三菱、欧姆龙、AB 同样有"最大连接数"参数,通常在以太网模块参数里设置
  · 串口链路:一条 RS485 总线同一时刻只能有一个主站在问(半双工)

  架构对策:
    1. 一台 PLC 只建【一条】TCP 连接,采集器内部复用(连接池 size = 1)
    2. 多个业务模块共享同一条连接的数据,不要各开一条
    3. 断开时确保真的 FIN/RST,避免 PLC 侧残留"僵尸连接"
       (只能靠重启 PLC 清理的僵尸连接是现场噩梦)
    4. 调试用的编程软件记得关掉,它也占连接;跨 NAT 时会话表也会耗尽

8. 网络拓扑:现场层怎么组网

8.1 典型三层拓扑

  ┌──────────────────────┐
  │ 企业网络 / 办公网      │  ERP / MES / 云平台
  └──────────┬───────────┘
             │ 工业防火墙 / 网闸(只放行采集器 → 平台的单向出站)
  ┌──────────┴───────────┐
  │ DMZ:边缘采集服务器     │  时序库 / 前置机
  └──────────┬───────────┘
             │ 工业防火墙(只放行采集器 → PLC 的指定 IP:Port)
  ┌──────────┴─────────── 车间控制网 VLAN 10 ─────────────────┐
  │  ┌──────────┐   ┌──────────┐   ┌──────────┐               │
  │  │工业交换机 │═══│工业交换机 │═══│工业交换机 │  ← 环网       │
  │  └────┬─────┘   └────┬─────┘   └────┬─────┘               │
  │   ┌───┴────┐    ┌────┴────┐    ┌────┴────┐                │
  │   │S7-1200 │    │S7-1500  │    │  HMI    │                │
  │   │  .10   │    │  .11    │    │  .20    │                │
  │   └────────┘    └─────────┘    └─────────┘                │
  │  ┌──────────┐──RS485 总线──[仪表1][仪表2][仪表3]            │
  │  │串口服务器│  .30                                         │
  │  └──────────┘                                             │
  └───────────────────────────────────────────────────────────┘

8.2 拓扑形态对比

拓扑 结构 优点 缺点 冗余恢复时间
星型 所有设备接中心交换机 简单、易排查、故障隔离好 中心交换机单点故障 无冗余
环形 交换机首尾相连成环 布线省、单点断线仍能通信 需冗余协议,配置复杂 MRP(IEC 62439) < 200ms;RSTP 数秒(需实测)
总线型 RS485/CAN 手拉手 布线最省、距离长 一处断全线瘫;速率受限 无冗余
双星型 设备双网口接两台交换机 冗余度最高 成本高,需设备支持双口 < 100ms(需实测确认)

车间主干建议环形 + 冗余协议(MRP、HIPER-Ring 等),设备接入用星型。一次断网造成的历史数据空洞,成本远高于一台冗余交换机。

8.3 VLAN 隔离与工业交换机

  推荐划分:
    VLAN 10  1 号车间控制网   192.168.10.0/24
    VLAN 20  2 号车间控制网   192.168.20.0/24
    VLAN 30  视觉/相机网络    192.168.30.0/24   ★ 相机流量大,必须单独隔离
    VLAN 40  采集器上行       192.168.40.0/24
    VLAN 99  管理网络         192.168.99.0/24

  ★ 工业相机(GigE Vision)单台可达上百 Mbps,与 PLC 同网会直接打死控制网实时性。

  工业交换机选型要点:
    · 导轨式(DIN rail)、宽温 -40~75℃、冗余电源(DC 24V 双输入)
    · 支持环网冗余协议(MRP / RSTP)、802.1Q VLAN、802.1p QoS
    · 支持端口镜像(抓包排查必需);无风扇(车间粉尘大,风扇是故障源)
    · 关闭未使用端口,防止私接设备

8.4 DTU / 串口服务器的透传模式

串口服务器把 RS485/RS232 转成以太网,有两种模式,区别巨大:

模式 原理 上层看到什么 优点 缺点
透明传输(TCP Server/Client) 串口字节流原样装进 TCP 采集器必须自己按 Modbus RTU 组帧、算 CRC、判帧边界 通用、便宜、不改协议 "3.5 字符时间"在 TCP 上消失,必须靠 CRC + 协议结构自己定界
协议转换(RTU→TCP 网关) 盒子自己做协议转换 采集器发标准 Modbus TCP(MBAP) 上层简单、可用标准库 盒子贵、常有并发限制(很多一次只处理一个请求)
  透传模式下的"TCP 粘包"问题:
    RS485 上靠 3.5 字符静默区分两帧;TCP 上静默被缓冲抹平,两帧可能粘在一起。
    定界方法(推荐 1 + 2 组合):
      1) 按协议结构推算:读满 addr+func,据 func 推算剩余长度,再读满
      2) 用 CRC 试探:从缓冲区起始按候选长度试算 CRC,匹配则为一帧
      3) 超时法:等待 N ms 无新数据认为一帧结束(简单但慢,不推荐)

采集架构上如何选择网关、透传与硬接线,参见 01-工业数据采集全景与架构.md 第 5 节。


9. 请求-响应模型:半双工、全双工与 3.5 字符时间

9.1 半双工 vs 全双工

概念 定义 例子 工业含义
半双工 双向但不能同时 RS485 两线、对讲机、Modbus RTU 同一时刻只有一方能发;主站发完必须"松开总线"
全双工 双向可同时 RS232、交换式以太网、RS485 四线 收发独立通道,无冲突
  Modbus RTU(半双工)时序:
    主站 TX ████████░░░░░░░░████████░░░░░
    从站 TX ░░░░░░░░████████░░░░░░░░░░░░░░
             请求      应答    下一帧前 ≥3.5 字符
    ★ RS485 靠 DE/RE 引脚做方向切换,切换有延迟;波特率越高越容易切错,
      表现为"第一个字节丢失"——高波特率 RTU 最常见的硬件层问题(需实测确认)。

  Modbus TCP(应用层仍是请求-响应,传输层全双工):
    客户端可不等 resp1 就发 req2(流水线),靠 MBAP 的 TID 匹配。
    ★ 但绝大多数从站只支持串行处理,发第二个请求会返回异常或直接丢弃
      → 工程上默认"一问一答,串行"。

9.2 为什么 Modbus RTU 需要 3.5 字符时间

  根本原因:Modbus RTU 帧【没有起始符和结束符】
    Modbus ASCII:以 ':' 开头、CR LF 结尾        → 有定界符
    Modbus TCP  :MBAP 里有 length 字段           → 有长度字段
    Modbus RTU  :只有 addr+func+data+crc         → 没法定界!

  既然帧本身没有边界,就只能靠"时间"定界:
    · 总线静默 ≥3.5 字符时间 → 上一帧结束,后面是新帧
    · 帧内字符间隔 >1.5 字符时间 → 帧出错,丢弃

  为什么是 3.5:工程折中。太小(1.5)慢速从站来不及应答就被判超时;
  太大(10)总线利用率太低。3.5 是早期 Modicon 设备响应能力的经验上界
  (确切依据需查原始规范确认)。

  后果:
  ```text
  1) 波特率越高帧间隔越短 → 利用率越高(9600bps 下 4.01ms,115200 下仅 0.33ms)
    2) 低成本仪表计时不准 → 兼容性问题频发
    3) 主站必须"一次性写完整帧",分字节发送会被判成帧间隔超时

9.3 一次 RTU 轮询的耗时估算

  场景:9600bps 8N1,读 10 个保持寄存器
    主站请求帧 8 B × 11 bit      =  9.17 ms
    3.5 字符静默                 =  4.01 ms
    从站响应帧 25 B × 11 bit     = 28.65 ms
    3.5 字符静默                 =  4.01 ms
    ─────────────────────────────────────
    单次轮询 ≈ 45.8 ms → 一条 RS485 总线每秒最多约 21 次轮询
    挂 20 台仪表时,每台最快约 1 秒才能轮到一次
    → 这就是 RS485 采集周期通常只能做到秒级的物理原因

    提速:提高波特率(同时重评距离与干扰)/ 拆成多条总线 /
          减少单次读取点数 / 关键设备独占总线

10. 抓包分析入门:Wireshark 看 Modbus TCP 与 S7comm

抓包是排查采集问题最有力的手段。怀疑"报文发错了"时,抓包比猜快 10 倍。

10.1 抓包位置选择

  位置 1:采集服务器上抓(最常用)— 简单,但看不到报文是否真到达 PLC
  位置 2:交换机端口镜像(SPAN)— 能看到真实线路流量含 PLC 应答,需交换机支持
  位置 3:串口(RS485)— 并联 USB-RS485 处于"只听"模式;Wireshark 抓不了串口,
          需用 Modbus Poll 日志等串口工具
  位置 4:PLC 侧诊断缓冲(罕见)— 部分 PLC 提供通信日志

10.2 Wireshark 过滤表达式速查

  # ===== 基础定位 =====
  ip.addr == 192.168.1.10            tcp.port == 502    # Modbus TCP
  tcp.port == 102                    tcp.port == 9600   # S7comm / MC,FINS

  # ===== Modbus =====
  modbus / mbtcp                     # 已解析的 Modbus 报文
  modbus.func_code == 3              # 读保持寄存器
  modbus.func_code == 16             # 写多个寄存器(16 = 0x10)
  modbus.func_code == 0x83           # 异常响应(0x80 置位)
  modbus.func_code > 0x80            # 所有异常
  modbus.reference_num == 100        # 按地址过滤
  modbus.word_cnt == 10              # 按读取数量过滤

  # ===== S7comm =====
  s7comm                             s7comm.param.func == 0x04   # Read Var
  s7comm.param.func == 0x05          # Write Var
  s7comm.param.func == 0xf0          # Setup communication(握手)
  s7comm.rosctr == 1                 # Job(请求)
  s7comm.rosctr == 3                 # Ack_Data(应答)
  s7comm && tcp.len > 0              # 排除纯 ACK

  # ===== TCP 问题诊断 =====
  tcp.flags.reset == 1               # RST:连接被拒/被断 ★重点★
  tcp.analysis.retransmission        # 重传(链路质量差)
  tcp.analysis.duplicate_ack         tcp.analysis.zero_window
  tcp.time_delta > 0.1               # 响应慢(>100ms)
  tcp.len == 0                       # 纯 ACK / 心跳

  # ===== 组合 =====
  tcp.port == 502 && tcp.flags.reset == 1      # 连接被拒 → 连接数满了?
  ip.addr == 192.168.1.10 && tcp.analysis.retransmission

10.3 怎么看一条 Modbus TCP 报文

Wireshark 中间面板的分层树:

  ▼ Frame: 78 bytes on wire
  ▼ Ethernet II, Dst: SiemensI_xx:xx:xx      Type: IPv4 (0x0800)
  ▼ Internet Protocol, Src: 192.168.1.100, Dst: 192.168.1.10
  ▼ Transmission Control Protocol, Src Port: 49152, Dst Port: 502
       Destination Port: 502            ← 确认端口对
       Flags: 0x018 (PSH, ACK)
       [TCP payload: 12 bytes]          ← 应用层长度,对不上就是粘包/拆包
  ▼ Modbus
    ▼ Modbus/TCP
         Transaction Identifier: 1      ← 请求/响应配对
         Protocol Identifier: 0         ← 必须为 0
         Length: 6                      ← unitId + PDU 的字节数
         Unit Identifier: 1             ← 从站地址,网关场景必看
    ▼ Modbus
         Function Code: Read Holding Registers (3)
         Reference Number: 100          ← 起始地址(0 基)
         Word Count: 10

排查清单:

  1) 有 SYN 直接 RST?→ 端口未开放 / PLC 连接数已满 ★最常见★
     没有 SYN-ACK → 网络不通 / 防火墙拦 / PLC 没开机
  2) 有没有 Modbus 请求发出来?没有 → 采集器逻辑问题(点表没配、调度没跑)
  3) Unit Identifier 对不对?网关场景(一 IP 挂多台 RTU 从站)全靠它
  4) 响应 Function Code 是否 ≥0x80?看异常码:0x01 非法功能、
     0x02 非法数据地址★最常见、0x03 非法数据值、0x04 从站故障、0x06 从站忙★要降频
  5) TCP payload 长度与 MBAP Length 是否一致?不一致 → 粘包/拆包解析错
  6) 是否有大量 retransmission?→ 链路质量问题(网线、交换机、干扰)
  7) tcp.time_delta 是否稳定?忽大忽小 → PLC 忙或网络拥塞

10.4 怎么看一条 S7comm 报文

  ▼ Transmission Control Protocol, Dst Port: 102
  ▼ TPKT                        ← RFC1006,ISO-on-TCP 封装
       Version: 3
       Length: 31               ← 含 TPKT 头自身的总长度
  ▼ ISO 8327-1 OSI COTP
       PDU Type: DT Data (0x0f) ← 0xe0=CR(连接请求) 0xd0=CC 0x0f=DT(数据)
  ▼ S7 Communication
       Header: (ROSCTR: Job)    ← 0x01=Job 0x02=Ack 0x03=Ack_Data 0x07=UserData
       Parameter: (Read Var)

S7comm 排查清单:

  1) 有没有 COTP 连接请求(CR, 0xE0)与确认(CC, 0xD0)?
     握手失败 → rack/slot 不对,或 PLC 不允许该连接。★ S7-1200/1500 需在
     TIA Portal → CPU 属性 → 保护 → 连接机制里勾选"允许来自远程对象的
     PUT/GET 通信访问"★(不同固件选项名可能不同,需实测确认)
  2) 有没有 Setup Communication(Func=0xF0)?没有 → 驱动未做协商,直接读会失败
  3) Read Var 应答的 Return Code:0xff = 成功(其余含义需查 S7comm 文档确认)
  4) 新固件可能用 S7comm-Plus(加密/混淆),Wireshark 只显示"不明负载",
     第三方库也可能读不了 → 改用 Modbus TCP 或 OPC UA

无图形界面时可用 tcpdump -i eth0 host 192.168.1.10 and port 502 -w /tmp/modbus.pcap -c 1000 抓包后拷回本地分析;只看 RST 用 tcpdump -i eth0 'tcp port 502 and (tcp[tcpflags] & tcp-rst != 0)'。


11. Java 手写 Modbus TCP 报文(可直接运行)

下面这段代码不依赖任何第三方库,用 JDK 原生 Socket 完成一次 Modbus TCP 读保持寄存器,演示三个关键工程点:MBAP 组装、readFully 分段读防粘包、异常码判断。

import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 原生 Socket 实现 Modbus TCP 读保持寄存器(功能码 0x03)。
 *
 * 编译:javac ModbusTcpRawDemo.java
 * 运行:java ModbusTcpRawDemo 192.168.1.10 502 1 100 10
 *
 * 演示要点:
 *   1) MBAP 组装与 length 字段的正确算法(length = 1 + PDU 长度)
 *   2) 先读 7 字节 MBAP、再按 length 用 readFully 读满 —— 彻底避免 TCP 粘包/拆包
 *   3) 异常响应(功能码最高位置 1)的判断
 *   4) Modbus RTU 的 CRC16(多项式 0xA001,且低字节在前)
 */
public class ModbusTcpRawDemo {

    /** MBAP 头固定 7 字节 */
    private static final int MBAP_LEN = 7;

    /** 事务标识,每次请求自增,用于匹配请求与响应 */
    private static final AtomicInteger TID = new AtomicInteger(1);

    public static void main(String[] args) throws Exception {
        String host   = args.length > 0 ? args[0] : "127.0.0.1";
        int    port   = args.length > 1 ? Integer.parseInt(args[1]) : 502;
        int    unitId = args.length > 2 ? Integer.parseInt(args[2]) : 1;
        int    offset = args.length > 3 ? Integer.parseInt(args[3]) : 0;  // 0 基偏移
        int    count  = args.length > 4 ? Integer.parseInt(args[4]) : 10; // 上限 125

        byte[] request = buildReadHoldingRegisters(unitId, offset, count);
        System.out.println("TX: " + toHex(request));

        try (Socket socket = new Socket()) {
            socket.connect(new InetSocketAddress(host, port), 3000); // 连接超时必设
            socket.setSoTimeout(3000);                              // 读超时必设(防半开连接)
            OutputStream out = socket.getOutputStream();
            out.write(request);
            out.flush();
            DataInputStream in = new DataInputStream(socket.getInputStream());

            // ---- 第一段:读固定 7 字节的 MBAP ----
            byte[] mbap = new byte[MBAP_LEN];
            in.readFully(mbap);
            int tid = ((mbap[0] & 0xFF) << 8) | (mbap[1] & 0xFF);
            int len = ((mbap[4] & 0xFF) << 8) | (mbap[5] & 0xFF);  // 后续字节数(含 uid)
            int reqTid = ((request[0] & 0xFF) << 8) | (request[1] & 0xFF);
            if (tid != reqTid) {
                throw new IOException("事务标识不匹配,可能与其它响应串包");
            }

            // ---- 第二段:按 length 读满剩余字节(防粘包的关键)----
            byte[] rest = new byte[len];
            in.readFully(rest);

            // rest[0] 是 unitId,PDU 从 rest[1] 开始
            int fc = rest[1] & 0xFF;
            if ((fc & 0x80) != 0) {
                int ex = rest[2] & 0xFF;
                throw new IOException("Modbus 异常 fc=0x" + Integer.toHexString(fc)
                        + " 异常码=0x" + Integer.toHexString(ex)
                        + " (" + describeException(ex) + ")");
            }
            if (fc != 0x03) {
                throw new IOException("功能码不匹配: 0x" + Integer.toHexString(fc));
            }
            int byteCount = rest[2] & 0xFF;
            if (byteCount != count * 2) {
                throw new IOException("字节计数异常: " + byteCount + " 期望 " + (count * 2));
            }

            // ---- 第三段:解析寄存器(每个 2 字节,大端)----
            for (int i = 0; i < count; i++) {
                // ★ & 0xFF 不可省:Java 的 byte 有符号,0xFF 会被当成 -1
                int v = ((rest[3 + i * 2] & 0xFF) << 8) | (rest[4 + i * 2] & 0xFF);
                System.out.printf("  偏移[%d] 0x%04X = %d%n", offset + i, v, v);
            }
        }
    }

    /**
     * 构造读保持寄存器(0x03)请求 ADU。
     * MBAP: tid(2) pid(2)=0 len(2)=1+pdu.length uid(1)
     * PDU : fc(1)=0x03 startAddr(2) quantity(2)
     */
    public static byte[] buildReadHoldingRegisters(int unitId, int offset, int quantity) {
        byte[] pdu = {0x03,
                (byte) ((offset >> 8) & 0xFF),   (byte) (offset & 0xFF),
                (byte) ((quantity >> 8) & 0xFF), (byte) (quantity & 0xFF)};
        int tid = TID.getAndIncrement() & 0xFFFF;
        byte[] adu = new byte[MBAP_LEN + pdu.length];
        adu[0] = (byte) ((tid >> 8) & 0xFF);
        adu[1] = (byte) (tid & 0xFF);
        adu[2] = 0x00;                                    // protocolId 必须为 0
        adu[3] = 0x00;
        adu[4] = (byte) (((1 + pdu.length) >> 8) & 0xFF); // length = uid(1) + pdu
        adu[5] = (byte) ((1 + pdu.length) & 0xFF);
        adu[6] = (byte) (unitId & 0xFF);
        System.arraycopy(pdu, 0, adu, MBAP_LEN, pdu.length);
        return adu;
    }

    /** Modbus RTU 的 CRC16:多项式 0xA001,初值 0xFFFF。★ 必须用无符号右移 >>> */
    public static int crc16(byte[] data) {
        int crc = 0xFFFF;
        for (byte b : data) {
            crc ^= (b & 0xFF);
            for (int j = 0; j < 8; j++) {
                crc = ((crc & 1) != 0) ? ((crc >>> 1) ^ 0xA001) : (crc >>> 1);
            }
        }
        return crc & 0xFFFF;
    }

    /** 把 CRC 按"低字节在前"追加到 RTU 帧尾(与 Modbus 其它字段的大端相反) */
    public static byte[] appendCrc(byte[] frame) {
        int crc = crc16(frame);
        byte[] out = new byte[frame.length + 2];
        System.arraycopy(frame, 0, out, 0, frame.length);
        out[frame.length]     = (byte) (crc & 0xFF);        // 低字节在前
        out[frame.length + 1] = (byte) ((crc >> 8) & 0xFF); // 高字节在后
        return out;
    }

    /** 异常码说明 */
    private static String describeException(int code) {
        switch (code) {
            case 0x01: return "非法功能";
            case 0x02: return "非法数据地址";   // ★ 最常见:地址算错了
            case 0x03: return "非法数据值";
            case 0x04: return "从站设备故障";
            case 0x05: return "确认(需长耗时)";
            case 0x06: return "从站设备忙";     // ★ 要降频
            case 0x0A: return "不可用网关路径";
            case 0x0B: return "网关目标设备响应失败";
            default:   return "未知";
        }
    }

    /** 字节数组转十六进制字符串,便于与 Wireshark 对照 */
    public static String toHex(byte[] data) {
        StringBuilder sb = new StringBuilder(data.length * 3);
        for (byte b : data) {
            sb.append(String.format("%02X ", b & 0xFF));
        }
        return sb.toString().trim();
    }
}

运行输出示例:TX: 00 01 00 00 00 06 01 03 00 64 00 0A → 偏移[100] 0x4148 = 16712、偏移[101] 0x0000 = 0。这两个寄存器若按 32 位浮点解释就是 12.5——但前提是你选对了字节序,见 03-字节序与数据类型编码.md。


小结


常见误区


本文是《工业通信与数据采集》栏目第 2/22 篇 · 上一篇:工业数据采集全景与架构 | 栏目总览 | 下一篇:字节序与数据类型编码