☰
S7协议从握手到读写:西门子PLC通信报文构造与实操指南
2026/9/28 16:07:49 网站建设 项目流程

1. 为什么值得花时间啃S7协议

搞工业自动化的朋友,尤其是跟西门子PLC打交道比较多的,大概率都遇到过这样的场景:上位机要跟PLC交换数据,用现成的OPC服务器或者组态软件当然省事,但一旦项目要求定制化、要求轻量化、要求跑在嵌入式网关或者Linux工控机上,就绕不开自己动手实现S7通信。我最早接触这块是因为一个数据采集项目,现场有S7-1200和S7-200 SMART混用,上位机是自研的C++程序,不想装几百兆的运行时,只能自己啃协议。

S7通信协议是西门子私有的应用层协议,跑在TCP/IP之上(早期S7-200走的是自由口串口,后来200 SMART和1200/1500都支持以太网S7通信)。它不像Modbus那样公开透明、文档齐全,很多细节得靠抓包加试验反推。但一旦搞明白它的握手流程和数据读写报文结构,你会发现它其实设计得相当规整,读一个DB块或者M区,报文格式基本固定,改改参数就能复用。

这篇文章适合几类人看:一是做上位机开发、需要跟西门子PLC直连通信的工程师;二是做工业网关、边缘计算盒子,要把PLC数据往云端或者MES推的开发者;三是单纯对工业协议感兴趣、想搞明白“PLC到底怎么跟外面说话”的技术爱好者。我会从连接握手讲起,一直讲到数据读写报文的构造,中间穿插我踩过的坑和实测有效的参数。看完你至少能做到:用抓包工具看懂S7报文,自己写代码建立连接并读写PLC数据。

2. S7协议的整体设计与分层思路

2.1 它到底跑在哪一层

很多人一上来就问“S7协议和TCP什么关系”,其实S7是应用层协议,底下直接坐TCP。以S7-1200/1500为例,默认走102端口。S7-200 SMART也是102端口,但它的实现跟1200有细微差别,后面会讲。整个通信栈大致是这样:物理层和链路层是以太网,网络层和传输层是TCP/IP,再往上就是S7自己的报文。

这里有个容易混淆的点:S7通信和ISO-on-TCP(RFC1006)的关系。严格来说,S7报文是封装在TPKT和COTP之上的。TPKT是ISO Transport Service on top of TCP,它给TCP流加了一个长度头,让接收方能知道一个完整报文从哪开始到哪结束。COTP是Connection-Oriented Transport Protocol,负责连接建立和数据传输的标识。你可以把TPKT理解成快递外包装上的尺寸标签,COTP理解成运单号,S7报文才是包裹里的实际货物。

为什么这么设计?因为TCP是字节流,没有消息边界。TPKT用4个字节告诉对方“我这个包总共多长”,接收方就能准确切分。COTP则负责在一条TCP连接上区分不同的“连接”,虽然实际用的时候通常就一条。

2.2 握手阶段的设计逻辑

S7的连接建立分两步:先是TCP三次握手,然后是COTP连接请求和确认,最后才是S7通信的协商。TCP三次握手是标准流程,SYN、SYN-ACK、ACK,这个不展开。重点说COTP和S7的握手。

COTP连接请求报文里有两个关键字段:源引用(Source Reference)和目标引用(Destination Reference)。源引用是发起方自己生成的,目标引用在请求阶段填0,等对方回复时它会带上自己生成的引用值。这两个值后续通信要一直带着,用来标识这条COTP连接。

COTP连接确认之后,S7层还要做一次“通信建立”。发起方发送一个S7 Communication Setup报文,里面包含最大PDU长度、并发作业数等参数。PLC回复确认,双方就按协商好的参数开始通信。这个设计的好处是灵活:不同型号的PLC可以支持不同的PDU大小,协商机制让双方都能接受。

我实测过S7-1200的协商结果,最大PDU通常是480字节,S7-1500可以到960甚至更大。S7-200 SMART一般是240字节左右。这个PDU大小直接决定了你一次能读写多少数据,后面读写部分会详细算。

2.3 为什么不用现成库而要自己实现

市面上有Snap7、libnodave这些开源库,功能很全。但自己实现有几个实际好处:一是可控,遇到问题能直接看报文,不用猜库内部干了什么;二是轻量,一个精简实现编译出来可能就几十KB,适合跑在资源紧张的嵌入式设备上;三是能针对特定场景优化,比如只读几个字节的数据,不需要库那么重的初始化流程。

当然,自己实现的前提是你得把协议吃透。下面我就按握手、协商、读、写四个环节,把关键细节拆开讲。

