ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python 通过 snap7 读写西门子 S7-300/400 PLC 实战指南

Python 通过 snap7 读写西门子 S7-300/400 PLC 实战指南

简介:这份资源面向需要用 Python 与西门子 S7-300、S7-400 系列 PLC 通信的工程师与学习者,通过 TCP/IP 直连方式实现数据读写,解决上位机与 PLC 交互、标签管理与数据落盘的常见需求。压缩包共 12 个文件,约 910KB,包含 3 个 py 脚本与 2 个 pyc 编译文件承载通信与业务逻辑,5 个 xls 用于 PLC 标签定义与校验记录,另有 1 个 ui 界面文件和 1 个 png 示意图,整体轻量、便于快速上手。资源中附带作者亲测可用的 DEMO 工程,涵盖 Python 对 Excel 的读写处理,并用 QT 简单搭建操作界面,读者可据此理解标签表驱动的采集流程、界面与通信逻辑的衔接方式,以及数据校验与结果导出的实现思路。目前已有 3616 人学习下载,适合希望打通 Python 与西门子 PLC 通信链路的开发者参考复用。

1. 用 Python 打通西门子 S7-300/S7-400:为什么老产线改造绕不开这条路

车间里跑着十几年的 S7-300 和 S7-400,上位机还是 WinCC 老版本,想加个数据看板、做个 MES 对接、或者把关键工艺参数推到内部平台,第一反应往往是“换 PLC”。但产线停一天的成本摆在那,换 PLC 意味着重新接线、重新组态、重新验证,没人敢拍板。更现实的做法是:不动 PLC 程序,用 Python 从外部把数据读出来。S7-300 和 S7-400 支持 ISO-on-TCP(RFC1006)和 S7 通信协议,只要 PLC 侧开放了对应连接资源,Python 就能通过以太网读写 DB 块、M 区、I/Q 区,甚至调用已有的 FC/FB。这条路适合三类人:做设备数据采集的自动化工程师、需要快速验证通信可行性的项目前期人员、以及维护老产线但不想大动干戈的运维团队。核心工具是 python-snap7,它封装了底层 S7 协议,把“连 PLC”这件事从写 C 库降到了十几行 Python。但别高兴太早,S7-300/400 的通信资源、机架槽号、DB 块访问权限、字节序,每一个都能让你卡半天。

2. 先搞清楚 S7-300/400 的通信底座:从 ISO-on-TCP 到 PUT/GET

2.1 为什么不是所有 S7-300 都能被 Python 直接连

S7-300 和 S7-400 的 CPU 分很多档次,通信能力差异很大。低端 CPU(比如某些 312、313 型号)只有 MPI 口,没有集成以太网口,想走 TCP 必须加 CP343-1 或 CP443-1 通信模块。即使有网口,CPU 的通信资源也是有限的:每个 CPU 能同时维持的 ISO-on-TCP 连接数、S7 连接数都有上限,通常在硬件手册里写着“最大连接数”。如果你用 Python 去连,占用的就是这些连接资源中的一个。更关键的是,S7-300/400 默认对 PUT/GET 通信是关闭的,也就是说,外部设备想读写它的数据,需要在硬件组态里显式勾选“允许来自远程对象的 PUT/GET 通信访问”。这个选项在较新的 TIA Portal 里叫“Permit access with PUT/GET communication from remote partner”,在老的 STEP7 V5.x 里叫“Enable PUT/GET communication”。不勾这个,Python 连上了也读不到数据,报错往往是“Access denied”或者直接超时。

2.2 ISO-on-TCP 和 S7 协议的分工

ISO-on-TCP(RFC1006)是传输层,负责在 TCP 之上建立一个面向连接的会话,默认端口是 102。S7 协议是应用层,跑在 ISO-on-TCP 之上,定义了读、写、启动、停止等操作。python-snap7 的 client.connect() 实际上做了两件事:先建立 TCP 连接,再发 S7 通信的握手包。握手包里包含本地 TSAP 和远程 TSAP,TSAP 决定了你连的是哪个机架、哪个槽位。对于 S7-300/400,远程 TSAP 通常是 03.02(机架 0,槽位 2),但如果你用了 CP 模块或者多机架,这个值会变。本地 TSAP 一般填 01.00 就行,除非你有特殊路由需求。很多人第一次连不上,就是 TSAP 填错了,或者 PLC 侧根本没开 PUT/GET。

