02 · S7 协议(西门子 S7comm)详解

定位:面向有 Java 基础、正在做工业物联网平台(FastBee)数据采集层的工程师,把 S7comm 从"能连上"讲到"能手工拼报文、看懂每个字节、定位现场故障"。文中字节值均取自本地两份开源库源码(HslCommunication Java 3.9.3、iot-communication)的真实常量与构造逻辑,标注了文件与行号;源码未定义或未实测的内容标注"(需实测确认)"。

目录

  1. 西门子通信家族概览
  2. 协议栈分层:TPKT + COTP + S7comm
  3. TPKT 头(RFC1006)
  4. COTP 的两种形态:CR/CC 与 DT
  5. TSAP 的奥秘(连接失败第一嫌疑)
  6. S7comm PDU:Header + Parameter + Data
  7. Setup Communication(0xF0):PDU 协商与两次握手
  8. 读请求构建:Function 0x04 与 S7ANY
  9. 区域码(Area code)对照表
  10. 读响应解析与返回码
  11. 写请求:Function 0x05、写位与写字
  12. 地址语法与解析代码
  13. PDU 长度限制与自动分包
  14. 完整报文实例(5 例)
  15. Fetch/Write 协议与 S7comm 对比
  16. 现场配置要点
  17. 常见错误码与排查清单
  18. 小结与现场坑清单

1. 西门子通信家族概览

名称 本质 传输层 / 端口 典型机型 是否公开 采集用途
S7comm 私有应用层,事实标准 TCP 102 300/400/1200/1500/200Smart 未公开 本篇主角,读写 I/Q/M/DB/T/C
ISO-on-TCP(RFC1006) S7comm 的承载层(TPKT+COTP) TCP 102 同上 公开 提供连接与长度分帧
Fetch/Write S5 遗留的简单读写协议 TCP 102(可配) 300/400(需 CP 侧启用) 半公开 老项目兼容,见 §15
OPC UA 平台无关标准 TCP 4840 1500 内置;1200 部分固件支持 公开 跨厂商集成
Profinet 实时工业以太网(二层帧,非 TCP) EtherType 0x8892 全系列 + 分布式 IO 公开 IO 级联,非采集协议
MPI / PROFIBUS / PPI 串行现场总线 RS485 200(PPI)、300/400 半公开 老设备,需串口网关
S7comm-Plus TIA 与 1200/1500 间的扩展/加密版本 TCP 102 1200/1500 未公开 工程下载/诊断(需实测确认)
Modbus TCP 标准协议(PLC 作服务器) TCP 502 1200/1500 需库指令 公开 备选通道

工程结论:做采集就认 S7comm over ISO-on-TCP,端口 102,唯一能同时覆盖 300/400/1200/1500/200Smart 且无需在 PLC 写代码的通道。


2. 协议栈分层:TPKT + COTP + S7comm

S7comm 没有自己的分帧能力,寄生在 ISO-on-TCP 之上:TPKT 提供长度,COTP 提供连接语义。

+-----------------------------------+  应用层
| S7comm PDU                        |
|  Header     (10 B 请求 / 12 B 响应)|  0x32 开头
|  Parameter  (可变)  Function+items |
|  Data       (可变)  数据项         |
+-----------------------------------+
| COTP (ISO 8073)                   |  传输层
|  CR/CC : 11 E0 / 11 D0 ...        |  连接建立(仅握手)
|  DT    : 02 F0 80                 |  数据传输(之后每帧固定 3 字节)
+-----------------------------------+
| TPKT (RFC1006)  03 00 <len>       |  4 字节,长度含自身
+-----------------------------------+
| TCP (102) / IP / Ethernet         |

会话帧序:

客户端                                    PLC
  |-- TCP 三次握手 ------------------------>|
  |== 第一次握手:COTP ====================|
  |-- CR (Connect Request) -------------->|  03 00 00 16 11 E0 ...
  |<- CC (Connect Confirm) ---------------|  03 00 00 16 11 D0 ...
  |== 第二次握手:S7 通信协商 =============|
  |-- DT: Job / Setup Comm (0xF0) ------->|  03 00 00 19 02 F0 80 32 01 ...
  |<- DT: AckData / Setup Comm -----------|  03 00 00 1B 02 F0 80 32 03 ...
  |== 数据交互(可反复,严格串行)========|
  |-- DT: Job / Read Var (0x04) --------->|  |-- DT: Job / Write Var (0x05) -------->|
  |<- DT: AckData / Read Var -------------|  |<- DT: AckData / Write Var ------------|

关键认知:握手后所有读写帧的前 7 字节永远是 03 00 xx xx 02 F0 80 = TPKT(4) + COTP DT(3)。


3. TPKT 头(RFC1006)

偏移 字节数 示例值 含义
0 1 0x03 版本号,RFC1006 固定为 3
1 1 0x00 保留,恒 0
2-3 2 00 16 长度(大端),含自身 4 字节,示例 22

源码:iot-communication/.../s7/model/TPKT.java:41-65(BYTE_LENGTH=4、version=0x03、reserved=0x00,注释明确"长度包括 payload+版本号+预留+长度");HslCommunication/Core/IMessage/S7Message.java:5-15 的 GetContentLengthByHeadBytes() 返回 (HeadBytes[2]<<8 | HeadBytes[3]) - 4——减 4 正是因为含自身。

3.1 为什么它是天然的粘包解法

TCP 是字节流,S7 帧长随内容变化,必须自带长度。TPKT 恰好在第 3、4 字节给出整帧长度,收包逻辑是标准两步:① 阻塞读满 4 字节并校验 byte[0]==0x03 && byte[1]==0x00(S7Message.java:18-29);② 由 byte[2..3] 得 N,再读满 N-4 字节即为一帧。工程提醒:N < 4 或异常大说明流已错位,应断开重连;一个 socket 上必须严格"发一帧、收完整一帧"再发下一帧(MaxAmQ 通常为 1,见 §7)。


4. COTP 的两种形态:CR/CC 与 DT

4.1 DT(Data Transfer):02 F0 80

帧内偏移 字节数 值 含义
4 1 02 COTP 长度 = 2(不含本字节)
5 1 F0 PDU 类型 DT Data(enums/EPduType.java:67)
6 1 80 bit7=最后数据单元 + TPDU 编号 0

源码:model/COTPData.java:105-112 的 createDefault()(length=2、DT_DATA、tpduNumber=0、lastDataUnit=true),序列化为 setBit(0x00,7,true)|0 = 0x80(:75)。

4.2 CR(Connect Request)逐字节拆解

以 Profinet/Siemens/SiemensS7Net.java:832-836 的默认常量(S7-1200)为例:

private byte[] plcHead1 = new byte[] {
        0x03, 0x00, 0x00, 0x16, 0x11, (byte) 0xE0, 0x00, 0x00, 0x00, 0x01, 0x00, (byte) 0xC0, 0x01,
        0x0A, (byte) 0xC1, 0x02, 0x01, 0x02, (byte) 0xC2, 0x02, 0x01, 0x00 };
