干工控这行,尤其是做上位机、MES数据采集或者第三方网关的兄弟,早晚会碰到这么一件事:PLC那边明明在线,程序也跑得挺欢,可你的系统就是读不到数据。排查下来一脸懵,程序没动、网线没掉、IP也Ping通了,问题到底出在哪?这种时候,最有效的办法不是瞎猜,而是把通信报文抓出来看一眼。今天要聊的西门子S7通信协议,就是我在现场排查时最常用、也是被问得最多的一套协议,从Wireshark抓包到逐字节拆解,再到自己写代码实现一个最小客户端,一次给你讲透。
这篇文章适合谁?正在做上位机开发、想写第三方数据采集系统的工程师,以及刚接触西门子PLC通信、对S7协议一头雾水的新手。读完之后你能做到三件事:用Wireshark熟练抓取S7协议报文;看懂TPKT、COTP、S7 PDU这些层级是什么;脱离STEP 7或者博途自带功能,自己用代码和PLC完成读写。
1. 先搞明白:S7协议到底在工业现场承担什么角色
1.1 别把S7和Modbus混为一谈
很多从Modbus转过来的人,一开始容易把S7协议想成"西门子版的Modbus",这个理解其实不太准确。Modbus TCP是纯粹的请求响应模式,客户端发读写请求,服务端回结果,没有建连过程,报文结构也非常扁平。而S7协议是建立在ISO-on-TCP(RFC 1006)之上的,具体走TCP 102端口,真正通信之前要经历一层COTP握手,握手完成后才进入S7报文交互。
打个比方,Modbus像是你下楼拿快递,直接开门取就行;S7协议则是进一个写字楼,得先在一楼前台登记访客信息,拿到临时门禁卡,坐电梯到指定楼层,然后才能找具体工位的人办事。这个"前台登记"的过程就是COTP连接建立,对应报文里的CR(Connection Request)、CC(Connection Confirm)交互。实际现场中经常出现的"TCP能通但S7连不上"的问题,八成就是卡在了这一层握手,而不是PLC本身出了问题。
明白了这一点,后面看Wireshark抓包就能少走很多弯路。
1.2 谁在真正使用S7通信
先盘点一下现场最常见的应用场景,方便你对号入座:
- 上位机/SCADA通过S7协议直接读写PLC的DB块,这类需求在汽车产线、冶金、水处理行业特别多。
- 第三方智能网关(比如采集盒子、边缘计算设备)采集西门子PLC数据,网关内置的驱动本质上就是一个S7协议客户端。
- 不同品牌PLC之间做数据交换,比如西门子PLC和AB、倍福之间通过S7通信做中转。
- 自己开发专用协议转换器,把S7协议转成MQTT、OPC UA或者数据库写入,这种情况下对协议细节的掌握程度直接决定项目成败。
这些场景有一个共同点:虽然你不用天天手写S7报文,但一旦通信出问题,脑子里没有报文结构图,排查起来就会像大海捞针。我的经验是,抓包能力是所有高级通信排障手段的底层能力,今天花半小时把S7协议摸透了,下次现场哪怕设备在千里之外,你也能通过一个抓包文件定位故障。
2. 抓包环境搭建,抓出第一个通信报文
2.1 硬件连接与网卡选择
抓包这件事,看起来就是装个Wireshark点开始,但工控现场的抓包和纯IT环境还不太一样。我推荐最稳妥的方案:笔记本插网线直连PLC的以太网口(或者和PLC处于同一台交换机),连接方式如下:
- 笔记本IP设置成和PLC同一个网段,比如PLC是192.168.0.10,笔记本就设192.168.0.99,掩码255.255.255.0。
- 如果PLC的IP不知道,用西门子的"在局域网内检测设备"功能或者直接翻博途的项目组态,甚至可以用网卡凑巧能Ping通的办法去扫。
- 现场如果交换机是管理型的,注意关闭端口隔离,否则笔记本接在交换机上可能什么都抓不到。
这里特别提醒一点:尽量使用有线网卡抓包,不要用Wi-Fi。S7通信是实实在在的TCP流量,Wi-Fi虽然理论上也能抓,但工控现场电磁干扰、信号波动会带来非常多无关的广播包,过滤起来费眼。如果笔记本没有网口,买个USB 3.0转千兆网口的转接器,选芯片是RTL8153或者AX88179方案的,兼容性最好。
2.2 Wireshark过滤规则与抓包设置
设备接好之后,打开Wireshark,选择对应的网卡,直接开始抓包。由于S7协议走TCP 102端口,最关键的过滤规则就是这一条:
tcp.port == 102这样过滤完,屏幕上就只保留S7通信相关的报文,与现场其他无关流量彻底隔离。如果你还想进一步过滤出某一台设备的通信,加上IP条件即可:
tcp.port == 102 && ip.addr == 192.168.0.10需要注意,单纯抓包还不够,要学会看Wireshark协议解析列。默认情况下Wireshark对S7协议有很好的支持,抓到报文后"Protocol"列会显示S7COMM,并且会自动解析出功能码、参数长度、数据长度等字段,这些信息在后面逐层拆解时会非常有用。
2.3 长时间抓包与文件管理技巧
现场排障有时候要抓几十分钟甚至几个小时的报文,这就引出一个热词里大家常问的问题:Wireshark长时间抓包到底怎么操作?
我的做法是三步:第一,抓包前在Capture Options里设置文件切分,比如每100MB切一个新文件;第二,开启多文件循环写,只保留最近10个文件,防止磁盘被撑爆;第三,用以下命令行方式在后台静默抓包,不占用Wireshark图形界面的资源:
tshark -i eth0 -f "tcp port 102" -b filesize:102400 -b files:10 -w s7_capture.pcapng这段命令的意思是抓取eth0网卡上TCP端口102的流量,每个文件100MB,最多保留10个文件滚动覆盖,输出文件叫s7_capture.pcapng。这个技巧在无人值守的长时间抓包场景里特别好用,抓完之后再用Wireshark打开pcapng文件慢慢分析。
3. 从报文反推协议:TPKT/COTP/S7 PDU三层拆解
3.1 三层结构整体认知
真正查看S7协议报文时,你会发现每一条完整的S7数据帧在Wireshark里其实被解析成三层:TPKT层、COTP层、S7COMM层。这三层的整体关系就像寄快递:
- TPKT是快递外包装,负责告诉接收方"这个包裹总共多大、怎么拆"。
- COTP是快递盒里的内部填充层,负责传输控制,比如要不要分片、序号是多少。
- S7COMM才是真正的内容,里面装的才是你关心的读DB、写DB、读诊断信息等请求和响应。
下面是一条典型的S7读请求报文的原始字节,我用Wireshark抓取后导出的十六进制形式:
03 00 00 21 02 f0 80 32 01 00 00 00 00 08 00 00 04 01 12 0a 10 02 00 01 00 00 84 00 00 00 00 00 08一眼看上去全是十六进制,但拆开之后逻辑非常清晰。先看最前面的03 00 00 21,这是TPKT头:第一个字节03表示协议版本,第二个字节00是保留位,后面两个字节00 21表示整个报文的长度——0x21换算成十进制是33字节。从报文头数到最后一个字节,刚好33字节,分毫不差。这就是TPKT层的意义,让接收方知道到哪里算结束。
3.2 连接建立阶段的握手细节
前面说过,TCP连接建立起来之后,并不会立刻开始传S7报文,而是先有一组COTP握手报文。这组报文在Wireshark里显示为COTP,而不是S7COMM,很多新手在这里懵过。
首先是客户端发起的CR(Connection Request)报文,典型的字节如下:
03 00 00 16 11 e0 00 00 00 01 00 c1 01 00 c2 02 03 01逐一拆解:
- 03 00 00 16:TPKT头,总长度22字节。
- 11:COTP头长度17字节。
- e0:PDU类型,0xE0表示CR(连接请求)。
- 00 00:目标引用,初始为0。
- 00 01:源引用,客户端随机生成。
- 00:协议类别。
- c1 01 00:这是"呼叫TSAP",值为0x0100,表示客户端(编程设备/上位机)的传输层地址。
- c2 02 03 01:这是"被叫TSAP",值为0x0301,对应S7-1200 PLC的可连接TSAP。
这组TSAP就是前面说的"前台登记"的具体内容,它直接决定PLC是否愿意和你通信。如果PLC是S7-300(CPU槽位2),被叫TSAP通常写c2 01 02;如果是S7-1500,常见值是c2 01 00。这个细节经常会坑人,后面单独讲。
紧接着PLC会回一个CC(Connection Confirm)报文,表示"前台登记成功,给你发门禁卡"。CC报文结构类似,PDU类型变成0xD0,目标引用变成客户端之前发来的0001,同时会带上服务端自己的TSAP信息。到这一步,COTP握手完成,后续报文就可以封装S7 PDU了。
3.3 读请求与读响应报文逐字节拆解
COTP握手完成后,真正的S7读写请求就来了。还是以上面那条读请求为例,去掉TPKT和COTP层,剩下的核心S7COMM部分是这样:
32 01 00 00 00 08 00 00 04 01 12 0a 10 02 00 01 00 00 84 00 00 00 00 00 08第一部分是S7 Head:
- 32:协议ID,固定为0x32,所有S7通信报文都有这个头。
- 01:ROSCTR(报文类型),0x01表示JOB(主动请求),0x03表示ACK_DATA(带数据的确认响应),0x07表示USERDATA(用于符号寻址、时钟、SZL读取等功能)。
- 00 00:冗余与协议标志。
- 00 08:PDU引用,每次请求自增,用于匹配请求和响应。
- 00 00:参数长度,此处为0,因为参数部分还未开始?不对,这里需要再精确一点。
这里我要纠正一下上面的标注,更准确地说,S7 Head一共有12字节,其中第7、8字节是参数长度,第9、10字节是数据长度。我们重新排列上面那段:
- 32:协议ID
- 01:ROSCTR,JOB
- 00:冗余
- 00:协议标志
- 00 08:PDU引用
- 00 00:参数长度
- 00 04:数据长度?不对,抓包里这里显示是00 00 04 01,所以参数长度是0x0000?这个说法会让人困惑。
为了避免误导,我把一条标准的、带完整参数部分的读请求放上来,仔细对照Wireshark的解析结果来看。将原报文的S7COMM部分按实际Wireshark解析划分:
- 协议ID:32
- ROSCTR:01(JOB)
- 冗余:00
- 协议标志:00
- PDU引用:00 08
- 参数长度:00 08
- 数据长度:00 00
- 参数部分(8字节):04 01 12 0a 10 02 00 01
- 数据部分(最后4字节):00 00 00 08?也不对。
这里我需要坦诚地说,抓包值的细节必须实际对着Wireshark看,不同版本、不同PLC型号会有差异。为了避免纸上谈兵,我把参数部分的核心结构讲清楚,这才是重点:
读请求参数部分是一串变长结构,核心格式是:
- 04:功能码,Read Var(读变量)。
- 01:本次请求的变量数。
- 12:描述这个变量的地址项长度,18字节。
- 0a:地址规范类型,S7ANY。
- 10:传输大小,0x10表示字节数组。
- 02 00:元素数量,即读取2个字节。
- 01 00:DB号,DB1。
- 84:存储区标识,0x84代表数据块DB区。
- 00 00 00 00:字节地址,从DBD0开始读。
- 00:位地址。
读响应报文的结构则变成:
32 03 00 00 00 08 00 02 00 04 00 00 04 01 00 00 ff 04 00 00 12 34其中ROSCTR变成了0x03(ACK_DATA),第7、8字节是参数长度0002,第9、10字节是数据长度0004。参数部分是两个字节的返回项信息,数据部分是实际的4字节数据。看到0xFF标志位,这是读成功的标志,后面跟着的00 12 34等才是你真正要的数据值。
这一步搞明白了,从抓包到协议实现的中间环节就打通了。
4. 实战项目:读一个DB块,从报文到代码
4.1 项目背景与连接参数
理论拆解清楚了,接下来上点硬货。我之前做过一个小项目:用一台普通工控机,完全不依赖西门子通信库,直接通过TCP Socket和S7-1200通信,循环读取设备状态。这个项目最大的价值就是证明了一件事——只要把协议报文的逻辑搞明白,S7通信完全可以"裸写"。
假设场景如下:
- PLC型号:S7-1200 CPU 1214C
- PLC IP:192.168.0.10,端口102
- 连接参数:本地TSAP(呼叫TSAP)0x0100,远程TSAP(被叫TSAP)0x0301
- 目标:读取DB1.DBD0处的一个4字节REAL数据
这里再提一下TSAP的记忆方法:S7-1200常见远程TSAP是0301,S7-300槽位2就是0102,S7-1500常见0100。具体一定要以博途连接配置里的TSAP为准,很多兼容性问题都出在这个参数没有对齐上。
4.2 用Python实现最小S7客户端
我用Python 3 + 标准库socket实现了一个最小客户端,没有第三方依赖,逻辑完全复刻手工构造报文的流程:
import socket import struct PLC_IP = "192.168.0.10" PLC_PORT = 102 def tpk_cotp_dt(data: bytes) -> bytes: """封装TPKT + COTP DT Data + S7 PDU""" cotp_len = len(data) + 3 return struct.pack(">BBH", 0x03, 0x00, len(data) + 7) + \ bytes([0x02, cotp_len, 0x80]) + data def build_read_request(pdu_ref: int) -> bytes: """构造读DB1.DBX0.0起的2字节请求""" s7_header = bytes([0x32, 0x01, 0x00, 0x00]) + \ struct.pack(">H", pdu_ref) + \ struct.pack(">HH", 8, 0) params = bytes([0x04, 0x01, 0x12, 0x0a, 0x10]) + \ struct.pack(">H", 2) + \ struct.pack(">H", 1) + \ bytes([0x84]) + \ struct.pack(">I", 0)[1:] + \ bytes([0x00]) return tpk_cotp_dt(s7_header + params) def read_from_plc(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((PLC_IP, PLC_PORT)) # 1. 发送COTP CR连接请求 cr = bytes.fromhex("03 00 00 16 11 e0 00 00 00 01 00 c1 01 00 c2 02 03 01") sock.sendall(cr) resp = sock.recv(1024) if len(resp) < 5 or resp[5] != 0xd0: print("COTP连接建立失败") return # 2. 发送S7读请求 req = build_read_request(0x0001) sock.sendall(req) resp = sock.recv(1024) # 3. 简单解析:最后4字节是数据 if resp[5] == 0xf0 and resp[6] == 0x80 and resp[7] == 0x32: print("响应数据:", resp[-4:].hex()) sock.close() if __name__ == "__main__": read_from_plc()代码里的关键点我解释一下。首先封装TPKT和COTP DT的时候,长度字段是动态计算的,这里最容易算错,建议对着Wireshark的抓包逐字节核对。其次,build_read_request函数里参数的拼装顺序和第三节里拆解的顺序完全一致:功能码、变量数、地址项长度、传输大小、元素数量、DB号、存储区、字节地址、位地址。代码和报文是一一对应的,这就是从抓包反推协议带来的最大好处——不需要看任何库的源码,你也能自己实现协议。
这里再补充一个经验:抓包时如果发现发出去的请求PLC没有响应,优先用十六进制比对Wireshark里西门子软件自己发出的一条成功的读请求,看你的报文和它有什么差异,排查效率远高于盲试。
4.3 结合抓包验证代码正确性
代码写完之后,如何在真机上验证它到底对不对?我的习惯是抓包,看自己的程序发出去一段报文,然后对比Wireshark里正常报文的差异,重点看三处:
- 第一,COTP CR报文里的被叫TSAP是否与PLC匹配,sock连接建立之后第一步是不是立刻发送了CR;
- 第二,S7 PDU里的PDU引用是否每次请求都在自增,如果你连续发两个请求,PDU引用还是同一数字,有些PLC版本会不响应;
- 第三,参数部分的长度字段是否准确,长度算错一个字节,整个请求就是废报文。
有一次我在现场排查一个自研网关,现象是"十个请求偶尔成功一个",抓包后发现问题不是出在报文格式上,而是程序在收到响应之前又发了一个新请求,违反了一条TCP连接上的请求响应顺序。S7协议通常要求请求到达后,响应返回之前不要发送下一条请求,某些固件版本在并发请求超过连接资源限制后,表现得极其不准。这是写自研S7客户端时非常容易踩的浅层逻辑坑。
5. 现场排查实录:S7通信的常见坑
5.1 抓不到包或只能抓到TCP握手
场景还原:笔记本和PLC接在同一个交换机上,Wireshark过滤条件也正确,但抓不到S7报文,或者只能看到TCP三次握手,后续立刻出现RST(重置连接)。
排查思路是下面几条,按优先级排序:
- 检查网卡混杂模式是否开启。Wireshark抓交换机镜像口或者直连PLC时尤其重要,如果没开混杂模式,只能抓到发给本机的单播包,其他流量直接过滤掉。
- 检查是否有防火墙阻挡TCP 102端口。Windows的防火墙经常干这事,程序主动连接PLC时最好临时关掉防火墙测试。
- 确认PLC是否真的启用了PUT/GET访问,或者是否已经在博途里勾选了"允许来自远程对象的通信访问"。S7-1200在未配置连接机制时,直接通过TCP 102建立S7连接会被拒绝,抓包看到的现象就是连接被重置。
这里顺带解答热词里一个很常见的困扰:"西门子PLC与施耐德变频器通过Modbus通讯"这种跨品牌组网,其实和S7不冲突。S7协议是PLC与上位机、PLC与PLC之间的应用协议,而变频器走Modbus只负责驱动层的读写。现场抓包时记住一个原则:抓S7就过滤TCP 102,抓Modbus就过滤TCP 502,两者不要混在一个过滤器里看。
5.2 连接建立失败与TSAP对齐问题
TSAP不对齐是S7通信异常里最高频的问题之一。我见过一个最典型的案例:同事用S7-1200的IP抓包看连接,报错总是"Connection terminated by remote host",抓包后仔细对比发现,他代码里写的是c2 02 03 02,但博途组态里PLC实际的TSAP是0301,TSAP差一个数字,连接直接被拒。
解决办法也很简单:
- 如果PLC不明确,优先抓一次用西门子官方软件(博途、TIA Portal、SIMATIC Manager)建立连接时的报文,看Wireshark解析出的被叫TSAP到底是多少,照着填准没错。
- S7-1200的TSAP通常与CPU槽位相关,0301对应槽位1,0201对应HMI连接等。
- S7-300如果是CPU 315-2 PN/DP,TASP的常见值是0102或者0103,以实际组态为准。
5.3 S7-1500优化DB块访问问题
S7-1500的DB快有"优化"和"非优化"之分。这个坑特别隐蔽,因为它不是在通信层报错,而是表现为"连接正常、读写指令正常,但返回的数据不对"。
简单解释一下:优化DB块是S7-1500的默认属性,变量在内存里的位置由编译器自动分配,没有固定的偏移地址。你用S7协议按DB号和偏移去读,如果偏移不是实际分配的内存地址,读到的数据自然不是预期值。
解决方案有两个:一是在博途里把目标DB块的属性改为"非优化访问",这样变量就有了固定偏移,可以直接用传统方式按地址读写;二是在抓包时用Wireshark尝试解析S7-1500的符号寻址(ROSCTR为0x07的USERDATA报文),但这类报文的解析难度要高出不少。
5.4 长连接频繁断开与排查技巧速查表
最后把常见的S7通信问题整理成一个速查表,方便后面翻查。
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| TCP能通,COTP握手被拒 | TSAP参数不对 | 抓正规软件通信报文对照TSAP |
| 握手成功,但S7请求无响应 | PDU引用未自增/参数长度错误 | 逐字节比对Wireshark报文 |
| 响应返回码非0xFF | 地址越界/DB不存在 | 核对DB号和偏移是否在有效范围内 |
| 读非优化DB正常,读优化DB异常 | DB块为优化属性 | 在博途设置为非优化访问 |
| 长时间运行后连接断开 | PLC连接资源耗尽 | 检查程序是否每次请求后关闭连接 |
| 抓包有S7连接,但无数据交互 | 客户端未发送JOB请求 | 确认代码执行到了发送步骤 |
| 上位机同时连多个PLT经常掉线 | 连接数超出PLC上限 | 减少并发连接,复用已有连接 |
5.5 Python调用PyShark抓包失败怎么办
再补一个开发过程中经常碰到的问题。有人想在Python里直接调用Wireshark抓包,写自动化脚本分析大量报文,于是用到PyShark库,结果抓包一直失败,尤其模拟器或旧版本环境里更常见。有两个点是查了无数遍才发现的关键:
一是PyShark只是一个壳,真正干活的是tshark可执行文件,所以本机必须装了Wireshark并且tshark能被PyShark找到,通常需要在配置里显式指定tshark路径:
import pyshark cap = pyshark.LiveCapture( interface="eth0", bpf_filter="tcp port 102", tshark_path="C:/Program Files/Wireshark/tshark.exe" ) cap.sniff(timeout=30)二是权限问题。非管理员权限下,网卡抓包经常抓不到任何流量,Linux下需要在启动Python之前给dumpcap授权或直接用root运行,Windows下则是右键以管理员身份运行终端。PyShark相关的问题,十有八九就出在这两点上。
最后的实操体会
S7协议本身并不难,难的是第一次上手时面对一大串十六进制报文无从下手。我的经验是,面对任何通信协议,都先不要急着写代码,先用Wireshark把正常通信的报文抓下来,然后用十六进制逐个字段去查资料对照,搞懂了之后代码就是在复述你抓包看到的字节流。读完这篇文章如果你只记住一件事,那我希望是:抓包不是排障的最后手段,而是理解协议的第一现场。以后遇到通信不上、数据不对、连接不稳这类问题,先抓包,再动手,绝大多数坑都能用这个习惯绕过去。