S7协议通信实战:从握手到数据读写的深度解析

发布时间:2026/9/28 21:28:52
S7协议通信实战:从握手到数据读写的深度解析 1. 工控现场为什么要死磕S7协议搞工控的兄弟大多有过这种经历产线上位机要采一批西门子PLC的数据拿了个现成的库连上能读但偶尔断、偶尔慢、偶尔读回来的浮点数明显不对。翻日志只看到一句“连接超时”剩下的全靠猜。这时候如果对S7通信协议本身的握手流程和数据读写机制没有底排查基本就是碰运气。S7通信协议是西门子S7系列PLC对外通信的核心协议族覆盖S7-200 SMART、S7-300、S7-400、S7-1200、S7-1500这几代主力机型。它跑在TCP/IP之上也有基于ISO-on-TCP的变体负责上位机、HMI、SCADA、MES与PLC之间的数据交换。说白了你用的WinCC、组态王、Kepware、各种开源PLC库底层跟PLC对话用的都是这套东西。这篇文章面向三类人一是刚接触工控上位机开发、想搞明白“为什么连上了还读不到数据”的工程师二是做设备集成、需要自己写通信模块的开发者三是运维现场、经常被“通信闪断”折磨的自动化从业者。我会从协议分层讲到握手细节再落到数据读写的报文结构和实操代码把那些文档里不写、但现场一定会踩的坑摊开讲。需要先说明一点S7协议是西门子的私有协议官方并没有像Modbus那样公开一份完整规范。市面上流传的文档大多来自社区逆向和实测总结不同型号、不同固件版本之间会有差异。所以下面讲的内容是基于常见实践和大量实测归纳出来的通用逻辑具体到某个型号时务必以实测为准。2. 协议分层与连接建立的底层逻辑2.1 S7协议到底跑在哪一层很多人一上来就问“S7的端口是多少”答案通常是102。但102端口背后其实有两套不同的承载方式这个区别直接决定了你握手报文长什么样。第一套是ISO-on-TCPRFC1006这是S7-300/400时代的经典方式也是S7-1200/1500默认支持的方式。它在TCP之上又套了一层用TPKT和COTP两个小协议来管理连接。你可以把它理解成TCP负责把字节流送到TPKT负责标记“这一包有多长”COTP负责“这条连接是给谁的”。第二套是原生TCP上的S7部分新型号和高版本固件支持省掉了COTP那层握手更简洁。但兼容性不如前者现场遇到老设备时还是得走ISO-on-TCP。这里有个关键点102端口只是ISO-on-TCP的默认端口不是S7协议本身的端口。有些设备会改端口有些网关会做端口映射所以排查时不能死认102。2.2 TPKT和COTP各自管什么TPKT的报文头只有4个字节结构非常固定字节位置含义常见值0版本号0x031保留0x002-3后续报文总长度大端变长TPKT本身不做任何逻辑它就是个“长度标签”。但正因为简单它也是最容易被忽略的地方——如果长度字段算错了PLC会直接丢弃整包连错误码都不给。COTP的报文头复杂一些核心字段是TPDU类型和TSAP。TPDU类型标识这一包是连接请求CR、连接确认CC、数据传输DT还是断开DR。TSAP则是“传输服务访问点”相当于给PLC内部不同服务分配的“门牌号”。2.3 TSAP连接握手的隐形门槛TSAP是S7握手环节里最容易翻车的地方。上位机发起连接时要在COTP的CR报文里带上两个TSAP源TSAP自己和目标TSAPPLC。目标TSAP的常见构造规则是这样的前两个字节通常是0x03和0x00后面跟着机架号Rack、槽号Slot以及连接类型。比如经典的S7-300/400目标TSAP经常是03 00 02 00或03 00 01 00这种形式其中02/01对应机架00对应槽位。但S7-1200/1500的规则不一样。它们的TSAP往往跟连接资源号绑定常见的是03 00 00 00开头后面跟一个动态分配的编号。如果你拿S7-300的TSAP去连S7-1500大概率会被拒绝而且拒绝原因不会明确告诉你“TSAP不对”。提示TSAP不匹配是“TCP连上了但S7握手失败”的头号原因。排查时先用抓包工具看PLC返回的CC报文里它自己的TSAP是什么再反推你该填什么。2.4 三次握手在S7里的真实位置网络热词里“TCP三次握手”被反复提及但很多人搞混了一件事TCP三次握手和S7握手是两回事。TCP三次握手SYN、SYN-ACK、ACK发生在传输层目的是建立一条可靠的字节流通道。这一步成功了只说明“网络通了、端口开了”不代表PLC愿意跟你做S7通信。S7握手发生在TCP通道建立之后走的是COTP的CR/CC交换。上位机发CRPLC回CC这一步成功了才说明“PLC认可你的身份和TSAPS7会话建立了”。之后才轮到S7层的Setup Communication设置通信参数和真正的数据读写。所以现场遇到“连不上”时第一步要分清是卡在TCP层还是卡在COTP层。用抓包工具看如果只有SYN没有SYN-ACK是网络或端口问题如果有完整的TCP握手但没有CR/CC是S7握手没发起如果CR发了但CC没回或回了拒绝是TSAP或连接资源问题。3. 握手报文逐字段拆解3.1 连接请求CR报文长什么样一条典型的COTP连接请求报文去掉TPKT头之后核心字段大致是这样排列的长度指示标识COTP头本身的长度TPDU类型0xE0表示CR连接请求目标引用通常填0x0000源引用上位机自己生成的连接标识Class/Option协商参数常见0x00源TSAP长度 源TSAP目标TSAP长度 目标TSAPTPDU大小协商最大报文长度常见0x0A002560字节这里每个字段都有讲究。源引用是上位机自己定的PLC在CC报文里会原样带回用来配对请求和响应。如果你并发发起多条连接源引用必须唯一否则会串包。TPDU大小决定了后续单包数据的上限。填太小会导致大数据块被拆成很多包效率低填太大有些老设备不接受。0x0A00是个比较稳妥的通用值。3.2 连接确认CC报文怎么读PLC返回的CC报文结构和CR类似但TPDU类型变成0xD0。重点看两个地方一是目标引用它应该等于你CR里的源引用用来确认这是对你那条请求的响应。如果对不上说明PLC那边连接管理出了问题。二是PLC自己的TSAP它会出现在CC报文里。这个值非常有用——当你不知道目标TSAP该填什么时可以先随便填一个发CR看PLC回什么有些设备即使拒绝也会在错误响应里带上自己的TSAP信息。3.3 Setup CommunicationS7层的参数协商COTP握手成功后还没到读写数据的地步。上位机要先发一条S7层的Setup Communication报文协商几个关键参数参数含义常见取值Max AmQ Calling上位机最大并行作业数1-8Max AmQ CalledPLC最大并行作业数1-8PDU Length单次数据单元最大长度240-960PDU Length是这里最关键的。它决定了你一次读写能带多少数据。S7-300常见240字节S7-1200/1500可以到960字节甚至更大。如果你请求的数据长度超过协商的PDUPLC会返回错误或者要求你分多次读。注意PDU Length是双方协商的结果不是你单方面说了算。你填960PLC可能只接受480最终以PLC返回的值为准。写代码时要把这个值存下来后续所有读写都按它来切分。3.4 一个完整的握手时序把上面的流程串起来一次成功的S7连接建立大致是TCP三次握手SYN / SYN-ACK / ACK上位机发COTP CRPLC回COTP CC上位机发S7 Setup CommunicationPLC回Setup Communication响应带上协商后的PDU Length连接可用开始数据读写这六步里任何一步失败表现都是“连不上”但原因完全不同。抓包是唯一能快速定位的手段没有之一。4. 数据读写报文结构与地址映射4.1 S7读写报文的基本结构S7的数据读写走的是COTP的DT数据传输报文里面包着S7 PDU。一条S7读请求的核心字段包括协议标识固定0x32ROSCTR0x01表示读0x03表示写PDU引用用来配对请求和响应参数长度 参数区数据长度 数据区参数区里最关键的是功能码和Item。读操作的功能码是0x04写是0x05。每个Item描述一个要操作的变量包含变量规格、长度、地址等信息。4.2 地址是怎么编码的S7的地址编码是新手最容易懵的地方。它不是一个简单的偏移量而是由区域标识 字节地址 位地址组合而成。区域标识常见的有区域标识值说明输入I0x81过程映像输入输出Q0x82过程映像输出位存储M0x83标志位数据块DB0x84需配合DB号定时器T0x1D计数器C0x1C地址部分用3个字节表示格式是“字节地址 位地址”。比如M10.3字节地址是10位地址是3。对于DB块还要额外指定DB号。举个具体例子读DB1.DBD0DB1里从第0字节开始的4字节双字Item里的地址字段大致是区域0x84、DB号1、字节地址0、位地址0、长度4。写操作结构一样只是多带数据区。4.3 位读写和字节读写的差异位操作比如读M10.3和字节操作比如读MW10在报文层面差别不大但有一个坑位地址字段在字节操作时通常填0而长度字段决定读几个字节。如果你读MW102字节地址填字节10、位0、长度2如果你读M10.3地址填字节10、位3、长度1但返回的数据里只有那一位有效。实测下来很多库在处理位操作时会先把整个字节读回来再在本地做位运算。这样做的好处是减少往返次数坏处是如果那个字节里其他位在快速变化你读到的可能不是同一时刻的值。4.4 批量读写与PDU切分当你一次要读几百个字节时超过PDU Length的部分必须切分。切分策略有两种一种是按Item切分把多个变量分成几组每组总长度不超过PDU。这种方式适合变量分散的场景。另一种是按连续地址切分如果变量在地址空间里是连续的可以一次读一大块再在本地按偏移量拆。这种方式效率高但要求地址连续。我个人的经验是优先按连续地址读大块因为S7协议对单次大块读写的支持比多次小请求好得多。一次读960字节比读10次96字节快不止10倍因为每次请求都有固定的报文开销和PLC处理延迟。5. 实操从零写一个S7读数据流程5.1 环境准备与工具选型要自己实现S7通信你需要一台能ping通的西门子PLC或PLCSIM Advanced仿真抓包工具Wireshark过滤tcp.port 102一个能发原始TCP报文的调试工具比如Python的socket或者专门的报文构造脚本PLC侧的连接资源确认有些型号默认不允许PUT/GET通信需要在硬件组态里勾选“允许来自远程对象的PUT/GET通信访问”最后这一条是新手最常踩的坑。S7-1200/1500出于安全考虑默认关闭了PUT/GET。你报文写得再对PLC也会拒绝。这个选项在博途的PLC属性里连接机制那一栏。5.2 构造TCP连接和COTP握手用Python举例核心步骤是import socket import struct # 1. 建立TCP连接 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((192.168.0.1, 102)) # 2. 构造COTP CR报文 # TPKT头: 03 00 长度 # COTP: 长度 E0 目标引用 源引用 class 源TSAP 目标TSAP TPDU大小 src_tsap b\x01\x00 # 上位机自己的TSAP dst_tsap b\x03\x00\x00\x00 # 目标TSAP需按实际PLC调整 cotp b\x11\xe0\x00\x00\x00\x01\x00 cotp bytes([len(src_tsap)]) src_tsap cotp bytes([len(dst_tsap)]) dst_tsap cotp b\x0a # TPDU大小高字节 cotp b\x00 # 占位实际长度需重算 # 补全TPKT长度 tpkt b\x03\x00 struct.pack(H, len(cotp) 4) sock.send(tpkt cotp) # 3. 接收CC响应 resp sock.recv(1024) # 检查resp[5]是否为0xD0CC这段代码里目标TSAP是变量需要根据PLC型号调整。S7-300常见03 00 02 00S7-1200/1500常见03 00 00 00加动态编号。如果CC没回来先换TSAP试。5.3 发送Setup CommunicationCOTP握手成功后发S7 Setup Communication# S7 Setup Communication报文 # 协议标识0x32, ROSCTR 0xF0, 保留, PDU引用, 参数长度, 参数区 setup b\x32\xf0\x00\x00\x00\x01\x00\x08\x00\x00 setup b\xf0\x00\x00\x01\x00\x01\x03\xc0 # 协商参数PDU Length 960 tpkt b\x03\x00 struct.pack(H, len(setup) 4) sock.send(tpkt setup) resp sock.recv(1024) # 解析resp里的PDU Length存下来这里的0x03\xc0是PDU Length字段0x03C0就是960。PLC返回的响应里会带上它实际接受的值后续按这个值切分数据。5.4 构造读请求并解析响应读DB1.DBD0的请求# S7读请求 # 协议标识0x32, ROSCTR 0x01, 保留, PDU引用, 参数长度, 数据长度 read_req b\x32\x01\x00\x00\x00\x02\x00\x0e\x00\x00 # 参数区: 功能码0x04, Item数1 read_req b\x04\x01 # Item: 变量规格0x12, 长度, 区域0x84, DB号, 地址 read_req b\x12\x0a\x10\x02 # 规格和长度 read_req b\x00\x01 # DB号1 read_req b\x84\x00\x00\x00 # 区域DB, 字节0, 位0 read_req b\x00\x04 # 读4字节 tpkt b\x03\x00 struct.pack(H, len(read_req) 4) sock.send(tpkt read_req) resp sock.recv(1024) # 解析resp: 跳过TPKT和COTP头找到数据区按大端解析浮点数响应报文里数据区在参数区之后。对于DBD0这种4字节浮点数按大端IEEE 754解析即可。如果读回来是乱码先确认字节序再确认地址对不对。5.5 写操作的差异点写操作功能码0x05和读的结构几乎一样区别在于ROSCTR变成0x03参数区里每个Item多一个“传输大小”字段报文末尾要带数据区写入的数据按大端排列写位操作时数据区里只有那一位有效但很多实现会写整个字节。这时候要注意读-改-写的顺序不能乱否则会覆盖掉同一字节里其他位的状态。6. 现场排查那些文档不写的坑6.1 连接资源耗尽S7-1200/1500对并发连接数有硬限制。S7-1200常见是8个S7-1500多一些但也有限。如果你的程序反复建连不释放很快就会把资源占满之后所有新连接都被拒绝。表现是一开始能连跑一段时间后连不上重启PLC又好了。排查方法是看PLC的连接资源使用情况博途里可以在线诊断看到。解决方法是复用连接不要每次读写都新建。6.2 PDU Length协商失败有些老固件的PLC对PDU Length很敏感。你填960它可能直接拒绝Setup Communication。这时候要退回到240或480试。实测下来S7-300系列用240最稳S7-1200/1500可以上960。6.3 浮点数读回来不对三个可能原因字节序、地址偏移、数据类型。S7的浮点数是IEEE 754大端但有些库默认按小端解析。地址偏移方面DBD0是第0字节DBD4是第4字节别把DBW和DBD搞混。数据类型方面Real是4字节浮点DInt是4字节整数读之前先确认PLC里定义的是什么。6.4 通信闪断的排查顺序现场遇到闪断按这个顺序查物理层网线、交换机、端口协商TCP层抓包看是否有RST或重传COTP层连接是否被PLC主动断开应用层是否有超长请求导致PLC处理超时资源层连接数是否接近上限这个顺序是从底层往上层查因为底层问题会伪装成上层问题。我见过太多人一上来就改代码结果发现是网线接触不良。6.5 常见问题速查表现象可能原因排查手段TCP连不上端口错、防火墙、IP错ping telnet 102TCP通但S7握手失败TSAP错、PUT/GET未开抓包看CC响应握手成功但读不到数据地址错、区域标识错核对DB号和偏移读回来是乱码字节序、数据类型按大端解析确认类型跑一段时间断连接资源耗尽查PLC连接诊断大数据块读失败PDU超限按协商值切分7. 几个容易被忽略的细节7.1 连接保持与心跳S7协议本身没有强制的心跳机制但TCP层有keepalive。如果中间有防火墙或NAT设备空闲连接可能被悄悄断开。稳妥的做法是应用层定期发一条轻量请求比如读一个字节保持连接活跃。7.2 多线程下的PDU引用管理PDU引用是用来配对请求和响应的。单线程下随便填都行但多线程并发时如果两条请求用了同一个PDU引用响应就分不清是谁的。正确做法是用一个原子计数器分配PDU引用保证全局唯一。7.3 错误码的解读S7响应里的错误码分两类一类是协议层错误比如PDU格式错一类是数据层错误比如地址越界。数据层错误码常见的有0x05地址越界、0x0A对象不存在。看到错误码先别急着改代码对照PLC的地址表确认变量是否存在。7.4 仿真环境与真实设备的差异PLCSIM Advanced能模拟S7通信但它的TSAP规则和真实设备不完全一样PDU Length的接受范围也可能更宽松。在仿真上跑通的代码到真机上可能因为TSAP或PDU问题失败。所以最终验证一定要上真机。8. 关于协议实现的一点个人体会我最早接触S7协议时也是拿现成库直接用出了问题就换库。后来被逼着自己抓包分析才发现很多“库的bug”其实是自己对协议理解不到位。比如TSAP很多库把它做成一个配置项默认值只对特定型号有效换个PLC就失效。如果你知道TSAP的构造逻辑自己算一下就能解决不用等库作者更新。还有PDU Length库通常会自动协商但协商失败时的降级策略各家不一样。有的库直接报错有的库默默降到最小值。了解协议本身你就能判断哪种行为是合理的哪种是在掩盖问题。最后说一个实操建议抓包工具要常开。S7通信的很多问题看报文比看日志快十倍。日志只会告诉你“失败了”报文会告诉你“在哪一步、因为什么失败”。这个习惯养成之后排查效率会有质的提升。后续如果要做更复杂的场景比如跨网段、多PLC并发、大数据量采集可以在连接池和请求队列上做文章。核心思路是复用连接、批量读写、异步处理把协议层的开销摊薄。这些展开又是另一个话题了先把握手和数据读写这两块吃透剩下的都是在这之上的工程优化。