S7协议从握手到读写:西门子PLC通信报文构造与实操指南
发布时间:2026/9/28 16:07:54来源:尧图网络
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-TCPRFC1006的关系。严格来说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字节固定0x03TPKT保留1字节固定0x00TPKT长度2字节整个报文总长度大端COTP长度1字节从COTP长度字节之后算起的长度COTP类型1字节连接请求是0xE0目标引用2字节请求时填0x0000源引用2字节自己生成随便填但后续要用Class/Option1字节通常0x00参数代码1字节0xC0表示TPDU大小参数长度1字节0x01TPDU大小1字节0x0A表示1024字节实际抓包看到的连接请求TPKT长度通常是0x0016也就是22字节。COTP长度是0x1117字节。源引用我一般用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作业请求保留0x0000PDU引用2字节自己维护的序号参数长度2字节数据长度2字节0x0000功能码0xF0Setup Communication保留0x00最大AmQ2字节并发作业数通常1最大PDU2字节请求的PDU大小最大AmQ调用2字节通常1最大PDU这个字段很关键。你填一个值PLC会回复它能接受的值。比如你填960S7-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大小。如果你填0x0A1024但实际发送的报文超过1024PLC会拒绝。不过S7的Setup报文很短一般不会超。真正要注意的是后续读写报文如果PDU协商成480你的TPKT长度不能超过480加上各种头的开销。第四个坑是连接保持。S7连接建立后如果长时间不发数据PLC可能会主动断开。我一般每隔30秒发一个空的读请求或者保持报文具体看PLC型号。S7-1200的默认超时好像是几分钟但不同固件版本可能不一样实测最稳的是20到30秒发一次心跳。4. 数据读写报文的构造与参数计算4.1 读报文的完整结构读操作的功能码是0x04。一个典型的读DB块报文长这样字段值说明TPKT头03 00 00 xxCOTP头02 F0 80S7协议ID0x32ROSCTR0x01作业请求保留0x0000PDU引用2字节递增序号参数长度2字节通常是0x000E数据长度2字节0x0000功能码0x04读项数1字节读几个变量变量规格0x12固定变量长度1字节后续地址描述的长度存储区1字节0x81DB0x82输入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 环境准备与工具选型要复现这套流程你需要一台西门子PLCS7-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协议IDROSCTR保留 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就是96001 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, # 语法IDS7-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:16data_len] # 跳过返回码和长度描述 return data[4:]这里有几个关键点。语法ID在S7-1200上是0x10S7-200 SMART上是0x12这个差异坑了我很久。DB号是2字节地址是3字节。返回的数据前面有4个字节的返回码和长度描述要跳过。读出来的浮点数用struct.unpack(f, data[i:i4])解析。注意S7的浮点数是IEEE754大端跟网络字节序一致。5.4 写数据的实现与验证写一个浮点数到DB1.DBD0def 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填错抓包对比正常通信的TSAPSetup被拒绝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语法ID0x100x100x12TSAP01 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通信。每个方向都够写一篇长文有机会再聊。
网站建设高端定制企业官网