☰
Python+Snap7读取西门子触摸屏DInt/LReal数据全解析
2026/10/17 1:06:10 网站建设 项目流程

车间里摸到一块西门子触摸屏,想用Python把里面的工艺参数读出来,结果发现最难的不是代码,而是搞不清“这数据到底存在哪儿”。标题里说的“触摸板”,结合西门子的产品线来理解,其实就是HMI触摸屏,比如精智面板(Comfort Panel)或者老款TP系列;DInt对应32位有符号整数,LReal对应64位双精度浮点,等价于C/C++里的double。这个需求我见过不少:设备交付后PLC程序不开放,但触摸屏上能看到全部工艺参数,产线要做MES/SCADA采集,第一个能碰的入口就是那块屏。

这篇文章不是甩给你一个demo脚本,而是把从“数据在哪”到“字节怎么对”,再到“现场怎么排查”的完整链路讲清楚。无论你是入行不久的工控工程师,还是被叫去对接设备数据的IT开发,读完都能照着把DInt和LReal读出来,并且知道哪些坑不该踩。

1. 先说清数据在哪:HMI变量不等于PLC变量

1.1 HMI变量表里的两类“数据地盘”

用Snap7读西门子触摸屏之前,必须先建立一个认知:触摸屏上看到的数字,不等于都存在触摸屏里。打开HMI的变量表,变量其实分两种:一种是PLC变量,HMI只是通过S7协议周期性地从PLC刷新过来显示,数据真正躺在PLC的DB块、M区或者I/Q区;另一种才是HMI内部变量,只存在于触摸屏自己的RAM、存储卡或配方系统中,从画面脚本到报警记录都可能用到。

这个区别直接决定了后面代码读哪里。如果你要采集的是PLC变量,那么Snap7直接连PLC的IP就行,HMI只是中间那面镜子。如果你要采集的是配方数据、历史数据记录、用户管理信息、画面脚本中间变量这类HMI内部数据,那才真正触及“读触摸屏”这个动作。很多项目里大家默认“屏上能看到的都能从屏读”,结果连上HMI却读不到,问题就出在这里——读了一个没有数据的服务器。

我把两类变量整理成了一张对照表,现场判断时对着看就行:

变量类型物理存储位置典型内容外部读取方式
PLC变量PLC的DB/M区等温度、压力、设备状态、报警码直连PLC或通过HMI转发
HMI内部变量HMI的RAM/存储卡配方、数据记录、用户等级、画面脚本需HMI开放S7服务/OPC UA,或映射到PLC DB

1.2 采集通道二选一:直连HMI还是从PLC借道

明确了变量归属,接下来选路。读西门子触摸屏数据其实有两条采集通道,我分别说下适用场景和坑。

通道A是直连HMI的S7服务。Snap7本身就是S7协议的客户端,只要HMI侧开放了S7通讯,且型号、固件支持作为S7服务器对外提供数据,你就能把HMI当成一个“伪PLC”来访问它的DB区。优点是PLC侧不用动,不占用PLC连接资源。缺点是新款HMI很多默认不开放这个功能,组态里往往有个“允许通过S7通讯访问HMI数据”之类的开关,不开就连不上,而且直连HMI的Rack/Slot参数和PLC不一样,后面章节会细讲。

通道B是从PLC读取,HMI内部变量映射到PLC的DB块。这也是我在现场最常用的路径。具体做法是在HMI组态软件里,把需要采集的HMI内部变量(比如配方字段)同步映射到PLC的某个DB,再由外部系统用Snap7读PLC。优点是稳定、兼容性好,Snap7对PLC的支持远比HMI服务器成熟,寻址也符合工程师的习惯。缺点是需要PLC侧允许PUT/GET远程访问,HMI组态也要动。

实际选型我的结论很简单:默认走B,只有B因为权限原因走不通,并且确认HMI支持S7服务器时才走A。别一上来就试图把触摸屏当PLC来读,那样会被各种固件差异折磨得够呛。

2. 连接和寻址的门道:IP、Rack/Slot、DB偏移