偏移 字节数 示例值 含义
0-3 4 03 00 00 16 TPKT,长度 22
4 1 11 COTP 长度 = 17(不含本字节,即 5~21)
5 1 E0 PDU 类型 CR(EPduType.java:42)
6-7 2 00 00 DST-REF
8-9 2 00 01 SRC-REF(客户端自定义)
10 1 00 flags:Class 0 / 无扩展格式 / 无显式流控
11-13 3 C0 01 0A 参数项 TPDU-Size:码 0xC0、长 0x01、值 0x0A(2^10 = 1024)
14-17 4 C1 02 01 02 参数项 SRC-TSAP:码 0xC1、长 0x02、值 0x0102(本地 TSAP)
18-21 4 C2 02 01 00 参数项 DST-TSAP:码 0xC2、长 0x02、值 0x0100(远端 TSAP)

参数项顺序不敏感;iot-communication 的顺序同为 C0 01 0A → C1 02 .. → C2 02 ..(COTPConnection.java:145-161)。

4.3 CC(Connect Confirm)

真实抓包(SINUMERIK 828D,src/test/java/.../service/S7PLCMachine1Test.java:47):

03 00 00 16 11 D0 00 01 00 01 00 C0 01 0A C1 02 04 00 C2 02 0D 04
偏移 字节数 值 含义
0-3 4 03 00 00 16 TPKT 长度 22
4-5 2 11 D0 COTP 长度 17 + PDU 类型 CC(EPduType.java:47)
6-9 4 00 01 00 01 DST-REF / SRC-REF(回显并交换)
10-13 4 00 C0 01 0A flags + TPDU-Size = 1024
14-17 4 C1 02 04 00 回显本地 TSAP 0x0400
18-21 4 C2 02 0D 04 回显远端 TSAP 0x0D04

CC 只把 E0 换成 D0、REF 交换、TSAP 原样回显(COTPConnection.java:196-213)。若远端 TSAP 不被接受,PLC 回 REJECT(0x50,EPduType.java:62)或直接 RST;HslCommunication 表现为"第一层通信的初始化"失败(SiemensS7Net.java:229-230),iot-communication 抛 The connection request was denied(service/PLCNetwork.java:187-190)。


5. TSAP 的奥秘(连接失败第一嫌疑)

5.1 构成

TSAP 是 COTP 标识端点的 2 字节值,S7comm 赋予它业务含义:

远端 TSAP(DST-TSAP,写在 0xC2 参数项)
  +----------------+-------------------+
  | byte0          | byte1             |
  | ConnectionType | (rack << 5) | slot|
  +----------------+-------------------+
   0x01 = PG         高 3 位 rack,低 5 位 slot
   0x02 = OP
   0x03 = S7 单边(服务器模式)
   0x10+ = S7 双边通信

源码:SiemensS7Net.java:112 与 :133 —— plcHead1[21] = (byte)((this.plc_rack * 0x20) + this.plc_slot);(0x20 = 1<<5);byte0 在 plcHead1[20],注释给出 PG=0x01、OP=0x02、S7Basic=0x03..0x10(SiemensS7Net.java:138-157)。

5.2 机型对照表

机型 rack/slot HSL 远端 TSAP(0xC2) HSL 本地 TSAP(0xC1) iot remote iot local
S7-1200 0 / 0 0x0100 0x0102 0x0300 + rack*0x20 + slot 0x0100
S7-1500 0 / 0 0x0100 0x0102 0x0300 + slot 0x0100
S7-300 0 / 2 0x0102 0x0102 0x0302 0x0100
S7-400 0 / 3 0x0103 0x0100 0x0303 0x0100
S7-200 — 0x4D57("MW") 0x4D57 0x4D57 0x4D57
S7-200Smart — 0x0300 0x1000 0x0300 0x1000
SINUMERIK 828D — — — 0x0D04 0x0400

源码依据:

5.3 为什么两个库不一样

两者用了不同 ConnectionType:HSL 用 0x01(PG),iot 用 0x03(S7 单边)。PLCNetwork.java:161 注释写明:

经测试:S1200 支持 0x0100、0x0200、0x0300,S200Smart 支持 0x0200、0x0300

即 1200/1500 通常忽略 slot 位,只要 byte0 ∈ {01,02,03} 即可;300/400 严格校验 rack/slot——这就是"S7-300 是 (0,2)、S7-400 是 (0,3)"的由来(硬件槽位约定:300 的 CPU 在第 2 槽,400 在第 3 槽)。

5.4 排查动作

先用 TIA Portal("在线和诊断"或硬件配置)确认 CPU 的机架/槽号;连不上先换 ConnectionType 试(0x0100 → 0x0200 → 0x0300);S7-200Smart 的 local 建议 0x1000、remote 只能取 0x0200/0x0201/0x0300/0x0301(PLCNetwork.java:171);抓包看 CC:回 D0 即通过,回 50 或无响应必是 TSAP。


6. S7comm PDU:Header + Parameter + Data

6.1 结构图

+--------------------------------------------------------+
| Header(请求 10 B / 响应 12 B)                         |
|  0     Protocol ID = 0x32                              |
|  1     ROSCTR: 01=Job 02=Ack 03=AckData 07=UserData     |
|  2-3   Reserved (Redundancy Identification) = 0x0000    |
|  4-5   PDU Reference(大端,递增,响应回显)            |
|  6-7   Parameter Length(大端)  8-9 Data Length(大端)|
|  10    Error Class(仅响应)  10-11 Error Code(仅响应)|
+--------------------------------------------------------+
| Parameter  0: Function(04 读 / 05 写 / F0 协商)       |
|             1: Item Count   2..: n 个 12 字节 S7ANY     |
+--------------------------------------------------------+
| Data   读响应每 item = ReturnCode+TransportSize+Length+数据|
|        写请求每 item = 00+TransportSize+Length+数据      |
+--------------------------------------------------------+

6.2 Header 逐字节表(请求)

偏移 字节数 示例值 含义
0 1 0x32 Protocol ID,恒 0x32
1 1 0x01 ROSCTR:01=Job、02=Ack、03=AckData、07=UserData(enums/EMessageType.java:42-60)
2-3 2 00 00 Reserved / Redundancy Identification
4-5 2 00 01 PDU Reference,大端,每次传输递增
6-7 2 00 0E Parameter Length(示例 14 = 2 + 12×1)
8-9 2 00 00 Data Length(读请求 0;写请求 = 4 + 数据长度,每 item)

源码:model/Header.java:55-95 与 toByteArray()(:118-127);PDU Reference 自增见 :103-110。HSL 侧对应 SiemensS7Net.java:920-945(_PLCCommand[7..16])。

6.3 响应头 AckHeader(12 字节)

偏移 字节数 示例值 含义
0 1 0x32 Protocol ID
1 1 0x03 AckData
2-3 2 00 00 Reserved(回显)
4-5 2 00 01 PDU Reference(回显请求值)
6-7 2 00 02 Parameter Length
8-9 2 00 0C Data Length
10 1 0x00 Error Class
10-11 2 00 00 Error Code(源码按 2 字节读,真实错误码 = class + code)

源码:model/AckHeader.java:45、53-61、83-98、109-120。错误类(enums/EErrorClass.java:43-85):

值 含义 值 含义
0x00 没有错误 0x84 服务处理中错误
0x81 应用关系错误 0x85 请求(supplies)错误
0x82 对象定义错误 0x87 访问错误
0x83 没有可用资源 0xD2 下载错误