2.3 用 snap7 建立第一条连接:最小可运行代码

先装库。Windows 下 pip install python-snap7 之后,还需要把 snap7.dll 放到系统路径或者脚本同目录;Linux 下需要 libsnap7.so,通常通过 apt 或编译安装。下面这段代码是最小可运行版本,连上之后读一个 DB 块的前 10 个字节。

import snap7 from snap7.util import * # 创建客户端实例 client = snap7.client.Client() # 连接参数:IP、机架号、槽号、本地TSAP、远程TSAP # 对于S7-300/400,机架0槽2是最常见的配置 client.connect('192.168.0.10', 0, 2, 0x0100, 0x0302) # 检查连接状态 if client.get_connected(): print("连接成功") # 读取DB1的前10个字节 data = client.db_read(1, 0, 10) print("DB1原始数据:", data.hex()) # 按字节解析:假设前2字节是整数 value = get_int(data, 0) print("偏移0的INT值:", value) else: print("连接失败") # 用完关闭 client.disconnect() client.destroy()

逻辑说明:connect() 的第三个参数是槽号,S7-300 通常 CPU 在槽 2,S7-400 可能在槽 2 或槽 3,取决于电源和 CPU 的插槽位置。本地 TSAP 0x0100 表示连接资源 01、机架 0、槽 0,远程 TSAP 0x0302 表示连接资源 03、机架 0、槽 2。db_read(1, 0, 10) 读的是 DB1,起始偏移 0,长度 10 字节。get_int 按大端序解析,因为 S7 协议默认大端。如果你读到的值是乱的,先检查字节序,再检查 DB 块的偏移地址是否对得上。

参数说明:机架号一般填 0,除非你有多机架扩展。槽号看 CPU 实际插在哪,S7-300 通常是 2,S7-400 可能是 2 或 3。本地 TSAP 填 0x0100 是通用做法,远程 TSAP 如果连不上,试试 0x0301 或 0x0303。db_read 的 DB 号必须存在,且 PLC 侧没有设置访问密码。

3. 读写 DB 块、M 区和 I/Q 区:地址映射与数据类型转换

3.1 DB 块访问:偏移地址和数据类型必须对齐

S7-300/400 的 DB 块里,变量是按字节偏移存放的。比如 DB1 里定义了一个 INT 在偏移 0,一个 REAL 在偏移 2,一个 BOOL 在偏移 6.0。Python 读的时候,db_read(1, 0, 8) 把前 8 个字节全读回来,然后用 snap7.util 里的 get_int、get_real、get_bool 按偏移解析。注意 BOOL 的偏移写法是“字节偏移.位偏移”,在 Python 里需要自己算:字节偏移 = 6,位偏移 = 0,读出来是一个字节,再用 get_bool 取第 0 位。

# 读取DB1中偏移0的INT、偏移2的REAL、偏移6.0的BOOL data = client.db_read(1, 0, 8) int_val = get_int(data, 0) # 偏移0,INT real_val = get_real(data, 2) # 偏移2,REAL bool_val = get_bool(data, 6, 0) # 偏移6.0,BOOL print(f"INT: {int_val}, REAL: {real_val}, BOOL: {bool_val}")

逻辑说明:get_int 读 2 字节,get_real 读 4 字节,get_bool 读 1 字节的第 n 位。如果 DB 块里变量定义顺序和偏移对不上,读出来的就是垃圾值。常见坑是 DB 块里用了 STRUCT 或者 ARRAY,偏移需要手动累加,不能想当然。

3.2 M 区、I 区、Q 区的读写差异

M 区(位存储区)、I 区(输入过程映像)、Q 区(输出过程映像)的读写方式和 DB 块类似,但 API 不同。读 M 区用 client.mb_read(offset, size),读 I 区用 client.eb_read(offset, size),读 Q 区用 client.ab_read(offset, size)。写操作对应 mb_write、eb_write、ab_write。注意 I 区和 Q 区的大小受 CPU 过程映像区限制,不是所有地址都能访问。比如 S7-300 的默认过程映像区是 128 字节,你读 I 区偏移 200 就会报错。