2.1 Snap7连接参数背后是TSAP

无论走哪条通道,用Snap7连接设备时都会遇到这一行代码:

client.connect(ip, rack, slot, tcpport=102)

四个参数看起来简单,但Rack和Slot不是随便填的。它们用来生成S7通讯里的TSAP(Transport Service Access Point,传输服务访问点),你可以理解成S7协议里的“门牌号”。PLC侧约定:S7-300的CPU通常Rack=0、Slot=2;S7-1200/1500的CPU通常Rack=0、Slot=1。如果你填错了,最典型的现象是connect()本身不报错,但后续read_area会返回“function refused by CPU”,或者干脆超时。

直连HMI时Rack/Slot组合又会变。老款HMI作为S7服务器时常见的组合是Rack=0、Slot=1,但不同固件差异不小,尤其新款精智面板。我建议不要翻手册硬猜,而是打开TIA Portal或者WinCC flexible里HMI的连接配置,新建一个外部S7连接,它会把对端的TSAP参数显示出来,你再对照Snap7的TSAP计算规则反推出Rack/Slot。这个方法比瞎试快得多。

这里再补充一个我在现场最常用的前置检查:先确认设备的102端口是否真的在监听。S7协议是基于ISO-on-TCP的,端口固定是102。可以先用Python做个快速socket探测,再去琢磨Snap7的报错:

import socket def check_s7_port(ip, port=102, timeout=3): s = socket.socket() s.settimeout(timeout) try: s.connect((ip, port)) return True except OSError: return False finally: s.close()

如果这个探测都失败,说明网络不通或者HMI上的S7服务没开,后面读代码写得再漂亮也没用。

2.2 在变量表里抠出DInt/LReal的精确偏移

连接参数搞定了,下一步是告诉Snap7“读哪块里的哪个位置”。这一步的信息来源是HMI组态软件里的变量表,或者PLC的符号表。

先说PLC变量。打开TIA Portal,找到对应的PLC数据块,例如DB1,查看符号表时注意每个变量的字节偏移。比如“配方号”是个DInt,起始偏移0;“温度”是个LReal,起始偏移4;“压力”是另一个LReal,起始偏移12。这里的关键是,TIA里变量表显示的偏移量是以字节为单位的,不是位、也不是字。DBD0叫“数据块双字0”,DBD4表示从第4个字节开始的数据块双字。

把这个映射关系抄成一张表,后面写代码就直接抄:

变量名变量类型起始偏移(字节)占用长度(字节)读法
recipe_noDInt04get_dint(buf, 0)
cook_tempLReal48get_lreal(buf, 4)
cook_pressLReal128get_lreal(buf, 12)

如果是HMI内部变量映射到PLC DB,那就在HMI组态的变量连接里,把配方字段的“数据位置”指向某个PLC DB地址,再从PLC那边确认这个DB的偏移。这一步不能省,因为HMI的配方默认存在自己的存储介质里,不映射的话Snap7在PLC侧根本找不到对应数据。

这里还要提醒一句:S7-1200/1500这类较新的PLC会自动做数据对齐,一个DInt后面紧跟LReal时,如果DInt的起始偏移不是4的倍数,编译器可能插入填充字节,导致LReal实际偏移和你手工“4字节+8字节”推算出来的不一样。所以永远以变量表显示的实际偏移为准,不要用连续加减法脑补地址。

3. DInt与LReal的字节真相:大端序与对齐

3.1 DInt:4字节带符号整数

DInt全称是Double Integer,32位有符号整数,取值范围-2147483648到2147483647。你把它理解成C语言的int或者Python的int就行。

但在S7协议里,这个整数在内存中是以大端字节序存储的。也就是说,十进制42在PLC内存里按字节排列是00 00 00 2A,而不是2A 00 00 00。Snap7的get_dint(data, offset)已经帮你把大端序处理好了,它底层等价于:

import struct def read_dint_manual(data, offset): return struct.unpack('>i', data[offset:offset + 4])[0]

