4、上位机与PLC通信实战(一):使用Python的Snap7库实现与西门子PLC的读写操作
好,咱们今天正式进入实战环节。
前面几章讲了网络基础、协议栈、硬件接线,说实话,都是铺垫。真正让分拣机动起来,还得靠上位机和PLC之间的数据对话。这一章,我带你用Python的Snap7库,亲手实现与西门子PLC的读写操作。
我个人习惯把这一步叫做「打通任督二脉」。为什么?因为一旦你能从电脑上读到PLC的数据,或者往PLC里写一个值,那整个自动化系统的灵魂就活了。你想想看,远程监控、数据采集、配方管理,全都建立在这个基础上。
4.1 Snap7库是什么?为什么选它?
Snap7是一个开源的通信库,专门用来和西门子S7系列PLC打交道。它支持S7-200、300、400、1200、1500,几乎覆盖了市面上主流的型号。
我最早接触它是在一个汽车零部件的产线项目上。当时甲方要求用Python做数据采集,我试过LibNoDave、pyads,最后发现Snap7最顺手。说白了,它封装了底层复杂的ISO-on-TCP协议,你只需要调用几个函数就能完成读写。
核心优势:
- 纯Python实现,跨平台(Windows/Linux/Raspberry Pi都行)
- 支持多种数据类型:bool、int、real、string等
- 读写速度快,实测在100ms以内完成一次DB块读取
- 社区活跃,遇到问题基本能找到答案
4.2 环境准备:安装Snap7
安装很简单,一行命令搞定:
pip install python-snap7
嗯,这里要注意。如果你用的是Linux系统,比如树莓派,还需要安装Snap7的底层库。Windows下一般不需要额外操作。
小提示:我在树莓派上踩过坑。记得先运行 sudo apt-get install libsnap7-dev,否则会报找不到库文件的错误。
验证安装是否成功,打开Python交互环境:
import snap7
print(snap7.__version__)
如果能正常输出版本号,说明环境没问题。
4.3 建立连接:让电脑和PLC「握手」
连接PLC之前,你需要知道三样东西:PLC的IP地址、机架号(Rack)、槽号(Slot)。
对于S7-1200和1500,默认机架号是0,槽号是1。S7-300/400可能不同,具体看硬件组态。
import snap7
# 创建客户端对象
client = snap7.client.Client()
# 连接到PLC
client.connect('192.168.1.10', 0, 1)
# 检查连接状态
if client.get_connected():
print("连接成功!")
else:
print("连接失败,请检查IP和网络。")
我曾经遇到过一个情况:PLC的IP能ping通,但Snap7就是连不上。折腾了半天,发现是PLC的防火墙没关。西门子PLC默认会开启防火墙,你得在博图里把「允许来自远程对象的PUT/GET通信」勾上。
避坑指南:
- 确保PLC和电脑在同一个网段
- 检查PLC的防护等级设置,S7-1200需要允许远程访问
- 如果连接超时,试试关闭Windows防火墙
4.4 读取DB块:把PLC里的数据拿上来
DB块是西门子PLC里最常用的数据存储区。分拣机的速度、位置、故障码,通常都放在DB块里。
读取DB块的核心函数是 db_read(),它需要三个参数:DB块编号、起始字节地址、读取长度。
# 读取DB1,从第0个字节开始,读取4个字节(一个32位整数)
data = client.db_read(1, 0, 4)
# Snap7返回的是字节数组,需要解析
import struct
value = struct.unpack('>i', data)[0]
print(f"DB1.DBD0的值是:{value}")
你可能会问:为什么要用 struct.unpack?因为Snap7读回来的是原始字节,你得告诉Python怎么解释这些字节。'>i'表示大端序的有符号32位整数。
读取实数(Real)也是一样的套路:
# 读取DB1的第4个字节开始的4个字节(一个实数)
data = client.db_read(1, 4, 4)
real_value = struct.unpack('>f', data)[0]
print(f"DB1.DBD4的实数值是:{real_value}")
经验之谈:我习惯把读取函数封装成一个工具类。比如写一个 read_db_int(db_number, start_byte),这样每次调用就一行代码,清爽多了。
4.5 写入M寄存器:让PLC听你的指令
M寄存器,也叫中间寄存器,常用于控制逻辑的启停。比如分拣机的启动按钮、急停信号,都可以映射到M区。
写入操作使用 mb_write() 函数。注意,它一次只能写一个字节(8个位)。
# 写入M0.0,即M寄存器的第0个字节的第0位
# 先读取当前M0字节的值
current = client.mb_read(0, 1)
# 把第0位设为1(启动)
new_value = current[0] | 0b00000001
client.mb_write(0, bytearray([new_value]))
print("M0.0已置位为1")
如果你想写一个整数到MW0(M字,占用2个字节),需要自己拼字节:
# 写入整数100到MW0
value = 100
data = struct.pack('>H', value) # '>H'表示大端序无符号16位整数
client.mb_write(0, bytearray(data))
print(f"MW0已写入值:{value}")
注意:写入操作要谨慎。我曾经在调试时不小心写错了地址,把M100写成了M10,结果产线直接停了。所以,写入前最好加一个确认步骤,或者先读一下当前值做个备份。
4.6 完整示例:一个简单的读写循环
下面是一个完整的示例,演示了连接、读取DB块、写入M寄存器,然后断开连接。
import snap7
import struct
import time
def main():
# 1. 建立连接
client = snap7.client.Client()
client.connect('192.168.1.10', 0, 1)
if not client.get_connected():
print("连接失败,退出程序")
return
print("连接成功,开始读写测试...")
try:
# 2. 读取DB1.DBD0(整数)
data = client.db_read(1, 0, 4)
speed = struct.unpack('>i', data)[0]
print(f"当前分拣速度:{speed} mm/s")
# 3. 读取DB1.DBD4(实数)
data = client.db_read(1, 4, 4)
temperature = struct.unpack('>f', data)[0]
print(f"当前电机温度:{temperature} ℃")
# 4. 写入M0.0,启动分拣
current = client.mb_read(0, 1)
new_value = current[0] | 0b00000001
client.mb_write(0, bytearray([new_value]))
print("已发送启动信号")
time.sleep(2)
# 5. 停止分拣(复位M0.0)
new_value = current[0] & 0b11111110
client.mb_write(0, bytearray([new_value]))
print("已发送停止信号")
except Exception as e:
print(f"读写过程中出错:{e}")
finally:
# 6. 断开连接
client.disconnect()
print("连接已断开")
if __name__ == "__main__":
main()
4.7 常见问题与调试技巧
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 连接超时 | IP地址错误或PLC未开机 | 先ping一下PLC的IP |
| 读取返回空数据 | DB块编号或地址错误 | 在博图里确认DB块的实际编号 |
| 写入不生效 | PLC处于STOP模式 | 将PLC切换到RUN模式 |
| struct解析报错 | 字节序或数据类型不匹配 | 检查PLC中变量的数据类型 |
调试小技巧:我常用Wireshark抓包看Snap7的通信过程。如果读写失败,抓包能帮你快速定位是网络问题还是协议问题。Snap7使用的是102端口,抓包时过滤 tcp.port == 102 就行。
4.8 小结
这一章我们完成了三件事:建立连接、读取DB块、写入M寄存器。说白了,就是让Python和PLC能「说上话」。
下一章,我会带你把这些操作封装成一个完整的监控类,再加上异常处理和日志记录。到时候,你就能写一个真正能上生产环境的远程监控程序了。
嗯,今天就到这里。代码不多,但每一步都很关键。建议你打开博图,建一个简单的DB块,亲手跑一遍上面的代码。遇到问题别慌,对照着表格排查,基本都能解决。