3. 握手阶段的核心细节与实操要点

3.1 TCP连接与COTP报文构造

TCP连接没什么好说的,用你熟悉的语言建socket连PLC的102端口就行。连上之后,第一件事是发COTP连接请求。这个报文的结构如下:

字段长度说明
TPKT版本1字节固定0x03
TPKT保留1字节固定0x00
TPKT长度2字节整个报文总长度,大端
COTP长度1字节从COTP长度字节之后算起的长度
COTP类型1字节连接请求是0xE0
目标引用2字节请求时填0x0000
源引用2字节自己生成,随便填但后续要用
Class/Option1字节通常0x00
参数代码1字节0xC0表示TPDU大小
参数长度1字节0x01
TPDU大小1字节0x0A表示1024字节

实际抓包看到的连接请求,TPKT长度通常是0x0016,也就是22字节。COTP长度是0x11,17字节。源引用我一般用0x0001,目标引用填0。TPDU大小填0x0A,表示支持最大1024字节的TPDU,这个值够用了。

PLC回复的连接确认报文,类型是0x0D,目标引用会填上你发的源引用,源引用是PLC自己生成的。这个PLC的源引用你要记下来,后续所有报文的目标引用都填它。

注意:有些资料说源引用和目标引用可以随便填,实测下来在S7-1200上如果引用值不匹配,PLC会直接断开连接。所以务必按规则来:你发的报文里目标引用填PLC的源引用,源引用填你自己的值。

3.2 S7通信建立报文的参数协商

COTP连上之后,紧接着发S7 Communication Setup。这个报文的结构稍微复杂一点:

字段值说明
TPKT头03 00 00 xx长度按实际算
COTP头02 F0 80数据传输类型
S7协议ID0x32固定
ROSCTR0x01作业请求
保留0x0000
PDU引用2字节自己维护的序号
参数长度2字节
数据长度2字节0x0000
功能码0xF0Setup Communication
保留0x00
最大AmQ2字节并发作业数,通常1
最大PDU2字节请求的PDU大小
最大AmQ调用2字节通常1

最大PDU这个字段很关键。你填一个值,PLC会回复它能接受的值。比如你填960,S7-1200可能回480。最终以PLC回复的为准。我一般请求960,让PLC自己决定。

PLC回复的Setup Communication报文里,ROSCTR是0x03(确认),功能码还是0xF0,后面跟着协商后的最大AmQ和最大PDU。拿到这个值之后,你后续读写报文的数据区长度就不能超过它。

实操心得:S7-200 SMART的Setup报文跟1200略有不同,它的PDU协商结果通常是240。如果你用同一套代码兼容两种PLC,建议把请求PDU设小一点,比如240,这样两边都能接受。另外200 SMART对COTP的TPDU大小也比较敏感,填0x0A一般没问题。

3.3 握手阶段的常见坑

第一个坑是字节序。S7协议里所有多字节字段都是大端,也就是网络字节序。如果你用C语言写,记得用htons转换。我见过有人在小端机器上直接memcpy结构体,结果长度字段全反了,PLC直接不回复。

第二个坑是TPKT长度计算。TPKT长度是从版本字段开始算到报文末尾的总字节数,包含TPKT头本身。很多人漏算或者多算,导致PLC认为报文不完整。建议写个函数专门算长度,别手算。

第三个坑是COTP的TPDU大小。如果你填0x0A(1024),但实际发送的报文超过1024,PLC会拒绝。不过S7的Setup报文很短,一般不会超。真正要注意的是后续读写报文,如果PDU协商成480,你的TPKT长度不能超过480加上各种头的开销。

第四个坑是连接保持。S7连接建立后,如果长时间不发数据,PLC可能会主动断开。我一般每隔30秒发一个空的读请求或者保持报文,具体看PLC型号。S7-1200的默认超时好像是几分钟,但不同固件版本可能不一样,实测最稳的是20到30秒发一次心跳。

4. 数据读写报文的构造与参数计算

4.1 读报文的完整结构

读操作的功能码是0x04。一个典型的读DB块报文长这样:

字段值说明
TPKT头03 00 00 xx
COTP头02 F0 80
S7协议ID0x32
ROSCTR0x01作业请求
保留0x0000
PDU引用2字节递增序号
参数长度2字节通常是0x000E
数据长度2字节0x0000
功能码0x04读
项数1字节读几个变量
变量规格0x12固定
变量长度1字节后续地址描述的长度
存储区1字节0x81=DB,0x82=输入,0x83=输出,0x84=位存储
地址3字节字节地址+位地址
数据类型1字节0x02=字节,0x04=字,0x06=双字等
长度2字节读多少个该类型的数据