关键:读一个不存在的 DB 不会在 Header 报错,而是 Header Error Class = 0x00,错误放在 Data 区每个 item 的 Return Code 里(§10)。Header 级错误通常只在 PDU 结构本身有问题时出现。


7. Setup Communication(0xF0):PDU 协商与两次握手

7.1 请求帧(HSL plcHead2,SiemensS7Net.java:837-841)

private byte[] plcHead2 = new byte[] {
        0x03, 0x00, 0x00, 0x19, 0x02, (byte) 0xF0, (byte) 0x80, 0x32, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00,
        (byte) 0xF0, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, (byte) 0xE0 };
偏移 字节数 值 含义
0-3 4 03 00 00 19 TPKT 长度 25
4-6 3 02 F0 80 COTP DT
7-16 10 32 01 00 00 00 04 00 08 00 00 Header:Job、PDU Ref=4、ParamLen=8、DataLen=0
17 1 F0 Function = Setup Communication(EFunctionCode.java:99)
18 1 00 Reserved
19-20 2 00 01 MaxAmQ calling(主叫 Ack 队列)= 1
21-22 2 00 01 MaxAmQ called(被叫 Ack 队列)= 1
23-24 2 01 E0 本端建议 PDU length = 480

字段定义:model/SetupComParameter.java:53-77,createDefault(pduLength) 在 :119-128。MaxAmQ = 1 即"同一时刻只允许 1 个未确认 Job",这是必须串行收发的协议依据。

7.2 响应帧(真实抓包 S7PLCMachine1Test.java:49)

03 00 00 1B 02 F0 80 32 03 00 00 00 00 00 08 00 00 00 00 F0 00 00 01 00 01 00 F0
偏移 字节数 值 含义
0-3 4 03 00 00 1B TPKT 长度 27
4-6 3 02 F0 80 COTP DT
7-18 12 32 03 00 00 00 00 00 08 00 00 00 00 AckHeader:AckData、ParamLen=8、DataLen=0、无错误
19 1 F0 Setup Communication
20 1 00 Reserved
21-22 2 00 01 MaxAmQ calling = 1
23-24 2 00 01 MaxAmQ called = 1
25-26 2 00 F0 协商后 PDU = 240

7.3 协商结果的用法

HSL SiemensS7Net.java:237-238:pdu_length = TransUInt16(BytesArraySelectLast(read_second.Content, 2), 0) - 28; if (pdu_length < 200) pdu_length = 200; —— 取响应最后 2 字节(协商 PDU)减 28,最低 200(−28 的含义见 §13.1)。iot service/PLCNetwork.java:138 直接用 PLC 回的值覆盖本地配置(this.pduLength = this.connectDtData())。常见协商值(enums/EPlcType.java:34-40 为库侧建议值,实际以 PLC 回的为准):S7-200/200Smart/300 = 240、S7-400 = 480、S7-1200 = 240、S7-1500 = 240~960(型号/固件相关,需实测确认)。


8. 读请求构建:Function 0x04 与 S7ANY

结构:TPKT(4) + COTP(3) + Header(10) + Parameter(2 + 12n) + Data(0),Parameter = 04(Read Var)+ ItemCount + n 个 12 字节 item。

8.1 S7ANY item 逐字节表

item 内偏移 字节数 示例值 含义
0 1 0x12 Specification Type:变量规范,恒 0x12
1 1 0x0A Length of following = 10(后面还有 10 字节)
2 1 0x10 Syntax ID:0x10 = S7ANY(ESyntaxID.java:18)
3 1 0x02 Transport size(见下表)
4-5 2 00 08 Count:按 transport size 计数的个数(大端)
6-7 2 00 01 DB number(非 DB 区填 0x0000)
8 1 0x84 Area code(§9)
9-11 3 00 00 00 Address:位地址,大端 3 字节

Transport size(enums/EParamVariableType.java:41-126,常用项):

值 含义 值 含义
0x01 BIT 0x06 DWORD
0x02 BYTE(读写字节/字/双字的通用取值) 0x08 REAL
0x03 CHAR 0x0C S5TIME
0x04 WORD 0x1C / 0x1D COUNTER / TIMER
0x05 INT 0x1E / 0x1F IEC_TIMER / IEC_COUNTER

重要:HSL 在 BuildReadCommand 里几乎总是填 0x02(BYTE),长度以字节计;只有 C/T(0x1E/0x1F)和 AI/AQ(0x06/0x07)走特殊分支(SiemensS7Net.java:966-989)。即"读 4 个字"= count 8 + transport size 0x02,而不是 count 4 + WORD。这与直觉相反。

8.2 地址是"位地址"——本篇最关键的一条

Address(3 字节) = 字节地址 × 8 + 位序号      即 byteAddress << 3 | bitIndex

三处源码一致:

  1. model/RequestItem.java:128-134:buff.putBytes(IntegerUtil.toByteArray((this.byteAddress << 3) + this.bitAddress), 1);反向 :196-202 用 >>3 与 &0x07。
  2. HslCommunication/Core/Address/S7AddressData.java:96-110(CalculateAddressStarted):无小数点则 ×8,有则 整数×8 + 小数。
  3. SiemensS7Net.java:894-901 同名方法的相同实现。
地址 字节地址 位 3 字节位地址
M0.0 0 0 00 00 00
M100 100 0 00 03 20(800)
M100.6 100 6 00 03 26(806)
DB1.DBW2 2 0 00 00 10(16)
DB1.DBD4 4 0 00 00 20(32)
V100 100 0 00 03 20

T/C 例外:iot 对 S7_TIMERS/S7_COUNTERS 用 byteAddress << 2(RequestItem.java:128-130),HSL 的 SplitS7Address 对 C/T 推进 alreadyFinished / 2(SiemensS7Helper.java:257-258)。该差异需实测确认。

8.3 构建代码(SiemensS7Net.java:910-1002 摘要)

if (s7Addresses.length > 19) throw new RuntimeException(... SiemensReadLengthCannotLargerThan19());
byte[] _PLCCommand = new byte[19 + readCount * 12];      // 19 = 4+3+10+2
_PLCCommand[7] = 0x32;  _PLCCommand[8] = 0x01;           // 协议标识 / Job
_PLCCommand[11] = BitConverter.GetBytes(msgId)[1];        // PDU reference 大端
_PLCCommand[12] = BitConverter.GetBytes(msgId)[0];
_PLCCommand[13] = (byte)((_PLCCommand.length - 17) / 256);  // 参数长度 = 2 + 12n
_PLCCommand[14] = (byte)((_PLCCommand.length - 17) % 256);
_PLCCommand[17] = 0x04;  _PLCCommand[18] = (byte) readCount;   // Read Var + item 数
for (int ii = 0; ii < readCount; ii++) {
    int b = 19 + ii * 12;
    _PLCCommand[b] = 0x12;  _PLCCommand[b+1] = 0x0A;  _PLCCommand[b+2] = 0x10;
    _PLCCommand[b+3] = 0x02;                                        // transport size BYTE
    _PLCCommand[b+4] = (byte)(addr.getLength() / 256);  _PLCCommand[b+5] = (byte)(addr.getLength() % 256);
    _PLCCommand[b+6] = (byte)(addr.getDbBlock() / 256); _PLCCommand[b+7] = (byte)(addr.getDbBlock() % 256);
    _PLCCommand[b+8] = (byte) addr.getDataCode();                   // area
    _PLCCommand[b+9] = (byte)(addr.getAddressStart() / 65536 % 256); // 位地址 3 字节大端
    _PLCCommand[b+10] = (byte)(addr.getAddressStart() / 256 % 256);
    _PLCCommand[b+11] = (byte)(addr.getAddressStart() % 256);
}

