尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python+Snap7实现西门子PLC数据读写:从环境配置到工程实践

Python+Snap7实现西门子PLC数据读写:从环境配置到工程实践 在工控现场摸爬滚打这些年我越来越确信一件事让PLC数据真正流动起来比单纯会写梯形图重要得多。生产线的设备数据、能耗数据、质量数据全部躺在控制器里如果只是靠人肉抄表或者专用组态软件导来导去效率实在感人。于是Python和西门子PLC之间的数据交互成了很多工程师绕不开的课题我用得最顺手的方案就是Snap7。Snap7是一个开源的S7协议通信库支持西门子S7-200/300/400/1200/1500系列PLC底层是C写的官方提供了C、C、Python、Node.js等语言的绑定。配合python-snap7这个包我们能在Windows、Linux、macOS甚至树莓派上用几行代码读写PLC的DB块、M区、输入输出区把数据直接送进数据库、可视化大屏或者AI模型里。这篇文章我会从环境准备讲到实际读写把踩过的坑、常用的工程化写法和排查思路一起整理出来。适合已经会用Python、但刚接触工业通信的自动化工程师也适合想摆脱商用上位机束缚、自己做数据系统的朋友。1. Python与西门子PLC通信选型时我在想什么1.1 一个原本被上位机绑死的需求之前做一条产线的数据采集系统甲方要求把十几台S7-1200的产量、报警、电流曲线全部汇聚到一个工业物联网平台里。一开始的方案是用组态软件画面画得漂亮但等到要对接内部的数据分析系统时问题就来了组态软件更像一个封闭的盒子数据导出要靠OLE DB、ODBC、或者它的专用API文档不友好开发效率也低。而且组态软件本身是按点位授权收费的点数一多成本立刻上去。后来我换了一个思路用Python直接通过Snap7读PLC地址把数据写到自己的服务里。这样灵活性一下就上来了——数据库用什么、接口怎么定、数据怎么清洗全都可以自己控制。现场调试的时候带着一台笔记本装好Python环境接上网线就能连PLC不用等上位机工程重新编译发布效率提升了不止一档。1.2 为什么是Snap7而不是OPC UA或Modbus很多朋友会问既然要通信为什么不干脆用OPC UAOPC UA确实是目前工业通信的主流方向跨平台、带安全机制、信息模型完善西门子新款PLC也原生支持服务器端。但有个很现实的问题OPC UA在西门子PLC上往往需要额外的授权旧型号S7-300/400支持得不够好配置也相对繁琐。如果你的项目只是想把数据读出来做分析杀鸡用牛刀反而给自己添麻烦。Modbus TCP是另一种常见选择不过西门子PLC的Modbus支持依赖于功能块或者专门的通信处理器S7-1200/1500需要调用MB_CLIENT之类的指令才能导入导出数据而且Modbus能访问的数据区映射规则比较死板碰到复杂数据类型会特别痛苦。Snap7直接走S7协议也就是西门子自己的通信协议不需要在PLC程序里写任何通信代码只要硬件支持PUT/GET通信就能直接读写DB块、M区和I/Q区。对于西门子用户来说这是最贴近原生、最省事的路径。1.3 跨平台的底气S7协议与libsnap7Snap7的核心是一个叫libsnap7的静态库它实现了S7协议的客户端和服务端。官网支持Windows、Linux、macOS、BSD甚至还有ARM版本所以在树莓派上挂一个采集服务完全没问题。Python-Snap7只是把底层的libsnap7封装成Python对象因此性能瓶颈不在封装层而在网络和PLC本身的通信能力上。这里有个基础但重要的点S7协议和Modbus最大的不同是它不是一个公开的标准化协议而是西门子私有协议的逆向工程产物。这些年Snap7在工业圈积累了大量的用户稳定性、兼容性都经过了验证。它把复杂的协议报文全部封装好了我们只需要关心“读哪个DB块、从哪个偏移地址开始、读多少字节”这些问题非常符合Python“让程序员专注业务”的调性。2. 准备环境与PLC侧配置2.1 Python和python-snap7的安装细节先说Python环境。我建议直接用Python 3.8以上版本Windows下装Python时记得勾选“Add Python to PATH”这一点不勾后面在命令行里敲pip会非常难受。Linux下不用多说发行版自带Python 3不过自带的python3-pip有时候没装先补上。然后安装python-snap7pip install python-snap7正常情况下会同时安装snap7这个底层库。但在Linux上从1.3版本开始pip包不一定自带libsnap7.so所以经常出现import snap7成功但创建客户端时报错找不到共享库的情况。我在Ubuntu上遇到过一次解决方案是去Snap7官网下载对应平台的release包解压后把libsnap7.so放到系统库目录或者放到自己的项目目录后用环境变量指定。Windows相对省心python-snap7的wheel包里通常会带snap7.dll。装好之后验证一下import snap7 print(snap7.__version__)如果没报错说明basic环境已经通了。注意这里还不能马上连PLC因为网络和PLC参数还没准备好。2.2 PLC侧需要抠明白的几个参数用Snap7连接西门子PLC有四个参数绕不开IP地址、机架号Rack、插槽号Slot以及——对于S7-1200/1500来说特别关键的——访问模式设置。老式S7-300/400默认情况下会开放PUT/GET通信只要知道Rack和Slot就能连上。S7-300常见Rack为0CPU所在的Slot为2S7-400常见Rack为0Slot取决于具体配置常见是3。S7-200 Smart用起来不太一样连接时可以直接用TSAP参数指定远程端口这个后面说。S7-1200/1500的情况要特别注意默认情况下Snap7这类客户端是连不上的。需要在TIA Portal项目里把CPU属性中的“保护与安全”里面的“连接机制”勾选“允许来自远程对象的PUT/GET通信访问”。不勾这个选项代码写得再对也是白搭报错全是连接拒绝。另外由于S7-1200/1500的通信设计连接时可以不用指定Rack和Slot有些版本直接传0就行。但如果你用的还是旧版固件稳妥起见还是填对为好。这里我建议先看PLC侧组态信息别靠猜。2.3 第一个连接测试脚本环境就绪之后第一件事就是写一个最基础的连接测试。我习惯先调通连接再谈数据读写不然中间出了岔子根本不知道是哪一层的问题。import snap7 client snap7.client.Client() client.connect(192.168.0.10, 0, 1) # IP, Rack, Slot if client.get_connected(): print(连接成功) client.disconnect() else: print(连接失败)第一次跑这个脚本的时候保证电脑和PLC在同一个网段物理链路通最好先用ping确认一下IP能通。如果连接成功说明基础通信没问题可以进入下一步正式读写操作了。3. 核心实操读写PLC数据的完整流程3.1 建立连接IP/Rack/Slot与超时参数连接API看起来简单但实际工程里不能只靠connect()。这个函数默认没有设置超时如果IP不可达可能会卡很久。我一般会在客户端参数里设置一个连接超时import snap7 from snap7.util import set_connection_params client snap7.client.Client() client.set_connection_params(192.168.0.10, 0, 1) client.set_connection_type(snap7.types.ConnectionType.CONNTYPE_PG) # 使用编程器连接类型 client.connect_timeout 3000 # 部分版本通过参数配置 client.connect()不同版本的python-snap7设置超时的方法略有差异connect_timeout这个属性不是所有版本都有。如果没有这个属性最简单的办法是用socket.setdefaulttimeout()或者干脆在调用connect前先手动ping一下IP不通就直接报错避免界面卡死。TSAP参数是另一个容易被忽略的坑。Snap7在连接S7-300/400时如果默认的TSAP不对可以用下面这种方式指定client.connect(192.168.0.10, 0, 2, tsap0x0301)这里的0x0301是本地TSAP和远程TSAP的某种约定具体值取决于PLC型号。对S7-1200/1500一般不传TSAP也能连上照着默认走就行。3.2 读DB块数据并按类型解析连接通了最常用的操作就是读DB块。DB块在PLC里就是数据存储区所有需要上传给上位机的数据最好都放到一个专门的DB块里这样采集和排查都方便。Snap7读取DB块的核心方法是db_read它会返回一个字节数组:data client.db_read(db_number1, start0, size10) print(data) # 返回类似 bytearray(b\x00\x01\x02...)这里要注意三个参数的含义db_numberPLC里DB块的编号比如DB1、DB10。start要读取的字节偏移地址。PLC里设置DB偏移量时第一个字节地址从0开始博途里看到的“偏移量”和这里的start是直接对应的。size要读取的字节数一次性把所有需要的连续区域读出来不要一个变量读一次效率差距很大。拿到了字节数组之后怎么转成真正的数据是关键。假设DB1里面有四个变量一个REAL4字节、一个INT2字节、一个BYTE1字节偏移量分别为0、4、6。解析方式如下import struct data client.db_read(1, 0, 7) real_value struct.unpack(f, data[0:4])[0] # 大端浮点数 int_value struct.unpack(h, data[4:6])[0] # 大端有符号短整数 byte_value data[6]西门子PLC的数据存储用的是大端字节序高字节在前Python的struct模块默认也是大端所以格式串里用前缀。很多新手踩坑都是因为这里没有加读出来的数据漂移得厉害。如果是字符串类型比如S7 String数据结构更复杂前两个字节分别是最大长度和当前长度后面才是字符内容。读的时候要先把这两个头字节解析出来再切出真实字符串。这也是一个容易被忽视的细节。3.3 写数据到PLC注意字节序和类型写入数据和读取逻辑基本对称。先按偏移量和大小把数据拼成字节数组再用db_write写进去。import struct # 往DB1的偏移0写一个浮点数 25.6 value 25.6 data struct.pack(f, value) client.db_write(db_number1, start0, datadata)这里有几个关键点写入的数据长度必须和PLC侧的变量大小一致写多了或者写少了都可能报错。每次写入都会占用一次通信请求如果要对多个变量写值建议把连续的地址合并成一次写入减少通信次数。写入BOOL类型时通常是把一个字节读回来修改对应的位再写回去。直接写一个字节覆盖会影响到同一字节里的其他BOOL变量。安全提示写入PLC数据一定要做权限控制和数据校验。在程序里写死地址容易但遇到生产环境一个错误的写入可能直接触发设备动作轻则报警重则事故。我建议在所有写入操作前加上二次确认逻辑记录日志关键设备必须手动确认后才允许自动写入。3.4 位读写与M区、I/Q区操作DB块只是PLC数据的一部分实际项目里M区位存储、输入区I、输出区Q也是高频操作对象。Snap7提供了read_area和write_area两个方法可以跨区域读取。先看一个读M区字节的例子area snap7.types.Areas.MK # M区 data client.read_area(area, db_number0, start0, size1)area参数可以用snap7.types.Areas枚举常用的有Areas.MKM区Areas.PE输入过程映像区IAreas.PA输出过程映像区QAreas.DBDB块注意read_area方法里DB区域使用Areas.DB时db_number才有效读M区/I/Q区时db_number通常传0。位读写是另一个常见需求。PLC里的BOOL变量通常以位的形式存在比如M10.3就是M区字节10的第3位。Snap7没有直接读位的API但我们可以用read_area读回整个字节然后用位运算取目标位byte_data client.read_area(snap7.types.Areas.MK, 0, start10, size1) bit_value (byte_data[0] 3) 0x01写位类似先把原字节读回来设置或清除目标位再write_area写回byte_data client.read_area(snap7.types.Areas.MK, 0, start10, size1) new_byte byte_data[0] if set_bit: new_byte | (1 3) else: new_byte ~(1 3) client.write_area(snap7.types.Areas.MK, 0, start10, databytearray([new_byte]))这种读改写方式有竞态风险如果PLC程序也在同一时间修改这个字节理论上可能互相覆盖。但是实际上在低速轮询几百毫秒级别和普通控制场景下风险完全可接受。如果你需要严格的原子操作最好把标志位集中放到一个字节里PLC程序负责修改Python只读不写通过心跳或命令号机制来协调。3.5 批量读写与高性能查询工业数据采集和单纯读一两个变量不一样现场动辄几十上百个点位如果每个变量都单独db_read一次通信周期会被拉得很长。Snap7提供了read_multi_vars和write_multi_vars接口可以在一次通信请求里读写多个地址。from snap7.snap7types import S7DataItem, S7WLByte items [] # 读取DB1 offset 0 的4字节 item1 S7DataItem() item1.Area snap7.types.Areas.DB item1.WordLen snap7.types.S7WLByte item1.DBNumber 1 item1.Start 0 item1.Amount 4 items.append(item1) # 读取M区 offset 10 的1字节 item2 S7DataItem() item2.Area snap7.types.Areas.MK item2.WordLen snap7.types.S7WLByte item2.DBNumber 0 item2.Start 10 item2.Amount 1 items.append(item2) result client.read_multi_vars(items) for item, data in zip(items, result): print(item.Start, data)这个接口稍微绕一点但熟悉之后很强大。它适合做固定点位的周期采集先把所有点位定义成一个数据结构每次采集直接调一次read_multi_vars几百个点位一秒钟刷新完全不成问题。批量写入的用法类似使用write_multi_vars。批量操作除了能减少通信次数还能让多个变量的状态变更尽量保持同步在联动控制场景下是个很大的优点。3.6 断开连接与异常回收很多人在实验环境里写完代码不注重资源回收但做长期采集服务时连接管理非常关键。PLC的通信资源有限如果Python进程反复异常退出而旧连接没断开PLC那边会累积很多ESTABLISHED状态的连接直到把通信资源耗尽。正确的做法是try: # 业务逻辑 pass finally: client.disconnect() client.destroy()disconnect()负责断开连接destroy()释放底层资源。在掉线重连场景下还要注意调用client.destroy()之后重新创建Client对象而不是直接复用旧对象否则可能出现状态残留。另外采集服务一般建议加上断线重连逻辑。我用的是最简单的指数退避策略第一次失败等2秒第二次等4秒最大间隔不超过60秒直到重连成功。这个策略比固定间隔重连好在不会把PLC通信资源打满。4. 常见问题与排查技巧实录4.1 连接不上从IP到TSAP逐步排查连接失败是Snap7最多发的问题也是新人最头疼的问题。我的排查顺序是这样物理链路先看网线、交换机指示灯确认设备在同网段。用ping 192.168.0.10验证IP通不通。工业环境里很多时候是IP冲突或者网段配置不对。防火墙Windows防火墙默认会拦掉S7协议的102端口通信测试时可以临时关闭防火墙或者在防火墙入站规则里放行Python进程。Linux服务器也要留意iptables/firewalld。PLC侧设置S7-1200/1500需要检查“允许来自远程对象的PUT/GET通信访问”是否勾选。这一步很多人都漏了连接后直接报errConnRefused。Rack和Slot连S7-300时Slot填错是很常见的报错通常是errNegotiatingPDU。查PLC组态确认CPU模块所在的机架和插槽。S7-1200/1500一般不用太纠结用0,1组合往往能通。TSAP如果前面都对还连不上可以试着显式指定TSAP。S7-300通常远程TSAP是0x0201或0x0302取决于CPU型号。网上很多案例是默认TSAP不对导致的。最后还有一条很多代码老手也会踩的坑在同一段脚本里用同一个Client对象反复连接、断开、再连接有时候第二次connect()会失败。这时候最干净的办法是重新new一个Client对象不要勉强复用。4.2 读出来的数据总是怪怪的连接通了但读出来的数据不对这种问题通常集中在三个地方字节序问题。西门子PLC的数据是大端序Python的struct.unpack必须用前缀用成小端或者不写前缀都会得到错值。这是我见过最多的错误。偏移量问题。PLC里DB变量的偏移地址在博途里默认从0开始但如果是通过外部符号表或者第三方工具导出的点表偏移量有时候是1起始的差一个字节所有数据全错位。写代码前最好拿一个已知变量验证一下。数据类型长度问题。PLC里的REAL是4字节INT是2字节DINT是4字节LREAL是8字节。读回来之后切片的长度必须和类型严格对应。有人读REAL时只切了2字节解析出来自然是垃圾值。我调试的时候会先做一个“已知值测试”在PLC里放一个固定常量比如100.0然后读回来确认程序解析是否正确。这一步过了再上真实数据能省掉大量排查时间。4.3 高频轮询的性能优化思路做数据采集大家最关心的是能跑到多高的频率。Snap7单次请求的耗时一般在几毫秒到十几毫秒频繁调用db_read也不是不行但高频轮询很容易把PLC的通信负载打高影响PLC的正常控制任务。我的优化优先级是能用批量绝不用单点。所有点位尽量合并进read_multi_vars或者把同一DB块内的连续数据一次性读出来再在Python里切片解析。降低PLC侧负担。如果数据变化不快没必要20ms刷一次。普通设备数据500ms甚至1s刷新完全够用。需要毫秒级响应的信号应该走PLC内部逻辑而不是靠上位机轮询。异步采集。Python里用threading或者asyncio把采集逻辑和业务逻辑解耦。采集线程专注于读写数据数据通过队列交给业务线程避免阻塞。我在一个实际项目里用单线程阻塞式db_read读200个点位1秒刷新一次CPU占用不到5%。如果把点位拆成多个批量请求可以轻松做到200ms刷新一轮。这一点对生产环境很有参考价值。4.4 关于线程安全与长期稳定运行python-snap7的Client对象不是线程安全的多个线程共用同一个Client会引发各种奇怪报错。我建议每个线程维护独立的Client连接或者全局用一把锁保护读操作虽然损失一点并发但稳定第一。如果设计成采集线程内部串行调用业务线程从队列拿数据基本不会碰线程安全问题。长期运行的另一个隐患是内存泄漏。Snap7底层是C库虽然Python封装做了资源管理但遇到异常情况比如网络中断后反复重试连接对象还是要及时销毁重建。我写采集服务时会把连接生命周期和心跳检测绑定心跳失败就销毁重建确保内存不涨、连接不泄漏。还有一个容易被忽略的稳定性问题PLC的通信负载和循环扫描周期是竞争关系。如果上位机以极高的频率读数据PLC的扫描周期可能会被拉长导致控制性能下降。现场出现过一次上位机每10ms读一次数据结果CPU负荷飙高后来把刷新周期调回200ms才稳定。工业现场通信一定要克制能慢就慢能少读就少读。最后分享一个我在调试现场常用的土办法在PLC程序里留一个专门的“调试DB块”里面放几个固定值、一个由PLC程序自己循环递增的计数器。上位机连不上或者数据不对的时候先读这个DB块如果计数器在涨说明通信链路和PLC程序都在正常跑问题大概率出在上位机的解析逻辑上。这个办法帮我定位过无数次问题。Python和Snap7这套组合让我从只能在固定上位机旁边看数据的局面里跳了出来随手写个脚本就能把产线数据拉回本地做统计和可视化。不管你是想做设备预测性维护、厂级数据中台还是只是想快速验证一个想法把通信这层打通了后面的路会宽很多。
返回列表