# 读M区偏移0的4个字节 m_data = client.mb_read(0, 4) m_int = get_int(m_data, 0) # 读I区偏移0的1个字节 i_data = client.eb_read(0, 1) i_bool = get_bool(i_data, 0, 0) # 写Q区偏移0的1个字节 q_data = bytearray(1) set_bool(q_data, 0, 0, True) client.ab_write(0, q_data)

逻辑说明:mb_read 的偏移是字节偏移,不是位偏移。I 区和 Q 区的读写权限取决于 PLC 的运行模式,STOP 模式下写 Q 区可能不生效。写操作要谨慎,尤其是 Q 区,写错了可能直接导致输出动作异常。

3.3 字节序和数据类型转换的坑

S7 协议是大端序,而 x86 机器是小端序。snap7.util 里的 get_int、get_real 已经帮你做了转换,但如果你自己用 struct 解析,必须指定字节序为 '>'。比如 struct.unpack('>h', data[0:2]) 才能得到正确的 INT。REAL 类型是 IEEE 754 单精度,大端序,get_real 内部也是按大端解析的。字符串类型比较麻烦,S7 的 STRING 是头部 2 字节(最大长度和当前长度)+ 字符内容,需要自己解析。

import struct # 手动解析INT,大端序 raw = data[0:2] int_val = struct.unpack('>h', raw)[0] # 手动解析REAL,大端序 raw = data[2:6] real_val = struct.unpack('>f', raw)[0] # 解析S7 STRING:前2字节是最大长度和当前长度 str_max = data[0] str_len = data[1] str_val = data[2:2+str_len].decode('ascii')

逻辑说明:struct.unpack 的格式字符串里,'>' 表示大端,'h' 是 short(2 字节),'f' 是 float(4 字节)。S7 STRING 的最大长度在 DB 块定义时指定,当前长度是实际字符数。如果字符串里有中文,S7 默认不支持,需要用 WSTRING 或者自己转码。

4. 连接参数、TSAP 和通信资源:那些让你反复翻车的细节

4.1 机架号、槽号和 TSAP 的对应关系

机架号通常是 0,但如果你用了扩展机架(IM 模块),机架号可能是 1、2、3。槽号取决于 CPU 插在哪:S7-300 的 CPU 一般在槽 2,S7-400 的 CPU 可能在槽 2 或槽 3。TSAP 的编码规则是:第一个字节是连接资源(01 表示本地,03 表示远程),第二个字节是机架号和槽号的组合。对于机架 0 槽 2,远程 TSAP 是 0x0302;机架 0 槽 3 是 0x0303。如果你连的是 CP343-1 模块,TSAP 可能不同,需要查模块手册。

# 常见TSAP配置 tsap_configs = [ (0x0100, 0x0302), # 机架0槽2,S7-300常见 (0x0100, 0x0303), # 机架0槽3,S7-400常见 (0x0100, 0x0301), # 机架0槽1,某些特殊配置 ] for local_tsap, remote_tsap in tsap_configs: try: client.connect('192.168.0.10', 0, 2, local_tsap, remote_tsap) if client.get_connected(): print(f"连接成功,TSAP: {hex(local_tsap)} -> {hex(remote_tsap)}") break except Exception as e: print(f"TSAP {hex(remote_tsap)} 失败: {e}")

逻辑说明:这段代码遍历常见 TSAP 组合,哪个能连上就用哪个。实际项目中,我一般先查 PLC 硬件组态里的 CPU 插槽号,再确定 TSAP。如果连不上,先用 ping 确认网络通,再用 telnet 测 102 端口是否开放。

4.2 通信资源被占满怎么办

S7-300/400 的通信资源有限,每个 ISO-on-TCP 连接、S7 连接都占一个资源。如果你用 Python 频繁连接断开,可能把资源耗尽,表现为“连接超时”或“资源不可用”。解决办法是复用连接,不要每次读写都 connect/disconnect。另外,PLC 侧如果有 WinCC、触摸屏、其他上位机在连,也会占资源。查资源占用情况可以在 STEP7 的“通信”诊断里看,或者用 client.get_cpu_info() 看 CPU 状态。