存储区代码要记清楚:DB块是0x81,输入区I是0x82,输出区Q是0x83,位存储区M是0x84。地址字段是3个字节,第一个字节是高位,比如DB1.DBX0.0,地址就是00 00 00。如果是DB1.DBX10.3,地址是00 00 0A,位地址在数据类型里体现。

数据类型代码:0x01是位,0x02是字节,0x03是字符,0x04是字,0x05是整数,0x06是双字,0x07是双整数,0x08是浮点数。读的时候长度字段表示读几个该类型。比如读DB1.DBD0开始的10个浮点数,数据类型0x08,长度0x000A。

PLC回复的读响应,ROSCTR是0x03,功能码0x04,后面跟着数据长度和实际数据。数据部分是按请求顺序排列的,每个变量前面有一个返回码,0xFF表示成功,其他值表示错误。

4.2 写报文的差异点

写操作功能码是0x05。写报文比读报文多了一个数据区,参数区描述要写到哪里、写什么类型、写多长,数据区放实际要写的值。

写报文的参数区结构和读类似,但多了一个传输大小字段。数据区就是你要写的原始字节。比如写一个浮点数3.14到DB1.DBD0,数据区就是3.14的IEEE754表示,4个字节。

注意:写操作的数据长度字段和参数区的长度字段要对应上。我见过有人参数区写长度4,数据区只放了2个字节,PLC直接返回错误码。另外写位变量的时候,数据类型填0x01,长度填0x0001,数据区放0x01或0x00。

4.3 PDU大小对读写的影响与计算

前面协商出来的最大PDU决定了你一次能读写多少数据。假设协商结果是480字节,那么整个S7报文(从S7协议ID开始算)不能超过480。TPKT头和COTP头大概占7个字节,S7头占10到12个字节,参数区读操作占12个字节,剩下给数据区的就不多了。

读操作的数据区在请求里是空的,所以读的时候限制主要在参数区。但响应报文里数据区会占满,所以实际能读多少取决于响应报文的总长度。粗略算一下:480减去TPKT和COTP的7字节,减去S7头的12字节,减去参数区的12字节,再减去每个变量4字节的返回码和长度描述,剩下的才是数据。读浮点数的话,大概能读100个左右。

写操作更紧张,因为请求里就带数据。480减去各种头,再减去参数区,剩下的除以数据类型大小,就是一次能写的数量。如果数据量大,必须分多次写。

我一般写个函数自动计算单次最大读写量,根据协商的PDU动态调整。这样换不同PLC不用改代码。

4.4 多变量读写的处理

S7协议支持一次请求读多个不连续的变量,这在采集场景很有用。比如同时读DB1.DBD0的浮点数、DB2.DBW10的整数、M100.0的位。参数区里项数字段填3,然后依次描述每个变量的地址、类型、长度。

但要注意,PLC对单次请求的变量个数也有限制,通常不超过20个。而且如果其中一个变量地址非法,整个请求可能都失败。我的做法是分组,把地址连续的变量合并成一个读请求,不连续的单独发。这样既减少请求次数,又避免一个错误拖垮全部。

写操作也可以一次写多个变量,但数据区要按顺序拼接。实际用的时候,写多变量容易出错,我一般还是一次写一个变量,除非对性能要求特别高。

5. 实操过程与核心环节实现

5.1 环境准备与工具选型

要复现这套流程,你需要:一台西门子PLC(S7-1200或200 SMART都行),一根网线,一台电脑。软件方面,抓包用Wireshark,过滤条件写tcp.port == 102就能看到所有S7报文。PLC编程用博途或者STEP 7-Micro/WIN SMART,建一个DB块或者用M区做测试。

代码语言我选Python,因为写起来快,用socket库直接发字节。实际产品里用C++或者Go更合适,但原理一样。Python里用struct.pack来打包大端字节,很方便。

PLC侧要做的准备:确保PLC的IP和电脑在同一网段,在博途里勾选“允许来自远程对象的PUT/GET通信访问”,这个选项不勾,外部读写会被拒绝。S7-200 SMART在系统块里也有类似选项。

5.2 建立连接的完整代码流程

先建TCP连接:

import socket import struct plc_ip = "192.168.0.1" plc_port = 102 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((plc_ip, plc_port))

然后发COTP连接请求:

# TPKT + COTP连接请求 cotp_request = bytes([ 0x03, 0x00, 0x00, 0x16, # TPKT头,长度22 0x11, 0xE0, # COTP长度17,类型连接请求 0x00, 0x00, # 目标引用 0x00, 0x01, # 源引用 0x00, # Class/Option 0xC0, 0x01, 0x0A, # TPDU大小参数 0xC1, 0x02, 0x01, 0x00, # 源TSAP 0xC2, 0x02, 0x01, 0x02, # 目标TSAP ]) s.send(cotp_request) resp = s.recv(1024)