很多人第一次自己用struct时习惯写<i(小端),读出来的数就会变成一个莫名其妙的超大值。如果你在代码里尝试手动解析字节,记住方向:S7协议家族统一大端,格式串一定是>i。

3.2 LReal:8字节双精度浮点

LReal全称Long Real,64位双精度浮点,就是IEEE 754里的double。它占8个字节,精度大约15到17位有效十进制数字,范围远超32位的Real。如果一个参数要求小数点后很多位,触摸屏/PLC里通常就会定义成LReal。

IEEE 754双精度的内存布局是:1位符号位、11位指数位、52位尾数位。1.0这个数在内存里是3F F0 00 00 00 00 00 00,同样是大端排列。Snap7较新版本提供了get_lreal(data, offset)可以直接读,但为了兼容老版本库,我更推荐自己封装一个,逻辑很稳定:

def read_lreal_manual(data, offset): return struct.unpack('>d', data[offset:offset + 8])[0]

这里有个容易混淆的点:很多PLC变量表里既有Real也有LReal。Real是32位,占4字节;LReal是64位,占8字节。如果你把LReal变量当Real读,或者反过来,读出来的数值会非常离谱——不是量级错,就是有效位数完全对不上。所以指定变量类型时,宁可多花一分钟在变量表里核对,也别在代码里猜。

3.3 现场错觉:乱码值的三种典型姿势

字节序和数据类型搞错之后,读回来的数据不会随便报错,而是给你一个看着“好像有点道理”但其实是错的值。我把现场最常见的三种乱码姿势列出来,方便你对着症状判断:

症状最常见原因解决方向
DInt读出来永远是0或小得离谱偏移指到了数据高位全0的位置,DB号不对核对变量表偏移和DB号
LReal读出来是科学计数法极小数,比如4.9e-324偏移差了4字节,读到了指数全0的次正规浮点核对偏移是否对齐,LReal是否从正确字节开始
读出来的数量级像整数,但小数部分完全乱把Real(32位)当LReal(64位)读,或反之核对变量类型

偏移差4字节这个问题非常隐蔽。比如变量表里LReal实际起始偏移是8,但你按连续排列的直觉填了4,那么读出来的8字节里,前4字节可能是前一个变量的残余数据或填充字节,后4字节才是LReal的前半段——拼出来的浮点自然不是真实值。这也是为什么我一直强调,地址一定要回到组态变量表里去抠,而不是靠算。

4. 用Snap7读DInt/LReal的代码落地

4.1 一条read_area拿到两种类型的完整例子

地址确认、类型确认之后,代码反而是最轻松的一步。下面这个例子假设数据在DB1,偏移0处是DInt类型的配方号,偏移4处是LReal类型的温度值,一次读取12个字节把两个变量都拿到:

import snap7 from snap7.util import get_dint, get_lreal from snap7.types import Areas client = snap7.client.Client() client.connect("192.168.0.20", 0, 1, 102) # DB1: 偏移0 DInt(4字节),偏移4 LReal(8字节) buf = client.read_area(Areas.DB, 1, 0, 12) recipe_no = get_dint(buf, 0) cook_temp = get_lreal(buf, 4) print("配方号 DInt:", recipe_no) print("温度 LReal:", cook_temp) client.disconnect()

这段代码里最值得说的一句话是:为什么一次读12字节,而不是分成两次分别读4字节和8字节?因为在工控通讯里,一次read_area拿到的是同一个通讯周期内的数据快照。如果你先读DInt再读LReal,这两次操作之间设备数据可能已经刷新,你拿到的两个值在时间上就不一致了。特别是温度和压力这种联动参数,后续要做匹配分析时,这种时间差会引入莫名其妙的误差。

4.2 封装一个带重连的HMI读取类

现场写采集程序,不能像demo那样裸奔。通讯随时可能因为网络抖动、HMI重启、防火墙策略而中断,所以最好把连接管理、读取操作、异常重连封到一个类里:

