深入解析西门子S7协议报文:从TPKT/COTP到数据读写实战

发布时间:2026/7/29 4:12:24
深入解析西门子S7协议报文:从TPKT/COTP到数据读写实战 1. 从一次“黑盒”调试说起为什么我们需要理解S7协议报文几年前我接手一个老旧产线的数据采集项目。现场有一台西门子S7-300 PLC负责控制几个关键阀门的开度。客户的需求很简单把阀门的实时开度值一个浮点数采集上来显示在新建的上位机监控系统里。听起来是个标准的OPC DA或者Modbus TCP就能搞定的小活儿。但到了现场才发现这台PLC除了一个以太网口没有任何开放的通信接口原有的上位机是一台早已停产的工控机里面的组态软件连厂家都找不到了。换句话说PLC对我来说就是个“黑盒”——我知道里面有数据但不知道用什么“语言”去问它要。当时第一个念头是找找有没有现成的驱动库。试了几个号称支持S7协议的商业库和开源组件要么连接不稳定时不时断线要么读取的数据全是乱码。在调试窗口里我看到软件发送和接收着一串串十六进制的数据但对面的PLC就像个沉默的巨人偶尔回应一下也完全看不懂它在“说”什么。那一刻我意识到如果不想被各种封装好的、但可能并不可靠的“黑盒”驱动牵着鼻子走就必须自己搞清楚西门子S7协议这套“语言”的语法。这不是为了炫技而是在复杂的工业现场当标准方案失效时能亲手“掰开”数据流定位问题根源的唯一途径。理解S7协议报文就是拿到了与西门子PLC直接对话的钥匙从被动的“使用者”变为主动的“沟通者”。今天我就把自己在多个项目中摸索、验证过的S7协议报文解析经验进行一次彻底的梳理。这不是一份官方的协议手册那东西既庞杂又枯燥而是一份从实战出发的“解码指南”。我们会从最基础的连接建立开始一步步拆解该协议的核心报文结构重点聚焦在最常用的数据读写功能上并用实际的代码片段展示如何构造和解析。无论你是正在开发自己的数据采集中间件还是在调试第三方软件时遇到了令人困惑的通信问题这篇文章都能帮你拨开迷雾直击核心。2. 协议基石TPKT与ISO-COTP——通信的“信封”与“快递单”在深入S7协议的核心内容之前我们必须先理解它赖以传输的两层“包装”。你可以把整个通信过程想象成寄信S7协议本身的读写指令是信纸上的具体内容比如“请告诉我DB10.DBD20的值”但这张信纸需要先装入一个标准信封TPKT然后在信封上贴上包含详细地址和流程信息的快递单ISO-COTP才能被网络投递。很多初学者直接去啃S7的“信纸内容”却忽略了“信封”和“快递单”的格式导致连最基本的连接都建立不起来。2.1 TPKT简单直接的传输包装TPKTISO Transport Service on top of the TCP是一个非常轻量级的头部定义在RFC 1006中它用于在TCP流中标识一个独立协议数据单元PDU的边界。其结构固定为4字节字节偏移字段名值示例说明0版本0x03固定为3代表TPKT版本号。1保留0x00保留字段始终为0。2-3长度0x00, 0x1F大端字节序。表示整个TPKT数据包包含这4字节头部的总长度。例如0x001F表示后续长度为31字节。关键点这里的“长度”是整个包的长度。计算时你需要将S7 PDU或COTP PDU的长度加上4TPKT头自身的长度。在解析时先读取这4个字节根据“长度”字段值就能从TCP流中准确截取出一个完整的数据包有效解决TCP的粘包问题。2.2 ISO-COTP面向连接的传输服务COTPConnection-Oriented Transport Protocol位于TPKT之内它负责管理连接的建立、释放以及数据的分片虽然S7通信中通常不分片。对于S7协议我们主要关注两种类型的COTP PDU连接请求CR和连接确认CC以及用于数据传输的数据包DT。COTP连接请求CR报文结构分析这是客户端主动发起连接时发送的第一个有效载荷。一个典型的CR报文如下包含在TPKT中TPKT Header: 03 00 00 16 COTP CR: 11 // 长度和PDU类型 e0 // 目标引用高 00 // 目标引用低 00 00 // 源引用 00 // 协议类0无显式流控 c1 // 参数目标TSAP高 02 // 参数目标TSAP低 c2 // 参数源TSAP高 02 // 参数源TSAP低长度1字节0x11表示COTP头部后续的字节数不包括长度字段本身这里是17字节。PDU类型1字节0xE0代表连接请求Connection Request。目标/源引用各2字节用于标识连接可以自行设定通常不重要。参数这是关键c1 02和c2 02分别定义了目标TSAP和源TSAP。TSAPTransport Service Access Point可以理解为TCP端口之上的另一个逻辑端口号用于在同一个物理连接上复用多个逻辑连接。西门子PLC使用TSAP来区分不同的通信服务如PG/PC编程、S7基本通信、S7连接等。计算规则对于S7-300/400/1200/1500常见的TSAP由两个字节组成。第一个字节通常是0x10 机架号第二个字节是0x00 槽号。但更常见的简化设置是目标TSAPPLC端固定为0x01, 0x00对应机架0槽2但常被视为默认设置源TSAP客户端则可以是0x01, 0x00或任意不冲突的值如0x01, 0x01。上面示例中的c1 02和c2 02是另一种编码形式c1表示目标TSAP标识02是长度后面跟两个字节的实际TSAP值。在实际构造时我们通常直接使用01 00和01 01这样的简单形式。COTP连接确认CC报文PLC在收到CR后会回复一个CC报文其PDU类型为0xD0。结构类似CR包含了PLC确认的参数。收到CC意味着COTP连接层已经建立成功。COTP数据DT报文在连接建立后所有S7协议PDU都包裹在COTP DT报文中发送。DT报文头极其简单0x02表示DT PDU 0xF0一个固定值指示这是最后一个数据单元无分片 TPDU编号1字节用于流控通常忽略。所以你看到的实际S7报文前面都会有02 f0 80这三个字节的COTP DT头。实操心得90%的S7连接失败问题都出在TPKT长度计算错误或TSAP设置不对上。务必使用网络抓包工具如Wireshark它内置了S7协议解析器对比你的报文和成功通信的报文。一个快速验证TSAP的方法是用西门子官方软件如Step 7或TIA Portal成功连接一次PLC然后用Wireshark抓包直接查看CR报文中的“Destination TSAP”和“Source TSAP”字段照抄即可。3. 核心对话S7协议PDU的请求与响应结构当COTP连接建立后我们终于可以开始“对话”了。S7协议PDU是对话的具体内容。它遵循一个非常固定的“请求-响应”模式。无论是读、写、还是其他功能请求和响应的报文结构都有清晰的层次。3.1 通用PDU头部协议、功能和冗余检查每一个S7 PDU都以一个10字节对于S7-300/400或12字节对于S7-1200/1500多一个保留字的头部开始。我们以经典的S7-300/400的10字节头部为例字段字节数典型值说明协议ID10x32固定值标识这是S7协议。PDU类型10x01 (请求) / 0x03 (响应)0x01: Job (客户端请求)0x02: Ack0x03: Ack-Data (服务器响应)0x07: Userdata。我们主要和0x01、0x03打交道。冗余标识20x0000用于冗余系统单机通常为0。协议数据单元参考2自增序列号客户端生成用于匹配请求和响应。比如你发送的请求是0x0001PLC返回的响应也应该是0x0001。这是排查“应答不对”问题的重要依据。参数长度2大端后续参数部分的字节数。数据长度2大端后续数据部分的字节数。对于读请求数据长度为0。参数部分紧随头部之后其结构根据PDU类型请求/响应和功能码不同而变化。功能码是参数部分的第一个字节。数据部分则存放着实际要读取或写入的值。3.2 读请求的构造你想要什么一个读请求PDU类型为0x01的核心在于其参数部分它明确告诉PLC“我要读哪个存储区、从哪个地址开始、读多少数据。”读请求的参数部分固定格式如下功能码0x04代表读变量。项目数量0x01表示本次请求只读取一个连续的数据块一次请求可以读多个不连续区域但常见的是单个。变量规格地址格式0x12。这是一个固定值表示后续的地址信息遵循“S7-Any”指针格式。读取长度0x0a0x00。2字节大端。表示请求读取10个字节。注意S7协议一次读取的最大长度受PLC型号和连接资源限制通常为200字节左右。超限需要分多次读。DB号0x000x01。2字节大端。如果非DB区此处为0。这里表示DB1。存储区与地址0x840x000x000x00。0x84这是一个复合字节。高4位0x8代表存储区类型0x1I输入0x2Q输出0x3M位存储器0x4DB数据块0x5DI背景数据块。这里0x8是0x4的另一种表示有时是0x84有时是0x30|0x04取决于协议细节但Wireshark能帮你确认。低4位0x4表示后续地址的字节数这里是4字节。0x000x000x003字节的偏移地址按位寻址。需要乘以8转换为字节内的位偏移。这里全是0表示从DB1.DBX0.0开始。一个完整的读DB1.DBB0开始10个字节的请求报文示例已包含TPKTCOTP DT头// TPKT Header 03 00 00 1f // 总长度31字节 // COTP DT Header 02 f0 80 // S7 PDU Header 32 01 00 00 00 01 00 0e 00 00 // 协议ID, Job, 序列号0x0001, 参数长14数据长0 // S7 Parameter (读) 04 // 功能码读 01 // 项目数1 12 // 地址格式S7-Any 0a 00 // 读取长度10字节 00 01 // DB号1 84 // 存储区DB地址长度4字节 00 00 00 // 偏移地址0字节 * 8 位偏移0关键解析84 00 00 00如何对应到DB1.DBB084表示DB区且地址信息占4字节。后3字节00 00 00是位偏移0。因为我们要读的是字节BB所以从位偏移0开始的连续8位一个字节就是DBB0。如果要读DB1.DBD4一个双字4字节偏移地址需要是4 * 8 32转换为3字节就是00 00 20因为32的十六进制是0x20。3.3 读响应的解析PLC给了你什么PLC对读请求的响应PDU类型为0x03(Ack-Data)。其结构也包含头部、参数和数据部分。响应参数部分很简单功能码0x04与请求对应。项目数量0x01。返回码0xff。这是最关键的一个字节0xff表示成功。任何其他值都代表错误例如0x05表示地址错误0x0a表示对象不存在等。解析响应时必须首先检查这个返回码。响应数据部分包含了实际读取到的值。其结构为数据类型0x04表示读取的是字节/字/块。返回长度2字节大端。表示后续实际数据字节数。实际数据连续的数据字节。接上例一个成功的读响应报文可能如下// TPKT Header 03 00 00 25 // 总长度37字节 // COTP DT Header 02 f0 80 // S7 PDU Header 32 03 00 00 00 01 00 02 00 08 // 协议ID, Ack-Data, 序列号0x0001, 参数长2数据长8 // S7 Parameter (读响应) 04 // 功能码读 01 // 项目数1 ff // 返回码成功 // S7 Data 04 // 数据类型字节/字/块 00 0a // 返回长度10字节 01 02 03 04 05 06 07 08 09 0a // 实际数据DB1.DBB0~DBB9的值解析时我们首先确认返回码是0xff然后从数据部分提取出00 0a后面的10个字节01 02 ... 0a这就是DB1.DBB0到DBB9的值。避坑指南响应数据部分的“返回长度”字段表示的是“实际数据字节数”。它不一定等于请求的长度。比如如果你请求读10个字节但DB块实际只有5个字节PLC可能会返回错误也可能只返回5个字节返回长度5。你的解析程序必须能处理这种情况不能僵化地按照请求长度去截取数据。4. 写操作与复杂数据类型处理理解了读操作写操作就顺理成章了它更像是读操作的“逆过程”。但这里涉及到如何将高级语言中的数据类型如Int、Real、String转换为S7协议能识别的字节序列这是实际应用中的另一个核心难点。4.1 写请求的构造告诉PLC改什么写请求PDU类型仍为0x01的参数部分与读请求高度相似但功能码是0x05。最大的不同在于它必须携带“数据部分”这部分明确指出了要写入的数据类型、长度和具体的值。一个写DB1.DBW2字值为0x1234的请求报文结构如下// ... TPKT, COTP, S7 Header 类似注意数据长度不为0 ... // S7 Parameter (写) 05 // 功能码写 01 // 项目数1 12 // 地址格式S7-Any 02 00 // 写入长度2字节 (一个字) 00 01 // DB号1 84 // 存储区DB地址长度4字节 00 10 00 // 偏移地址2字节 * 8 16位偏移 0x0010 // S7 Data 04 // 数据类型字节/字/块 00 02 // 写入数据长度2字节 12 34 // 要写入的数据0x1234地址计算DBW2表示从第2个字节开始的一个字2字节。字节偏移为2位偏移为2 * 8 16。16的十六进制是0x10用3字节表示为00 10 00大端表示实际有效位是中间的0x10。4.2 写响应的解析确认是否成功写响应的结构与读响应类似但更简单。参数部分包含功能码0x05和返回码0xff成功。数据部分通常很短只包含一个简单的确认信息。4.3 复杂数据类型的字节序与编码转换这是协议解析中最容易出错的地方。西门子PLC使用的字节序Byte Order与我们的PCx86架构通常不同。字节序EndiannessPCx86, ARM等通常采用小端序Little-Endian即低位字节在前。例如整数0x1234在内存中存储为34 12。西门子S7-300/400/1200/1500采用大端序Big-Endian即高位字节在前。0x1234存储为12 34。影响所有多字节数据类型Word, Int, DWord, DInt, Real, Time等在组包写入和解包读取时都必须进行字节序转换。常见数据类型编码示例Int (16位有符号整数)值-100。内存表示补码0xFF9C。S7报文中的字节序列FF 9C大端。你的程序发送或接收后需要转换为9C FF才能被小端系统正确解释为-100。Real (32位浮点数IEEE 754)值123.456。内存表示小端0x79 E9 F6 42。S7报文中的字节序列42 F6 E9 79大端。你需要将收到的42 F6 E9 79重新排序为79 E9 F6 42才能得到正确的浮点数。String (S7格式字符串)西门子有特定的字符串格式。第一个字节是最大长度第二个字节是当前长度后面才是字符数据ASCII。例如定义String[10]的变量存储Hello。在DB块中可能表示为0A 05 48 65 6C 6C 6F 00 00 00 00最大长度10当前长度5内容“Hello”剩余用0填充。解析时你需要根据这个特定格式来提取有效字符。实战技巧在代码中不要手动拼接这些字节。为每种数据类型Int, DInt, Real, Word等编写专用的ToBytes()和FromBytes()函数内部处理好字节序转换。对于字符串更要小心处理长度字节和填充字节。一个健壮的库应该封装好这些细节。5. 实战演练用Python构造与解析一个完整的读操作理论说得再多不如一行代码。下面我们用Python使用socket库演示如何手动构造一个读取DB1.DBD4双字4字节的请求并解析响应。这里假设PLC的IP是192.168.0.1TSAP设置如前所述。import socket import struct def build_read_request(sequence, db_number, byte_offset, read_length): 构造一个读DB块的请求报文 # 1. TPKT Header tpkt_ver 0x03 tpkt_reserved 0x00 # 总长度稍后计算 # 2. COTP DT Header cotp_dt bytes([0x02, 0xf0, 0x80]) # 3. S7 PDU Header (Job) protocol_id 0x32 pdu_type 0x01 # Job redundant_ident 0x0000 pdu_ref sequence 0xFFFF param_length 0x000E # 固定14字节 data_length 0x0000 # 读请求无数据 s7_header struct.pack(BBHHHH, protocol_id, pdu_type, redundant_ident, pdu_ref, param_length, data_length) # 4. S7 Parameter (Read) func_code 0x04 item_count 0x01 addr_format 0x12 # 计算位偏移 bit_offset byte_offset * 8 # 注意西门子地址是3字节大端但位偏移要放在中间字节实际是打包成3字节。 # 更常见的做法是将位偏移转换为一个4字节整数取低3字节按大端排列。 # 例如byte_offset4, bit_offset32 (0x20) addr_bytes struct.pack(I, bit_offset)[1:] # 取后3字节即 00 00 20 s7_param struct.pack(BBHHBB, func_code, item_count, read_length, db_number, 0x84, addr_bytes[0]) s7_param addr_bytes[1:] # 加上后两个地址字节 # 5. 组装完整S7 PDU s7_pdu s7_header s7_param # 6. 组装COTP PDU cotp_pdu cotp_dt s7_pdu # 7. 计算总长度并组装TPKT total_length len(cotp_pdu) 4 tpkt_header struct.pack(BBH, tpkt_ver, tpkt_reserved, total_length) # 完整报文 full_packet tpkt_header cotp_pdu return full_packet def parse_read_response(packet, sequence): 解析读响应报文返回数据字节或错误信息 # 跳过TPKT头 (4字节) 和 COTP DT头 (3字节) s7_pdu packet[7:] # 解析S7 Header proto_id, pdu_type, red_id, pdu_ref, param_len, data_len struct.unpack_from(BBHHHH, s7_pdu, 0) if pdu_ref ! sequence: return None, f序列号不匹配: 期望{sequence}, 收到{pdu_ref} if pdu_type ! 0x03: # 不是 Ack-Data return None, f非期望的PDU类型: {pdu_type} # 跳转到参数部分 param_offset 10 func_code, item_count, return_code struct.unpack_from(BBB, s7_pdu, param_offset) if func_code ! 0x04: return None, f功能码错误: {func_code} if return_code ! 0xff: return None, fPLC返回错误码: 0x{return_code:02x} # 跳转到数据部分 data_offset param_offset param_len data_type, ret_len struct.unpack_from(BH, s7_pdu, data_offset) if data_type ! 0x04: return None, f数据类型错误: {data_type} # 提取实际数据 actual_data s7_pdu[data_offset 3: data_offset 3 ret_len] return actual_data, None # 主程序 def main(): plc_ip 192.168.0.1 plc_port 102 # S7协议默认端口 sequence 1 db_number 1 byte_offset 4 # 读取 DBD4 read_length 4 # 读取4个字节 (一个双字) # 构造请求 request build_read_request(sequence, db_number, byte_offset, read_length) # 建立TCP连接 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5.0) # 设置超时 try: sock.connect((plc_ip, plc_port)) # 发送连接请求 (COTP CR)这里为简化假设已连接。实际需要先发CR。 # 发送读请求 sock.send(request) # 接收响应 response sock.recv(1024) # 解析响应 data, error parse_read_response(response, sequence) if error: print(f读取失败: {error}) else: print(f读取成功原始数据: {data.hex()}) # 假设我们知道读回来的是一个DInt32位有符号整数大端序 # 需要转换为小端序再解析 value_le int.from_bytes(data, byteorderbig, signedTrue) # 注意from_bytes with big 直接解析大端序 # 但通常我们的系统是小端所以如果直接按大端解析得到错误值需要转换 # 更清晰的做法 data_be data # 报文中的是大端数据 # 方法将大端字节序转换为整数 value struct.unpack(i, data_be)[0] # i 表示大端有符号32位整数 print(f解析后的DInt值: {value}) except socket.timeout: print(连接或接收超时) except Exception as e: print(f通信错误: {e}) finally: sock.close() if __name__ __main__: main()这段代码是一个高度简化的示例它省略了COTP连接建立的过程和完整的错误处理。但它清晰地展示了从组包、发送到解包的核心流程。在实际项目中你需要处理连接协商、序列号管理、超时重试、复杂地址计算等更多细节。6. 高级话题与排错实战掌握了基本读写你已经能解决80%的问题。剩下的20%往往出现在更复杂的场景和诡异的故障中。6.1 多项目读写与部分写入S7协议支持在一个PDU内读写多个不连续的区域。参数部分中的“项目数量”字段可以大于1后面跟随多个“变量规格”项。这在需要一次性读取散布在各处的多个变量时非常高效能减少通信往返次数。数据部分的组织也会相应变化每个读取项都有独立的返回头和返回码。实现此功能需要对协议有更精确的把握建议在单项目稳定后再尝试。部分写入Partial Write用于写入位Bit变量。其地址计算需要精确到位并且数据部分需要指明写入的是位值0或1。例如写入DB1.DBX0.5为1地址偏移是(0*8)55数据部分会包含特定的位操作编码。6.2 使用Wireshark进行深度协议调试当你的代码不工作时Wireshark是你的最佳搭档。按以下步骤操作过滤在Wireshark中使用过滤器tcp.port 102只看S7通信流量。对比用你的程序发起一次通信同时用西门子官方软件如Step 7的“监控与强制表”或TIA Portal的“在线访问”对同一个地址进行一次成功的读写操作。分析在Wireshark中对比两个通信流。连接阶段看CR报文的TSAP是否一致。请求阶段对比S7 PDU头部中的“协议数据单元参考”序列号是否正常递增参数部分的地址编码特别是DB号和偏移是否完全一致数据部分的字节序是否正确响应阶段PLC是否返回了响应返回码是0xff吗数据长度是否符合预期解码Wireshark的S7协议解析器能帮你把十六进制报文翻译成可读的格式如“Read Var, DB1, 4 bytes at offset 0”极大提升调试效率。如果Wireshark能正确解析官方软件的报文但不能解析你的报文那问题一定出在你的报文构造上。6.3 常见错误码与排查思路0x05 - 地址错误你请求的地址在PLC中不存在。检查DB号、字节偏移是否超出块长度存储区标识符是否正确例如对于M区存储区标识符是0x83。0x0a - 对象不存在通常是DB号错误或者该DB块未被下载到PLC中。0xd2 - 资源不可用PLC的连接资源已满。S7-300/400的每个CPU有有限的通信连接数。需要检查PLC配置或等待其他连接释放。无响应或连接被重置检查物理网络和IP地址。检查PLC的防火墙或访问保护设置特别是S7-1200/1500需要在“防护与安全”中勾选“允许来自远程对象的PUT/GET通信访问”。检查TSAP设置确保与PLC配置一致。对于S7-1200/1500机架/槽号通常为0TSAP可能是03.01或03.00需要查看PLC属性中的连接机制。确认PLC处于RUN模式某些操作在STOP模式下被禁止。理解S7协议报文就像是掌握了与西门子PLC直接沟通的母语。它让你不再依赖那些可能封装得并不完美的第三方库在出现通信故障时能够直击问题本质。从最基本的TPKT/COTP信封到S7 PDU的具体指令再到复杂数据类型的转换每一步都需要耐心和细致的理解。我建议你在自己的测试环境中从一个最简单的读操作开始用Wireshark抓包对照本文的解析亲手构造和解析几个报文。这个过程可能会遇到各种意想不到的问题但每一次解决问题的经历都会让你对工业通信协议的理解更深一层。当你能够不借助任何高级库仅凭socket和协议文档就稳定地与PLC交换数据时那种对系统底层的掌控感是使用现成工具无法比拟的。