9. 区域码(Area code)对照表

以 iot-communication/.../s7/enums/EArea.java:42-117 为准:

值 缩写 含义 HSL 地址前缀(S7AddressData.java)
0x03 — 200 系列系统信息 未使用
0x05 SM 200 系列系统标志 SM(:135-142)
0x06 AI 200 系列模拟量输入 AI(:144-149)
0x07 AQ 200 系列模拟量输出 AQ(:150-155)
0x80 PI/PQ 直接访问外设 P(:156-166)
0x81 I 输入映像区 I(:167-172)
0x82 Q 输出映像区 Q(:173-178)
0x83 M 内部标志位 M(:179-184)
0x84 DB 数据块 D / DB(:185-196)
0x85 DI 背景数据块 源码未实现(需实测确认)
0x86 L 局部变量 源码未实现(需实测确认)
0x87 V 全局变量(V 区) 见下方说明
0x1C C S7 计数器 —
0x1D T S7 定时器 —
0x1E — IEC 计数器(200 系列) C(:200-202)
0x1F — IEC 定时器(200 系列) T(:197-199)

9.1 两个必须说清的差异

(a)V 区 = DB1(S7AddressData.java:203-205):address.charAt(0)=='V' 时 DataCode = 0x84(按 DB 区处理)、DbBlock = 1(DB 号固定 1)。即 V100 ≡ DB1.DBB100,报文里 area = 0x84、DB = 0x0001。iot 把 0x87(UNKNOWN_YET)标为 "V",与 HSL 的 DB1 方案不同,真机行为需实测确认。

(b)T/C 有两套码:iot 用 0x1D(T)/0x1C(C),HSL 用 0x1F(T)/0x1E(C)。1200/1500 上读写 T/C 需实测确认;HSL 对 0x1E/0x1F 有专门的 transport size 与长度换算分支(SiemensS7Net.java:966-972、:1319-1322)。


10. 读响应解析与返回码

10.1 结构

TPKT(4) + COTP(3) + AckHeader(12) + Parameter(2) + Data
                                     |-- 04(回显)+ ItemCount
Data 区每 item:
  +-----------+---------------+---------------+------------------+
  | ReturnCode| TransportSize | Length (bits) | Data             |
  | 1 B       | 1 B           | 2 B 大端      | Length/8 字节     |
  +-----------+---------------+---------------+------------------+

数据区 TransportSize 用另一套枚举(enums/EDataVariableType.java:42-72):

值 含义 Length 字段
0x00 NULL(出错时常见) —
0x03 BIT 位(不乘 8)
0x04 BYTE_WORD_DWORD 位(= 字节数 × 8)
0x05 INTEGER 位
0x06 / 0x07 / 0x09 DINTEGER / REAL / OCTET_STRING 位(源码按原值解析)

源码:model/DataItem.java:54-98(序列化时 BYTE_WORD_DWORD/INTEGER 写 count*8,其余写 count),反序列化 :122-148 对称。

10.2 Return Code 完整表

来源 enums/EReturnCode.java:42-77;HSL 对 0x05/0x06/0x0A 有专门分支(SiemensS7Net.java:1351-1360、:1377-1388)。

值 含义 HSL 处理
0x00 预留 / 未定义 —
0x01 硬件错误 —
0x03 对象不允许访问 —
0x05 地址无效,超出 PLC 极限 报 "SiemensReadLengthOverPlcAssign"
0x06 数据类型不支持 报 "SiemensError0006"
0x07 数据类型不一致 —
0x0A 对象不存在(DB 不存在 / 越界) 报 "SiemensError000A"
0xFF 成功 正常解析数据

10.3 解析代码(SiemensS7Helper.java:81-116,与 SiemensS7Net.java:1315-1367 相同)

if (content.length >= 21) {
    for (int i = 21; i < content.length - 1; i++) {
        if (content[i] == (byte) 0xFF && content[i + 1] == 0x04) {        // 成功 + 字节数据
            int count = (content[i + 2] * 256 + content[i + 3]) / 8;      // 位长度 -> 字节数
            Utilities.ArrayListAddArray(list, SoftBasic.BytesArraySelectMiddle(content, i + 4, count));
            i += count + 3;
        } else if (content[i] == (byte) 0xFF && content[i + 1] == 0x09) { // 成功 + OCTET_STRING(T/C)
            int count = content[i + 2] * 256 + content[i + 3];  ... /* 按 3 或 5 字节一组剥离 */ i += count + 4;
        } else if (content[i] == 0x05 && content[i + 1] == 0x00) return ...;  // 地址无效
        else if (content[i] == 0x06 && content[i + 1] == 0x00) return ...;    // 类型不支持
        else if (content[i] == 0x0A && content[i + 1] == 0x00) return ...;    // 对象不存在
    }
}

为什么从下标 21 开始:4(TPKT) + 3(COTP) + 12(AckHeader) + 2(Parameter) = 21,Data 区正好从第 21 字节起。读位响应判 content[21]==0xFF && content[22]==0x03,数据取 content[25](SiemensS7Net.java:1369-1395)。


11. 写请求:Function 0x05、写位与写字

结构:TPKT(4) + COTP(3) + Header(10) + Parameter(2 + 12n) + Data(Σ(4 + 长度));Data 区每项 = 00 + TransportSize + Length(bits) + Data。

11.1 写字 vs 写位

源码:SiemensS7Net.java:1094-1164(BuildWriteByteCommand)与 :1174-1245(BuildWriteBitCommand)。

字段 写字/字节 写位
item transport size(偏移 22) 0x02(BYTE) 0x01(BIT)
item count(偏移 23-24) 数据字节数 0x0001
Data 区 TransportSize(偏移 32) 0x04 0x03(计数器区 0x1C 时用 0x09)
Data 区 Length(偏移 33-34) 数据长度 × 8(位) 1(位,不乘 8)
Data 区数据 原始字节 0x01 / 0x00

关键源码行(写字 :1154-1159 / 写位 :1227-1240):

// 写字
_PLCCommand[31] = 0x00;  _PLCCommand[32] = 0x04;                            // 按字写入
_PLCCommand[33] = (byte)(data.length * 8 / 256);  _PLCCommand[34] = (byte)(data.length * 8 % 256);
// 写位
if (analysis.Content.getDataCode() == 0x1C) { _PLCCommand[31] = 0x00; _PLCCommand[32] = 0x09; }
else                                        { _PLCCommand[31] = 0x00; _PLCCommand[32] = 0x03; }
_PLCCommand[33] = (byte)(buffer.length / 256);  _PLCCommand[34] = (byte)(buffer.length % 256);

11.2 写位要不要 Read-Modify-Write?

单个位不需要(S7 有原生位写,transport size 0x01);位数组需要。HSL 佐证:

OperateResultExOne<byte[]> read = Read( new S7AddressData[] { analysis.Content } );   // 先读整段
boolean[] boolArray = SoftBasic.ByteToBoolArray(read.Content);
System.arraycopy( values, 0, boolArray, offset, values.length );                      // 改位
return Write( analysis.Content, SoftBasic.BoolArrayToByte( boolArray ) );              // 写回