import time import snap7 from snap7.util import get_dint, get_lreal from snap7.types import Areas class HMIReader: def __init__(self, ip, rack=0, slot=1, tcpport=102, retry=3): self.ip = ip self.rack = rack self.slot = slot self.tcpport = tcpport self.retry = retry self.client = snap7.client.Client() self.connected = False def connect(self): if self.connected: return for attempt in range(1, self.retry + 1): try: self.client.connect(self.ip, self.rack, self.slot, self.tcpport) self.connected = self.client.get_connected() if self.connected: return except Exception: if attempt == self.retry: raise time.sleep(1) def read_bytes(self, db_number, offset, length): self.connect() return self.client.read_area(Areas.DB, db_number, offset, length) def read_dint(self, db_number, offset): data = self.read_bytes(db_number, offset, 4) return get_dint(data, 0) def read_lreal(self, db_number, offset): data = self.read_bytes(db_number, offset, 8) return get_lreal(data, 0) def read_dint_and_lreal(self, db_number, dint_offset, lreal_offset): start = min(dint_offset, lreal_offset) end = max(dint_offset + 4, lreal_offset + 8) data = self.read_bytes(db_number, start, end - start) dint_val = get_dint(data, dint_offset - start) lreal_val = get_lreal(data, lreal_offset - start) return dint_val, lreal_val def disconnect(self): if self.connected: self.client.disconnect() self.connected = False

这个类解决了两个问题:一是连接只做一次,断线时重新connect;二是把“读DInt”“读LReal”“合并读DInt+LReal”都变成干净的方法,现场加变量时不用改一堆重复代码。调用的时候长这样:

reader = HMIReader("192.168.0.20") d, lr = reader.read_dint_and_lreal(1, 0, 4) print(d, lr) reader.disconnect()

4.3 批量变量怎么一次读完

实际项目里要采集的变量通常十几个起步,如果每读一个变量发一次read_area,通讯效率会很差。优化思路是把同一个DB里的变量按偏移排序,把地址连续的变量合并成一个大块一次性读出,再在内存里切片解析。

下面这段代码就是一个很实用的批量读取实现,它接受一个变量配置列表,自动分组、排序、合并区间:

from collections import defaultdict def read_variables(reader, variables): groups = defaultdict(list) for var in variables: var["length"] = 8 if var["type"] == "LREAL" else 4 groups[var["db"]].append(var) results = {} for db, vars_in_db in groups.items(): vars_in_db.sort(key=lambda v: v["offset"]) i = 0 while i < len(vars_in_db): start = vars_in_db[i]["offset"] max_end = start + vars_in_db[i]["length"] j = i while j + 1 < len(vars_in_db): next_offset = vars_in_db[j + 1]["offset"] if next_offset > max_end + 64: break j += 1 max_end = max(max_end, next_offset + vars_in_db[j]["length"]) buf = reader.read_bytes(db, start, max_end - start) for idx in range(i, j + 1): var = vars_in_db[idx] if var["type"] == "DINT": results[var["name"]] = get_dint(buf, var["offset"] - start) elif var["type"] == "LREAL": results[var["name"]] = get_lreal(buf, var["offset"] - start) i = j + 1 return results

使用方式非常直接:

variables = [ {"name": "recipe_no", "type": "DINT", "db": 1, "offset": 0}, {"name": "cook_temp", "type": "LREAL", "db": 1, "offset": 4}, {"name": "cook_press", "type": "LREAL", "db": 1, "offset": 12}, {"name": "alarm_code", "type": "DINT", "db": 1, "offset": 20}, ] reader = HMIReader("192.168.0.20") print(read_variables(reader, variables)) reader.disconnect()

这里有一个工程取舍:合并区间时允许最大64字节的空洞。意思是一个变量的结束位置和下一个变量起始位置之间差距不超过64字节时,就合并成一次读取;超过就分开读。如果所有变量都挤在一起,可能几个read_area就把一整块DB读完了;如果变量很稀疏,也不至于一次读太多无用的数据。

5. 实际工程中的排查清单与经验沉淀

5.1 连不上时的四步排查

每次现场连不上触摸屏,我都会按固定的顺序排查,而不是反复改Rack/Slot碰运气。