这里有个细节:TSAP。S7-1200的TSAP通常是01 00和01 02,但不同型号可能不同。S7-200 SMART的TSAP是03 00和03 01。如果TSAP填错,PLC会拒绝连接。我建议先用Wireshark抓一次正常通信的包,看TSAP是什么,照抄就行。

收到COTP确认后,发S7 Setup:

# S7 Communication Setup setup = bytes([ 0x03, 0x00, 0x00, 0x19, # TPKT头,长度25 0x02, 0xF0, 0x80, # COTP数据传输 0x32, 0x01, 0x00, 0x00, # S7协议ID,ROSCTR,保留 0x00, 0x01, # PDU引用 0x00, 0x08, 0x00, 0x00, # 参数长度8,数据长度0 0xF0, 0x00, # 功能码Setup,保留 0x00, 0x01, # 最大AmQ 0x03, 0xC0, # 请求PDU 960 0x00, 0x01, # 最大AmQ调用 ]) s.send(setup) resp = s.recv(1024) # 解析resp里的协商PDU

解析响应时,PDU大小在倒数第4和第3个字节。比如回复03 C0就是960,01 E0就是480。

5.3 读DB块数据的完整实现

假设协商PDU是480,读DB1.DBD0开始的10个浮点数:

def read_db(s, db_num, start_byte, data_type, count, pdu_ref): # 参数区 param = bytes([ 0x04, # 功能码读 0x01, # 项数1 0x12, # 变量规格 0x0A, # 变量长度10 0x10, # 语法ID,S7-1200用0x10 data_type, # 数据类型 (count >> 8) & 0xFF, count & 0xFF, # 长度 (db_num >> 8) & 0xFF, db_num & 0xFF, # DB号 0x84, # 存储区DB (start_byte >> 16) & 0xFF, (start_byte >> 8) & 0xFF, start_byte & 0xFF, # 地址 ]) # 组装完整报文 s7_header = bytes([ 0x32, 0x01, 0x00, 0x00, (pdu_ref >> 8) & 0xFF, pdu_ref & 0xFF, (len(param) >> 8) & 0xFF, len(param) & 0xFF, 0x00, 0x00, ]) payload = s7_header + param tpkt = bytes([0x03, 0x00]) + struct.pack('>H', len(payload) + 7) cotp = bytes([0x02, 0xF0, 0x80]) s.send(tpkt + cotp + payload) resp = s.recv(4096) # 解析数据 data_len = struct.unpack('>H', resp[14:16])[0] data = resp[16:16+data_len] # 跳过返回码和长度描述 return data[4:]

这里有几个关键点。语法ID在S7-1200上是0x10,S7-200 SMART上是0x12,这个差异坑了我很久。DB号是2字节,地址是3字节。返回的数据前面有4个字节的返回码和长度描述,要跳过。

读出来的浮点数用struct.unpack('>f', data[i:i+4])解析。注意S7的浮点数是IEEE754大端,跟网络字节序一致。

5.4 写数据的实现与验证

写一个浮点数到DB1.DBD0:

def write_db_float(s, db_num, start_byte, value, pdu_ref): data_bytes = struct.pack('>f', value) param = bytes([ 0x05, # 功能码写 0x01, # 项数1 0x12, # 变量规格 0x0A, # 变量长度10 0x10, # 语法ID 0x08, # 数据类型浮点 0x00, 0x04, # 长度4字节 (db_num >> 8) & 0xFF, db_num & 0xFF, 0x84, (start_byte >> 16) & 0xFF, (start_byte >> 8) & 0xFF, start_byte & 0xFF, ]) s7_header = bytes([ 0x32, 0x01, 0x00, 0x00, (pdu_ref >> 8) & 0xFF, pdu_ref & 0xFF, (len(param) >> 8) & 0xFF, len(param) & 0xFF, (len(data_bytes) >> 8) & 0xFF, len(data_bytes) & 0xFF, ]) payload = s7_header + param + data_bytes tpkt = bytes([0x03, 0x00]) + struct.pack('>H', len(payload) + 7) cotp = bytes([0x02, 0xF0, 0x80]) s.send(tpkt + cotp + payload) resp = s.recv(1024) # 检查返回码 return resp[21] == 0xFF

写完之后,用博途或者读操作验证一下值是否写进去了。我习惯写完立刻读一次,确认数据一致。

实操心得:写操作最容易出错的地方是数据长度和参数区长度不匹配。参数区里写的长度是4,数据区就必须正好4字节。另外写位变量的时候,数据区是1字节,0x01表示置位,0x00表示复位。写多个位的时候,每个位占一个字节,不是打包的。

6. 常见问题与排查技巧实录

6.1 连接建立失败排查表

现象可能原因排查方法
TCP连不上IP不对、端口不对、防火墙ping一下,telnet 102端口
COTP无响应TSAP填错抓包对比正常通信的TSAP
Setup被拒绝PDU请求太大改小PDU,比如240
连接后立刻断开引用值不匹配检查目标引用是否填了PLC的源引用
读写返回错误码地址非法、权限不够检查DB号、地址范围、PUT/GET权限

6.2 读写报文的典型错误码

PLC返回的错误码在响应的数据区里,每个变量一个字节。常见的有:

  • 0xFF:成功
  • 0x05:地址超出范围
  • 0x0A:对象不存在
  • 0x03:访问被拒绝
  • 0x07:数据类型不匹配

遇到0x03,先检查博途里的“允许PUT/GET通信访问”有没有勾。遇到0x05,检查地址是不是超出了DB块的实际长度。遇到0x0A,检查DB号对不对,有些DB是优化的,外部不能直接访问,要在DB属性里取消“优化的块访问”。

6.3 抓包分析实战技巧

Wireshark过滤tcp.port == 102之后,你会看到一堆TCP包和S7包。右键某个包,选“Follow TCP Stream”,能看到完整的字节流。我一般把字节流复制出来,按前面讲的格式逐字段对照。

有个小技巧:Wireshark自带S7协议解析器,但它有时候解析不准,尤其是200 SMART的报文。我建议关掉S7解析,直接看原始字节,自己按格式拆。这样虽然慢,但能真正搞明白每个字节的含义。

另一个技巧是保存正常通信的报文作为模板。下次自己写的代码发出去的报文,跟模板逐字节对比,很快就能找到差异。

6.4 性能优化的几个经验

第一,复用TCP连接。每次读写都重新握手开销很大,建立一次连接后持续用,只递增PDU引用就行。

第二,合并读请求。把地址连续的变量合并成一个请求,减少往返次数。我实测过,读100个浮点数,合并成一次请求比分成10次快5倍以上。

第三,异步处理。如果上位机要同时跟多台PLC通信,用多线程或者异步IO,别串行等。Python里可以用asyncio或者threading。

第四,注意PDU引用回绕。PDU引用是2字节,从0到65535,用完要回绕。我一般从1开始,到65535后回到1。PLC不关心具体值,只要每次请求不一样就行,但有些PLC会检查重复,所以还是递增比较稳。

6.5 不同PLC型号的差异对照

特性S7-1200S7-1500S7-200 SMART
默认PDU480960240
语法ID0x100x100x12
TSAP01 00/01 0201 00/01 0203 00/03 01
优化DB访问默认开启默认开启无此概念
PUT/GET权限需勾选需勾选需勾选

这个表是我实测加查资料整理的,不同固件版本可能有细微差别。最稳的办法还是抓包确认。

7. 从协议到应用的延伸思考

搞明白S7协议之后,很多之前觉得神秘的东西就通了。比如为什么有些OPC服务器读PLC那么慢,因为它在用最保守的PDU和单变量请求。为什么有些网关能读几千个点还很快,因为它合并了请求、复用了连接、优化了PDU。

再往深了说,S7协议的设计思路其实反映了工业通信的一个核心矛盾:可靠性和效率的平衡。握手阶段的各种确认、引用值、序列号,都是为了可靠。而PDU协商、多变量请求、连接复用,是为了效率。理解了这个矛盾,再看其他工业协议,比如Modbus、Profinet、EtherCAT,都能触类旁通。

我在实际项目里还遇到过S7通信跨网段、跨路由的情况。只要TCP能通,S7就能通,因为它是标准TCP应用。但要注意NAT环境下的端口映射,102端口要正确转发。另外有些防火墙会深度检测S7报文,可能误拦,需要加白名单。

最后分享一个调试小技巧:如果手头没有PLC,可以用Snap7的服务器模式模拟一个S7从站,或者用PLCSIM Advanced。PLCSIM Advanced支持S7通信,但需要配置虚拟网卡。Snap7的服务器更轻量,适合快速验证客户端代码。

这个内容后续还可以往几个方向扩展:一是S7协议的安全机制,比如连接密码和访问级别;二是S7通信在Profinet环境下的表现;三是用FPGA或者专用芯片实现硬件级S7通信。每个方向都够写一篇长文,有机会再聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询