# 复用连接:全局一个client,不要频繁创建销毁 client = snap7.client.Client() client.connect('192.168.0.10', 0, 2, 0x0100, 0x0302) # 多次读写复用同一个连接 for i in range(100): data = client.db_read(1, 0, 10) # 处理数据 time.sleep(0.1) # 程序退出时再断开 client.disconnect() client.destroy()

逻辑说明:频繁 connect/disconnect 会快速消耗 PLC 的连接资源,而且每次连接都有握手开销。复用连接是基本操作,但要注意异常处理:如果连接断了,需要重连。建议加一个心跳机制,定期读一个已知地址,确认连接还活着。

4.3 读写超时和重试策略

网络抖动、PLC 忙、数据量大,都可能导致读写超时。snap7 的 client 有 set_param 方法可以设置超时时间。默认超时可能偏短,读大块数据时容易失败。我一般把 PDU 大小和超时都调一下。PDU 大小决定了单次读写能传多少字节,S7-300 的 PDU 通常 240 字节左右,S7-400 可能更大。读大块数据时,snap7 会自动分包,但分包多了容易超时。

# 设置超时和PDU大小 client.set_param(snap7.types.LocalPort, 0) # 本地端口随机 client.set_param(snap7.types.PingTimeout, 5000) # ping超时5秒 client.set_param(snap7.types.SendTimeout, 5000) # 发送超时5秒 client.set_param(snap7.types.RecvTimeout, 5000) # 接收超时5秒 # 读大块数据时,分多次读,每次不超过PDU def read_large_db(client, db_num, start, size, chunk_size=200): result = bytearray() for offset in range(start, start + size, chunk_size): chunk = client.db_read(db_num, offset, min(chunk_size, start + size - offset)) result.extend(chunk) return bytes(result)

逻辑说明:set_param 的参数类型在 snap7.types 里定义,PingTimeout 是连接保活超时,SendTimeout 和 RecvTimeout 是读写超时。分块读的时候,chunk_size 不要超过 PDU 大小,否则 snap7 内部还会再分,容易乱。我一般设 200 字节,留点余量。

5. 避坑与排查:连不上、读不到、值不对的 5 个血泪经验

5.1 现象:connect 返回成功但 db_read 报 Access denied

原因:PLC 侧没有勾选“允许 PUT/GET 通信”。S7-300/400 默认关闭这个选项,尤其是老程序从 STEP7 V5.x 移植过来的时候,很多人忘了勾。解决:在硬件组态里找到 CPU 属性,通信设置里勾上 PUT/GET 访问权限,重新下载硬件组态。注意下载硬件组态可能需要停机,提前安排好窗口。

5.2 现象:连接超时,ping 得通但 102 端口不通

原因:PLC 的 ISO-on-TCP 服务没启动,或者防火墙拦了 102 端口。有些 CP 模块需要单独配置 ISO-on-TCP 连接。解决:先用 telnet 192.168.0.10 102 测端口,如果不通,检查 CP 模块的组态,确认 ISO-on-TCP 服务已启用。如果是防火墙,加规则放行 102 端口。

5.3 现象:读到的 INT 值是个大数,明显不对

原因:字节序搞反了。S7 是大端,如果你用 struct.unpack('<h', ...) 就会得到错误的值。解决:统一用 snap7.util 的 get_int,或者手动指定 '>'。另外检查 DB 块的偏移地址,有时候 DB 块里前面有保留字节,偏移不是从 0 开始。

5.4 现象:写 Q 区后输出模块没动作

原因:PLC 在 RUN 模式下,Q 区被程序扫描覆盖,你写的值下一秒就被程序改回去了。或者写的是过程映像区,不是物理输出。解决:确认 PLC 模式,如果是 RUN,写 Q 区基本没用,除非程序里没有对该地址的写操作。真要控制输出,建议通过 DB 块或者 M 区跟程序交互,让程序去驱动 Q 区。

5.5 现象:连接一段时间后自动断开,重连也连不上