原因:物理最小可写单位是字节,位写指令一次只能写 1 位。改多个位必须自己保证同字节其他位不被破坏,代价是非原子,并发下需串行化。

11.3 写多个不连续地址

一次 Write Var 可带 n 个 item(地址可不连续)。SiemensS7Net.java:1247-1291:

ms.WriteByte( BitConverter.GetBytes( s7Address.length * 12 + 2 )[1] );   // Parameter length
...
for (int i = 0; i < data.size(); i++) {
    ms.WriteByte( 0x00 );                                            // return code
    ms.WriteByte( 0x04 );                                            // bit: 0x03    byte: 0x04
    ms.WriteByte( BitConverter.GetBytes( data.get(i).length * 8 )[1] );   // 位长度
    ms.WriteByte( BitConverter.GetBytes( data.get(i).length * 8 )[0] );
    ms.Write( data.get(i) );
    if ( i < data.size() - 1 && (data.get(i).length % 2 == 1)) ms.WriteByte( 0x00 );  // 奇数长度补 1 字节
}

奇数长度补零:非最后一项且长度为奇数时补 1 个 0x00(第 1277 行),解析侧同样处理(SiemensS7Server.java:432)。手写报文最容易漏这条。

11.4 写响应

写响应每个 item 回一个 ReturnCode,0xFF = 成功。源码注释里的两条真实抓包(SiemensS7Net.java:409-410):

03 00 00 1A 02 F0 80 32 03 00 00 00 01 00 02 00 05 00 00 04 01 FF 03 00 01 01     // 位写响应
03 00 00 16 02 F0 80 32 03 00 00 00 01 00 02 00 01 00 00 05 01 FF                 // 字节/字写响应

服务端构造模板(SiemensS7Server.java:400):03 00 00 16 02 F0 80 32 03 00 00 00 01 00 02 00 01 00 00 05 01 + status 字节。


12. 地址语法与解析代码

以 HslCommunication/Core/Address/S7AddressData.java:130-220 的 ParseFrom() 为准:

写法 DataCode(area) DB 起始地址计算 示例位地址
I0.0 / I0 / IB0 / IW0 / ID0 0x81 0 ×8 + bit I0.0 → 0
Q0.0 / QB0 / QW0 / QD0 0x82 0 ×8 + bit Q0.0 → 0
M0.0 / M100 / MB10 / MW10 / MD10 0x83 0 ×8 + bit MW10 → 80
DB1.DBX0.0 0x84 1 去前缀后 ×8 + bit 0
DB1.DBB0 / DB1.DBW2 / DB1.DBD4 0x84 1 同上 0 / 16 / 32
DB1.100(简写,字节地址) 0x84 1 100 × 8 800
V100 / VB100 / VW100 / VD100 0x84 1 ×8 + bit V100 → 800
T0 0x1F 0 不乘 8(isCT) 0
C0 0x1E 0 不乘 8 0
SM0.0 / AI0 / AQ0 / PIW100 0x05 / 0x06 / 0x07 / 0x80 0 ×8 + bit —

解析核心(:185-211 摘要):

} else if (address.charAt(0) == 'D' || address.substring(0,2).equals("DB")) {   // :185  DB/D 开头
    addressData.DataCode = 0x84;                                                // :186
    String[] adds = address.split("\\.");
    if (address.charAt(1) == 'B') addressData.DbBlock = Integer.parseInt(adds[0].substring(2));  // :189 "DB1"->1
    else                          addressData.DbBlock = Integer.parseInt(adds[0].substring(1));  // :191 "D1"->1
    String addTemp = address.substring( address.indexOf( '.' ) + 1 );           // :193  取 "." 之后
    if (addTemp.startsWith("DBX")||addTemp.startsWith("DBB")||
        addTemp.startsWith("DBW")||addTemp.startsWith("DBD")) addTemp = addTemp.substring(3);   // :194-195
    addressData.setAddressStart(CalculateAddressStarted(addTemp, false));       // :196
} else if (address.charAt(0) == 'T') { addressData.DataCode = 0x1F; ... isCT=true }   // :197-199
  else if (address.charAt(0) == 'C') { addressData.DataCode = 0x1E; ... isCT=true }   // :200-202
  else if (address.charAt(0) == 'V') { addressData.DataCode = 0x84; addressData.DbBlock = 1; }  // :203-205

数值计算(:96-110):

public static int CalculateAddressStarted( String address, boolean isCT ) {
    if (address.indexOf( '.' ) < 0) {
        if (isCT) return Integer.parseInt( address );        // T/C 不乘 8
        else      return Integer.parseInt( address ) * 8;
    } else {
        String[] temp = address.split( "\\." );
        return Integer.parseInt( temp[0] ) * 8 + Integer.parseInt( temp[1] );
    }
}

平台落地建议:点位表的地址字符串在配置加载时解析成 (area, dbNumber, byteAddress, bitAddress, lengthInBytes) 五元组缓存(即 S7AddressData),运行时只组包,避免每次采集都做字符串切分。


13. PDU 长度限制与自动分包

13.1 单次读取上限

一帧读请求 = Header(12) + Parameter(2 + 12n) + Data(Σ(4 + len_i)),故单个 item 的最大读取字节数为 maxLen = PDU - 12(Header) - 12(参数区 item) - 4(数据项头) = PDU - 28,这正是 SiemensS7Net.java:237 的 - 28;PDU=240 → 212,与 if (pdu_length < 200) pdu_length = 200; 量级吻合。其他库(snap7 系)常用 PDU - 18(不扣请求侧 item 的 12 字节)得 222,差 10 字节,建议先用保守的 −28再按机型实测放宽。写方向 SiemensS7Net.java:510 用固定值 Math.min(length - alreadyFinished, 200),未使用协商出的 pdu_length(已知保守实现)。

13.2 单次最多 19 个 item

源码两处硬限制:SiemensS7Net.java:912(> 19 直接抛异常)与 SiemensS7Helper.java:62(tmpArray.size() >= 19)。19 是工程经验值,不是协议上限(item count 字段为 1 字节)。真实约束仍是 PDU:

2 + 12n (Parameter) + 4n + Σlen (Data) <= PDU - 12  =>  16n + Σlen <= 226 (PDU=240)

n=19 时 16×19 = 304 已超 226(Σlen 只能为 0),所以 19 只在 PDU 较大(400 的 480、1500 的 960)时才有意义;1200 上实际可用个数远小于 19(需实测确认)。保守做法:按 n <= (PDU - 28 - Σlen) / 16 动态计算。

13.3 超长地址自动切片

单地址超长:SiemensS7Helper.java:248-270 的 SplitS7Address()(摘要):

while (alreadyFinished < length) {
    int readLength = Math.min(length - alreadyFinished, pduLength);
    S7AddressData addressData = new S7AddressData(s7Address);
    // C/T(0x1E/0x1F) 推进 alreadyFinished/2;其余区域推进 alreadyFinished * 8(位地址!必须 ×8)
    addressData.setAddressStart(s7Address.getAddressStart() + (isCT ? alreadyFinished / 2 : alreadyFinished * 8));
    addressData.setLength(readLength);
    array.Add(addressData);  alreadyFinished += readLength;
}