第一步,用前面那几行socket代码确认目标IP的102端口是否开放。不通就去看交换机的网线、VLAN、防火墙规则,或者HMI侧的网络状态;别继续耗在Python里。第二步,端口通了但Snap7连接报错,检查Rack/Slot和TSAP。直连HMI时最好从组态软件里翻出HMI侧实际的S7连接参数,不懂就照抄。第三步,connect成功但read_area被拒绝,这时基本可以判断:HMI的S7服务开关没开,或者固件版本不支持这个区域。去HMI组态里找“允许远程S7访问/OPC UA服务”相关选项。第四步,如果以上都正常但还是读不到,回到变量表,确认你要读的变量到底在HMI内部还是PLC,是不是映射到了错误地址。

现场最坑的一种情况是:HMI的IP能ping通,102端口也开着,但HMI组态里根本没有开放对外的数据访问。这种问题在网络层和代码层面都看不出来,只有回到组态软件才能发现。

5.2 读值不对劲时的六个自查项

读出来的数据不对时,不要急着怀疑设备,按下面六项自查,基本能覆盖九成问题:

  1. 变量类型真的确认成LReal了吗?有没有可能是Real,宽度差一倍。
  2. 偏移量和变量表里完全一致吗?LReal是否踩到了对齐后重新计算过的偏移。
  3. 读取长度够不够?DInt读4字节、LReal读8字节,缓冲区切片时有没有越界。
  4. 是不是用错了解析函数?DInt用get_dint,LReal用get_lreal,两者交叉使用会得到荒谬结果。
  5. DB号对得上吗?有些项目里多个DB都存在,读错DB号时会得到相似但错误的数据。
  6. 数据在HMI里真的已经更新了吗?HMI内部变量往往按刷新周期对外更新,刚写入的值不会立刻出现。

如果数据偶尔对、偶尔错,优先怀疑通讯时序和数据一致性。比如我上文说的合并读取,就是为了避免分开两次读导致的值不匹配。如果是“每次读都对,过一会就不对”,再看下是不是目标设备重新上电后连接被重置,这时候重连逻辑就起作用了。

5.3 合并读取、PDU限制与数据新鲜度

最后聊几个只有到了现场才会注意到的细节。

第一,read_area的单次读取长度受PDU限制。S7常见的PDU大小在240字节左右,虽然较新的S7-1200/1500支持协商更大的包,但HMI侧作为服务器时,某些固件可能只认较小的请求。所以批量读取时,我合并出来的块不要贪大,控制在200字节以内比较稳。如果实在需要读很大的DB,分段读然后拼接。

第二,数据有效性判断。触摸屏和PLC的通讯一旦中断,HMI画面上往往显示的还是断线前的缓存值,通过S7服务读取也可能拿到“假新鲜”的数据。所以在数据采集配置里加一个时间戳、版本号或者心跳计数变量,每次读取时一并取回,程序里判断这个值是否在跳变。这个习惯能帮你过滤掉大量脏数据。

第三,HMI内部变量和PLC变量的刷新周期不一样。直连HMI读内部变量时,不要用太高的轮询频率,否则既占通讯资源,又可能读到上一拍的数据。一般500毫秒到1秒的轮询间隔足够产线追溯用了。

用Snap7读西门子触摸屏的DInt和LReal,说白了就是三件事:弄明白数据存在哪个设备、把Rack/Slot和DB偏移对准、用大端序把字节解析成正确类型。真正常出问题的地方从来不是Python代码,而是你对设备里数据布局的理解。我现在的习惯是,动手写程序前先花半小时把变量表整理成Excel,标注每个变量的类型、DB号、偏移、对齐间隔,再写采集配置。这个前期工作看着费时间,但在现场能帮你少熬好几个夜。

最后再分享一个小技巧:当你直连HMI始终不顺利的时候,别死磕S7这条路。很多新款HMI对外提供OPC UA服务,用Python的OPC UA客户端也能读,而且不需要纠结Rack/Slot。当然那是另一个话题了,先把本文里的DInt和LReal读通,你会对S7通讯的脾气摸得更准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询