02 · S7 协议(西门子 S7comm)详解
定位:面向有 Java 基础、正在做工业物联网平台(FastBee)数据采集层的工程师,把 S7comm 从"能连上"讲到"能手工拼报文、看懂每个字节、定位现场故障"。文中字节值均取自本地两份开源库源码(HslCommunication Java 3.9.3、iot-communication)的真实常量与构造逻辑,标注了文件与行号;源码未定义或未实测的内容标注"(需实测确认)"。
目录
- 西门子通信家族概览
- 协议栈分层:TPKT + COTP + S7comm
- TPKT 头(RFC1006)
- COTP 的两种形态:CR/CC 与 DT
- TSAP 的奥秘(连接失败第一嫌疑)
- S7comm PDU:Header + Parameter + Data
- Setup Communication(0xF0):PDU 协商与两次握手
- 读请求构建:Function 0x04 与 S7ANY
- 区域码(Area code)对照表
- 读响应解析与返回码
- 写请求:Function 0x05、写位与写字
- 地址语法与解析代码
- PDU 长度限制与自动分包
- 完整报文实例(5 例)
- Fetch/Write 协议与 S7comm 对比
- 现场配置要点
- 常见错误码与排查清单
- 小结与现场坑清单
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 |
源码依据:
- HSL
SiemensS7Net.java:65-92:按机型改plcHead1[21](S300→2、S400→3、S1200/1500→0);S400 额外plcHead1[17]=0(本地 TSAP 由0x0102变0x0100)。200Smart/200 替换整条数组(:848-867):200Smart 为C1 02 10 00/C2 02 03 00/C0 01 0A;S7-200 为C1 02 4D 57/C2 02 4D 57/C0 01 09(注意 S7-200 的 TPDU-Size 是 0x09 = 512)。 - iot
service/PLCNetwork.java:158-191:remote = 0x0300,S300/400/1200/1500 执行remote += 0x20 * rack + slot;enums/EPlcType.java:34-40:S300(0,2)、S400(0,3)、S1200(0,1)、S1500(0,1)。
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
三处源码一致:
model/RequestItem.java:128-134:buff.putBytes(IntegerUtil.toByteArray((this.byteAddress << 3) + this.bitAddress), 1);反向:196-202用>>3与&0x07。HslCommunication/Core/Address/S7AddressData.java:96-110(CalculateAddressStarted):无小数点则×8,有则整数×8 + 小数。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 佐证:
- 单位:
Write(String, boolean)(:534-540)直接发BuildWriteBitCommand,无前置读。 - 位数组:
Write(String, boolean[])(:549-567)先读 → 改位 → 整字节写回:
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 排查清单(按出现频率)
- 能连不能读(1200/1500) → 99% 是没勾 "PUT/GET 通信访问"。
- 读 DB 返回 0x0A → DB 号错 / DB 未下载 / DB 仍是"优化的块访问"。
- 值荒谬(超大数或 0) → 字节序错(应大端);或地址忘了 ×8。
- 长数据前面对、后面错 → 分包地址推进漏了 ×8。
- 握手失败 → TSAP(rack/slot / ConnectionType)。
- 返回 0x05 → 越界:起始地址 + 长度超出区域范围。
- 返回 0x06 → 类型不支持:transport size 与区域不匹配。
- 偶发掉线 → PLC 连接资源打满,或未串行化导致错帧。
- 写多 item 后数据错位 → 奇数长度项未补 1 字节对齐(§11.3)。
- 写位把相邻位清掉 → 用了整字节写;位数组必须 Read-Modify-Write。
18. 小结与现场坑清单
18.1 小结
- 分层:TPKT(4,含长度) + COTP(CR/CC 或 DT
02 F0 80)+ S7comm(Header + Parameter + Data)。握手后每帧前 7 字节恒定。 - 两次握手:COTP CR→CC(定 TSAP、TPDU Size),S7 Setup Communication(0xF0)→AckData(定 MaxAmQ 与 PDU)。
- TSAP:远端 =
ConnectionType<<8 | (rack<<5 | slot)。300=(0,2)、400=(0,3)、1200/1500≈(0,0/1),200Smart 特殊。连接失败先查它。 - 地址是位地址:
Address(3B) = byteAddr×8 + bitIndex;地址计算与分包推进都要 ×8。 - 读写:Function 0x04/0x05,item 为 12 字节 S7ANY(
12 0A 10 <tsize> <count> <db> <area> <addr24>)。HSL 几乎总用 transport size0x02(BYTE) + 字节数,而非按字计。 - 响应:
FF=成功;04=字节数据(长度按位,需 /8);03=位数据(长度按位,不除);0A=对象不存在;05=地址越界;06=类型不支持。 - PDU:1200 通常 240,单次读取上限 = PDU − 28 = 212;item 数源码硬限 19,实际受 PDU 约束更小。
- 写位:单位写用原生位写(不需预读);位数组必须 Read-Modify-Write,且非原子。
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)详解