关键:切片后地址推进量是 已读字节数 × 8(因为 S7ANY 存位地址)。手写分包漏掉 ×8 的典型表现是"第一包对、后面全错"。

多地址分组:SiemensS7Helper.java:56-74 的 ArraySplitByLength():累计 tmpArray.size() >= 19 或 lengthTotle + len >= pduLength 时切一组。组合流程见 SiemensS7Net.java:343-365:先分组,组内单地址超长再切片,最后按序拼接结果。

iot 的做法:algorithm/S7SequentialGroupAlg.java 的 readRecombination(src, targetSize, extraSize, threshold) / writeRecombination(...) 把"每个 item 额外占用 extraSize 字节"显式建模并按 targetSize 贪心切分(注释含完整示例,:44-49、:84-92),比 HSL 的两级切分更精细,可借鉴。

13.4 字节序

S7 是大端。HSL 用 ReverseBytesTransform(SiemensS7Net.java:63)。读上来的 Int16/Int32/Float 必须按大端解析;按 Java 默认小端解析会得到荒谬值。多帧结果按 item 顺序直接拼接即可。


14. 完整报文实例(5 例)

例 1 的 CR/Setup 请求直接引用源码常量数组;例 1 的 CC/Setup 响应、例 5 的写响应来自仓库内真实抓包注释。例 2~4 的请求由源码构造逻辑逐字段推导(字段与源码行一一对应),响应按协议模型推导,实际返回值以实测为准。

例 1:COTP 握手 + S7 协商完整字节流

1a. CR(客户端 → PLC,S7-1200),源码 SiemensS7Net.java:832-836

03 00 00 16 11 E0 00 00 00 01 00 C0 01 0A C1 02 01 02 C2 02 01 00
偏移 字节数 值 含义
0-3 4 03 00 00 16 TPKT:版本 03、保留 00、长度 22(含自身)
4-9 6 11 E0 00 00 00 01 COTP 长度 17(不含本字节)、PDU 类型 CR、DST-REF=0x0000、SRC-REF=0x0001
10-13 4 00 C0 01 0A flags + TPDU-Size 参数(码 C0、长 01、值 0A = 1024)
14-17 4 C1 02 01 02 SRC-TSAP 参数(码 C1)= 本地 0x0102
18-21 4 C2 02 01 00 DST-TSAP 参数(码 C2)= 远端 0x0100(PG + rack0/slot0)

1b. CC(PLC → 客户端),真实抓包 S7PLCMachine1Test.java:47

03 00 00 16 11 D0 00 01 00 01 00 C0 01 0A C1 02 04 00 C2 02 0D 04
偏移 字节数 值 含义
0-3 4 03 00 00 16 TPKT 长度 22
4-9 6 11 D0 00 01 00 01 COTP 长度 17 + PDU 类型 CC + DST-REF/SRC-REF(回显并交换)
10-13 4 00 C0 01 0A flags + TPDU-Size = 1024(协商通过)
14-21 8 C1 02 04 00 C2 02 0D 04 回显本地 TSAP 0x0400、远端 TSAP 0x0D04(828D 真机;1200 上应为你发送的值)

1c. Setup Communication 请求(SiemensS7Net.java:837-841,本端建议 PDU=480;iot 真机版本见 S7PLCMachine1Test.java:48,建议值 00 F0=240)

03 00 00 19 02 F0 80 32 01 00 00 00 04 00 08 00 00 F0 00 00 01 00 01 01 E0
偏移 字节数 值 含义
0-6 7 03 00 00 19 02 F0 80 TPKT 长度 25 + COTP DT
7-16 10 32 01 00 00 00 04 00 08 00 00 Header:Job、PDU Ref=4、ParamLen=8、DataLen=0
17-22 6 F0 00 00 01 00 01 Setup Communication + Reserved + MaxAmQ calling=1 + MaxAmQ called=1
23-24 2 01 E0 建议 PDU = 480

1d. Setup Communication 响应(真实抓包 S7PLCMachine1Test.java:49)

03 00 00 1B 02 F0 80 32 03 00 00 00 00 00 08 00 00 00 00 F0 00 00 01 00 01 00 F0
偏移 字节数 值 含义
0-6 7 03 00 00 1B 02 F0 80 TPKT 长度 27 + COTP DT
7-18 12 32 03 00 00 00 00 00 08 00 00 00 00 AckHeader:AckData、ParamLen=8、DataLen=0、无错误
19-24 6 F0 00 00 01 00 01 Setup Communication + Reserved + MaxAmQ calling/called = 1
25-26 2 00 F0 协商 PDU = 240 → 单次读取上限 240 − 28 = 212 字节

例 2:读 DB1.DBW0 起的 4 个字(8 字节)

请求(由 BuildReadCommand,SiemensS7Net.java:910-1002;PDU Ref = 0x0001):

03 00 00 1F 02 F0 80 32 01 00 00 00 01 00 0E 00 00 04 01 12 0A 10 02 00 08 00 01 84 00 00 00
偏移 字节数 值 含义
0-6 7 03 00 00 1F 02 F0 80 TPKT 长度 31 + COTP DT
7-18 12 32 01 00 00 00 01 00 0E 00 00 04 01 Header(Job、PDU Ref=1、ParamLen=14、DataLen=0)+ Read Var + ItemCount=1
19-24 6 12 0A 10 02 00 08 S7ANY 头(12 0A 10)+ transport size BYTE + Count = 8 字节 = 4 个字
25-30 6 00 01 84 00 00 00 DB number = 1、Area = DB(0x84)、位地址 0(DBW0)

响应(模型推导,读到 0x0001/0x0002/0x0003/0x0004):

03 00 00 21 02 F0 80 32 03 00 00 00 01 00 02 00 0C 00 00 04 01 FF 04 00 40 00 01 00 02 00 03 00 04
偏移 字节数 值 含义
0-6 7 03 00 00 21 02 F0 80 TPKT 长度 33 + COTP DT
7-20 14 32 03 00 00 00 01 00 02 00 0C 00 00 04 01 AckHeader(ParamLen=2、DataLen=12、无错误)+ Read Var + ItemCount=1
21-24 4 FF 04 00 40 Success + BYTE_WORD_DWORD + Length = 64 位 = 8 字节
25-32 8 00 01 00 02 00 03 00 04 数据(大端 4 个字)

例 3:读 M0.0 位

请求(由 BuildBitReadCommand,SiemensS7Net.java:1011-1067;PDU Ref = 0x0002):

03 00 00 1F 02 F0 80 32 01 00 00 00 02 00 0E 00 00 04 01 12 0A 10 01 00 01 00 00 83 00 00 00
偏移 字节数 值 含义
0-6 7 03 00 00 1F 02 F0 80 TPKT 长度 31 + COTP DT
7-18 12 32 01 00 00 00 02 00 0E 00 00 04 01 Header(Job、PDU Ref=2、ParamLen=14)+ Read Var + ItemCount=1
19-24 6 12 0A 10 01 00 01 S7ANY 头 + transport size = BIT + Count = 1(1 个位)
25-30 6 00 00 83 00 00 00 DB number = 0、Area = M(0x83)、位地址 0(M0.0)

响应(模型推导,值为 1):