原因:PLC 的连接资源被占满,或者网络里有其他设备在扫描 102 端口。有些交换机开了 IGMP snooping 或者端口安全,会断掉长时间空闲的连接。解决:加心跳保活,定期读一个地址;检查 PLC 的连接资源占用,关掉不必要的上位机连接;如果是交换机问题,换端口或者调整交换机配置。

6. 进阶:用 Python 做批量采集和异常恢复的实战技巧

批量采集的核心不是“读得快”,而是“断了自己能爬起来”。我做过一个产线数据采集的小工具,同时连 5 台 S7-300,每 500ms 读一次关键 DB 块,跑了半年多,总结出几个实用技巧。第一,连接池不要超过 PLC 允许的连接数,S7-300 通常允许 8 到 16 个 S7 连接,你留一半给 WinCC 和触摸屏,剩下的自己用。第二,每个连接独立线程,但读写要加锁,snap7 的 client 不是线程安全的,多线程同时读写同一个 client 会出乱子。第三,异常恢复要分级:网络超时重试 3 次,每次间隔 1 秒;如果 3 次都失败,标记连接断开,进入重连循环,重连间隔从 1 秒逐步退到 30 秒,避免把 PLC 的资源打满。

import threading import time import snap7 from snap7.util import get_int, get_real class PLCReader: def __init__(self, ip, rack, slot, local_tsap, remote_tsap): self.ip = ip self.rack = rack self.slot = slot self.local_tsap = local_tsap self.remote_tsap = remote_tsap self.client = None self.lock = threading.Lock() self.connected = False self.retry_interval = 1 def connect(self): try: self.client = snap7.client.Client() self.client.connect(self.ip, self.rack, self.slot, self.local_tsap, self.remote_tsap) self.connected = self.client.get_connected() if self.connected: self.retry_interval = 1 print(f"{self.ip} 连接成功") except Exception as e: self.connected = False print(f"{self.ip} 连接失败: {e}") def read_db(self, db_num, start, size): with self.lock: if not self.connected: return None try: data = self.client.db_read(db_num, start, size) return data except Exception as e: print(f"{self.ip} 读取失败: {e}") self.connected = False return None def reconnect_loop(self): while True: if not self.connected: time.sleep(self.retry_interval) self.connect() if not self.connected: self.retry_interval = min(self.retry_interval * 2, 30) else: time.sleep(1) # 使用示例 reader = PLCReader('192.168.0.10', 0, 2, 0x0100, 0x0302) reader.connect() # 启动重连线程 reconnect_thread = threading.Thread(target=reader.reconnect_loop, daemon=True) reconnect_thread.start() # 主循环读取 while True: data = reader.read_db(1, 0, 10) if data: int_val = get_int(data, 0) real_val = get_real(data, 2) print(f"INT: {int_val}, REAL: {real_val}") time.sleep(0.5)

逻辑说明:PLCReader 类封装了连接、读写、重连逻辑。lock 保证同一时刻只有一个线程读写 client。reconnect_loop 在后台线程里跑,断线后按指数退避重连,避免频繁重连打满 PLC 资源。read_db 返回 None 表示读取失败,调用方需要处理。这个结构适合多台 PLC 并行采集,每台一个 PLCReader 实例,互不干扰。

参数说明:retry_interval 初始 1 秒,每次失败翻倍,最大 30 秒。这个退避策略在产线环境里比较稳,不会因为一台 PLC 断线影响其他 PLC 的采集。lock 的粒度是读写操作,不是整个连接生命周期,所以重连的时候不需要抢锁。如果你要采集的数据量很大,可以把 chunk_size 调小,分多次读,避免单次超时。

最后说一个我踩过的坑:有一次现场调试,Python 脚本读 DB 块一直读不到正确值,查了半天发现是 DB 块里用了“优化块访问”,这种 DB 块的偏移地址不是线性的,snap7 读不了。解决办法是在 DB 块属性里取消“优化的块访问”,改成“标准块访问”,重新编译下载。这个选项在 TIA Portal 里默认是勾上的,老程序从 STEP7 移植过来可能没这个问题,但新写的 DB 块很容易中招。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进