03 00 00 1A 02 F0 80 32 03 00 00 00 02 00 02 00 05 00 00 04 01 FF 03 00 01 01
偏移 字节数 值 含义
0-6 7 03 00 00 1A 02 F0 80 TPKT 长度 26 + COTP DT
7-20 14 32 03 00 00 00 02 00 02 00 05 00 00 04 01 AckHeader(ParamLen=2、DataLen=5、无错误)+ Read Var + ItemCount=1
21-24 4 FF 03 00 01 Success + BIT + Length = 1(位,不除 8)
25 1 01 数据 = 1(true)

与解析代码吻合:SiemensS7Net.java:1374-1376 判 content[21]==0xFF && content[22]==0x03 后取 content[25]。

例 4:写 DB1.DBW0 = 0x1234

请求(由 BuildWriteByteCommand,SiemensS7Net.java:1094-1164;PDU Ref = 0x0003):

03 00 00 25 02 F0 80 32 01 00 00 00 03 00 0E 00 06 05 01 12 0A 10 02 00 02 00 01 84 00 00 00 00 04 00 10 12 34
偏移 字节数 值 含义
0-6 7 03 00 00 25 02 F0 80 TPKT 长度 37(35 + 2)+ COTP DT
7-18 12 32 01 00 00 00 03 00 0E 00 06 05 01 Header(ParamLen=14、DataLen=6 = 4+2)+ Write Var + ItemCount=1
19-24 6 12 0A 10 02 00 02 S7ANY 头 + transport size BYTE + Count = 2 字节
25-30 6 00 01 84 00 00 00 DB number = 1、Area = DB、位地址 0
31-34 4 00 04 00 10 Data 区:Return Code 0 + TransportSize 0x04(按字写入)+ Length = 16 位
35-36 2 12 34 数据(大端)

响应(真实抓包,SiemensS7Net.java:410):

03 00 00 16 02 F0 80 32 03 00 00 00 01 00 02 00 01 00 00 05 01 FF
偏移 字节数 值 含义
0-6 7 03 00 00 16 02 F0 80 TPKT 长度 22 + COTP DT
7-20 14 32 03 00 00 00 01 00 02 00 01 00 00 05 01 AckHeader(ParamLen=2、DataLen=1、无错误)+ Write Var + ItemCount=1
21 1 FF Return Code = Success

多 item 写入真实样本(3 个 item,来自 SiemensS7Server.java:410 注释):

03 00 00 49 02 F0 80 32 01 00 00 00 01 00 26 00 12 05 03
12 0A 10 02 00 01 00 00 83 00 03 20
12 0A 10 02 00 01 00 00 83 00 06 40
12 0A 10 02 00 01 00 01 84 00 00 00
00 04 00 10 00 01 00 04 00 10 00 02 00 04 00 10 00 03
偏移 字节数 值 含义
0-3, 13-18 10 03 00 00 49 / 00 26 00 12 05 03 TPKT 长度 73(= 19 + 3×12 + 3×6);ParamLen=38、DataLen=18、Write Var、ItemCount=3
19-30 12 12 0A 10 02 00 01 00 00 83 00 03 20 item1:BYTE×1、DB0、M、位地址 0x000320 = 800 → M100
31-42 12 12 0A 10 02 00 01 00 00 83 00 06 40 item2:M、位地址 0x000640 = 1600 → M200
43-54 12 12 0A 10 02 00 01 00 01 84 00 00 00 item3:BYTE×1、DB1、位地址 0 → DB1
55-60 6 00 04 00 10 00 01 data1:00 + 0x04 + 16 位 + 00 01

注:该样本 item 的 count 为 1 字节,但 Data 区位长度写 00 10(16 位 = 2 字节),两者不一致,疑为源库按 2 字节写入所致,具体需实测确认。此处引用只为展示多 item 组帧形态。

例 5:错误响应 —— 读取不存在的 DB100

请求(例 2 的 DB number 改成 100 = 0x0064,PDU Ref = 0x0004):

03 00 00 1F 02 F0 80 32 01 00 00 00 04 00 0E 00 00 04 01 12 0A 10 02 00 08 00 64 84 00 00 00
偏移 字节数 值 含义
25-26 2 00 64 DB number = 100(该 DB 不存在)
27 1 84 Area = DB

响应(按协议模型推导,Return Code = 0x0A):

03 00 00 19 02 F0 80 32 03 00 00 00 04 00 02 00 04 00 00 04 01 0A 00 00 00
偏移 字节数 值 含义
0-6 7 03 00 00 19 02 F0 80 TPKT 长度 25 + COTP DT
7-20 14 32 03 00 00 00 04 00 02 00 04 00 00 04 01 AckHeader(ParamLen=2、DataLen=4、ErrorClass=0x00)+ Read Var + ItemCount=1
21-24 4 0A 00 00 00 Return Code = 0x0A(DB 不存在) + TransportSize NULL + Length = 0

判定依据:SiemensS7Net.java:1357-1359 / SiemensS7Helper.java:106-107 检测 content[i]==0x0A && content[i+1]==0x00 并报 SiemensError000A。PLC 实际返回的 DataLength 可能不同(如 2 字节),需实测确认。


15. Fetch/Write 协议与 S7comm 对比

15.1 报文结构(SiemensFetchWriteNet.java:246-286)

偏移: 0    1    2    3    4    5        6    7    8      9     10-11   12-13   14   15   16..
值  : 53   35   10   01   03   05/03    03   08   区域码  DB号  地址    长度    FF   02   数据(写)
      'S'  '5'  固定 固定 固定 读05/写03 固定 固定
偏移 字节数 示例 含义
0-1 2 53 35 魔数 "S5"
2-4 3 10 01 03 固定
5 1 05 / 03 读 = 0x05,写 = 0x03
6-7 2 03 08 固定
8 1 01 区域码(见下)
9 1 01 DB 号
10-11 2 00 00 字节地址(注意不是位地址)
12-13 2 00 02 长度(DB/C/T 按字数,其余按字节)
14-15 2 FF 02 固定结束符
16.. n — 写请求时追加数据

Fetch/Write 的区域码是另一套(SiemensFetchWriteNet.java:192-236):0x01=DB、0x02=M、0x03=I、0x04=Q、0x06=C、0x07=T。

15.2 对比

维度 S7comm Fetch/Write
报文头 TPKT(4)+COTP(3)+S7 Header(10/12) 16 字节固定头("S5"),无 TPKT/COTP
握手 必须两次(COTP CR/CC + S7 Setup) 无握手,TCP 连上直接发
长度分帧 TPKT 第 3-4 字节 头里无长度字段,靠请求内容反推(Core/IMessage/FetchWriteMessage.java:8-25)
地址单位 位地址(字节×8+位) 字节地址(:176-183 不乘 8)
DB 号范围 0~65535 ≤ 255(:214-217 显式校验并报错)
支持区域 I/Q/M/DB/PI/PQ/T/C/V/AI/AQ/SM 仅 DB/M/I/Q/C/T
位操作 支持(transport size 0x01) 不支持,只能整字节读写
机型 300/400/1200/1500/200Smart 主要 300/400(需 CP 侧启用)
错误定位 Data 区 Return Code 响应第 8 字节(:65、:120)

结论:新项目一律用 S7comm;Fetch/Write 只在对接老 CP343/CP443 且已启用该服务时考虑。它"无握手"看似简单,但缺少长度字段反而让粘包处理更麻烦。


16. 现场配置要点

S7-1200 / 1500 必做三件事:① 开启 "PUT/GET 通信访问"(TIA → CPU 属性 → 防护与安全 → 连接机制 → 勾选)——不勾则 TCP 能连、握手能过但读写失败,这是"能连不能读"的第一原因;② DB 块取消"优化的块访问",否则无绝对偏移、S7comm 绝对寻址失效;③ 取消优化后重新编译下载,按 DB 的"偏移量"列填点位表。

连接数:S7-1200 通常 8 个(含 PG/HMI,实测可用 3~5 个,需实测确认);S7-1500 型号相关,常见 16~64;300/400 更紧张。设计要点:一个 PLC 只建一条 TCP 连接串行轮询所有点位;连接池设上限防止耗尽 PLC 资源(耗尽表现为新连接被拒或已有连接掉线);重连要退避。

TSAP 配置:1200/1500 默认 slot=0(HSL)或 1(iot)实测大多都能连,连不上按 §5.4 换 ConnectionType;300 必须 slot=2、400 必须 slot=3;200Smart local=0x1000、remote ∈ {0x0200,0x0201,0x0300,0x0301}。

其他:确认 IP/102 路由可达(工业现场常见 VLAN/防火墙拦截);S7 是大端;DATE 从 1990-01-01 起算(SiemensS7Net.java:824),STRING 前 2 字节是"最大长度+实际长度"(SiemensS7Helper.java:128-148),别当裸字符串读;轮询周期建议 100ms~1s,优先合并读(一次读一大段 DB 远快于读 20 个分散地址)。


17. 常见错误码与排查清单

17.1 传输层 / 握手层

现象 原因 动作
TCP 连不上 IP 错、防火墙、PLC 未上电、连接耗尽 ping + telnet ip 102;减少并发连接
CR 无响应 TPKT 长度写错、TSAP 非法 抓包核对长度 = 22;换 TSAP
收到 COTP 0x50 远端 TSAP 被拒 换 ConnectionType(01/02/03);检查 PLC 连接机制
Setup 响应 PDU 异常小 固件限制、连接类型不匹配 用响应最后 2 字节的实际值

17.2 Header Error Class

值 含义 值 含义
0x00 无错误(此时看 Data 区 Return Code) 0x84 服务处理中错误
0x81 应用关系错误 0x85 请求错误
0x82 对象定义错误 0x87 访问错误
0x83 资源不足 0xD2 下载错误

17.3 S7 错误码(2 字节 = ErrorClass + Code)

完整表见 iot-communication/.../s7/constant/ErrorCode.java:60-265,高频摘录:

错误码 含义 错误码 含义
0x0000 没有错误 0x8301 内存/工作内存不足或介质不可访问
0x0110 块号无效 0x8302 可用资源太少或处理器不可用
0x0111 请求长度无效 0x8304 无法进一步并行上传(资源瓶颈)
0x0112 参数无效 0x8401 无效的服务序列
0x0114 找不到块 0x8402 对象状态导致服务无法执行
0x0119 密码不正确 0x8500 S7 协议错误:帧错误
0x011A / 0x011B PG / PLC 资源错误 0x8701 寻址对象出错(如区域长度错误)
0x011C 协议错误 0x8702 模块不支持所请求的服务
0x0140 可用内存不足 0x8703 拒绝访问对象
0x8100 远程模块未知的服务 0x8704 对象已损坏
0x8204 对象类型规范不一致 0xD001 / 0xD015 / 0xD026 非法作业号 / 标签地址非法 / 非法访问类型

17.4 排查清单(按出现频率)

  1. 能连不能读(1200/1500) → 99% 是没勾 "PUT/GET 通信访问"。
  2. 读 DB 返回 0x0A → DB 号错 / DB 未下载 / DB 仍是"优化的块访问"。
  3. 值荒谬(超大数或 0) → 字节序错(应大端);或地址忘了 ×8。
  4. 长数据前面对、后面错 → 分包地址推进漏了 ×8。
  5. 握手失败 → TSAP(rack/slot / ConnectionType)。
  6. 返回 0x05 → 越界:起始地址 + 长度超出区域范围。
  7. 返回 0x06 → 类型不支持:transport size 与区域不匹配。
  8. 偶发掉线 → PLC 连接资源打满,或未串行化导致错帧。
  9. 写多 item 后数据错位 → 奇数长度项未补 1 字节对齐(§11.3)。
  10. 写位把相邻位清掉 → 用了整字节写;位数组必须 Read-Modify-Write。

18. 小结与现场坑清单

18.1 小结

18.2 常见误区 / 现场坑清单

# 误区 正确做法
1 以为 S7 地址是字节地址 是位地址:×8 + bit,3 字节大端
2 读 4 个字写 count=4 + WORD HSL 用 count=8 + BYTE(SiemensS7Net.java:966-989)
3 以为只有一次握手 必须两次:COTP CR/CC 和 S7 Setup
4 用本地配置的 PDU 以 Setup 响应最后 2 字节为准(PLCNetwork.java:138)
5 分包按字节推进地址 按 字节数 × 8 推进(SiemensS7Helper.java:260)
6 认为 19 个 item 任何机型都成立 19 是源码硬限;PDU 240 时实际个数远小于 19(需实测确认)
7 一个点位一条 TCP 连接 一条连接串行轮询;1200 连接资源通常只有 8 个,且 MaxAmQ=1 不可并发 Job
8 忘记勾 "PUT/GET 通信访问" 1200/1500 必勾,否则握手过但读写失败
9 DB 保持"优化的块访问" 必须取消,否则绝对寻址失效、读回 0x0A
10 按 Java 默认(小端)解析 S7 是大端
11 位数组直接整字节写 必须 Read-Modify-Write,否则覆盖同字节其他位
12 写多 item 忽略奇数长度对齐 非最后一项且长度奇数时补 1 个 0x00(:1277)
13 T/C 与 I/Q/M 一样按 ×8 算 C/T 走 isCT 分支不乘 8(S7AddressData.java:96-110);0x1C/0x1D 与 0x1E/0x1F 两套码需实测确认
14 把 V 区当独立区域码 HSL 实现为 DB1(S7AddressData.java:203-205);iot 标 0x87(需实测确认)
15 读位响应按 长度/8 取字节 FF 03 00 01 XX 中长度 1 = 1 位,直接取第 25 字节
16 用 Fetch/Write 当主力协议 DB ≤ 255、只支持 DB/M/I/Q/C/T、无长度字段,仅老项目兼容
17 忽视 TPKT 长度含自身 收包时 还需读 = N - 4(S7Message.java:14)
18 每次采集都重新解析地址串 配置加载时解析成 (area, db, byteAddr, bitAddr, len) 缓存

关于"需实测确认":文中标注该处均属源码未定义、两库实现不一致或依赖 PLC 型号/固件的行为。落地 FastBee 时建议先用 HslCommunication 自带的 S7 服务端(Profinet/Siemens/SiemensS7Server.java)做本地回环验证,再上真机抓包比对。


本文是《工业通信与数据采集》栏目第 8/22 篇 · 上一篇:Modbus 协议详解 | 栏目总览 | 下一篇:三菱 MC 协议(MELSEC Communication Protocol / SLMP)详解