4. S7comm 协议读取西门子PLC数据

任务目标

本案例实现一个基于 S7comm 协议的西门子 PLC 数据读取程序,目标设备为 S7-1200 系列,读取其 DB1 块中偏移 0 开始的 4 个字节(即一个 REAL 实数)。程序使用纯 Python 实现,不依赖第三方 S7 通信库,直接构造 S7comm 报文完成连接、读写请求与响应解析。

环境与依赖

  • Python 3.8+,仅使用标准库 socket、struct
  • 目标 PLC:西门子 S7-1200(固件版本 V4.0+),需开启 PUT/GET 通信权限
  • PLC 网络参数:IP 地址、机架号(Rack)、槽号(Slot),默认 S7-1200 通常为 Rack=0, Slot=1

实现原理

S7comm 协议基于 ISO-on-TCP(RFC 1006),通信过程分为三步:建立 TCP 连接后先完成 ISO 连接握手(CR 报文),再发送 S7comm 协商报文(Job 0xF0),最后发送数据读取请求(Read Var)。响应报文需按 TPKT 头、S7 头、参数区和数据区逐层解析。本案例代码完整实现了上述三个步骤,并正确解析返回的 REAL 值。

关键完整代码

import socket
import struct

class S7CommClient:
    def __init__(self, ip, rack=0, slot=1, port=102):
        self.ip = ip
        self.rack = rack
        self.slot = slot
        self.port = port
        self.sock = None

    def connect(self):
        """建立 TCP + ISO-on-TCP + S7comm 连接"""
        self.sock = socket.create_connection((self.ip, self.port), timeout=5)
        # 1. ISO 连接请求 (CR)
        cr = bytes.fromhex(
            '0300001601100000000000c0010ac102' +
            f'0100c2{self.rack:02x}c3{self.slot:02x}'
        )
        self.sock.send(cr)
        resp = self._recv_tpkt()
        if len(resp) < 22 or resp[21] != 0x00:  # CR 响应第22字节为0表示成功
            raise RuntimeError("ISO 连接失败")

        # 2. S7comm 协商 (Job 0xF0)
        negotiate = bytes.fromhex(
            '0300001902f0803201000000000008000000f00000010001'
        )
        self.sock.send(negotiate)
        resp = self._recv_tpkt()
        if len(resp) < 27 or resp[26] != 0x00:
            raise RuntimeError("S7 协商失败")

    def read_real(self, db_number, offset):
        """读取 DB 块中指定偏移的 REAL 值(4字节)"""
        # 构造 Read Var 请求
        # 参数区:功能码 0x04(读),项数 1
        # 该项:变量类型 0x12(DB),读取长度 4,DB号,偏移量
        param = bytes.fromhex('040100120004') + struct.pack('>H', db_number) + struct.pack('>H', offset)
        # 数据区长度 = 0(读请求无数据)
        data_len = 0
        # 组装 S7 PDU(含 TPKT + COTP + S7 头)
        s7_header = bytes.fromhex('02f080') + struct.pack('>H', 0x0001) + struct.pack('>H', 0x0000) + struct.pack('>H', len(param) + data_len)
        pdu = s7_header + param
        tpkt = bytes.fromhex('0300') + struct.pack('>H', len(pdu) + 4)
        self.sock.send(tpkt + pdu)

        resp = self._recv_tpkt()
        # 解析响应:S7 头第 5 字节为返回码,0xFF 表示成功
        if len(resp) < 32 or resp[21] != 0xff:
            raise RuntimeError("读取失败,请检查 DB 号和偏移量")
        # 数据区:从第 29 字节开始为实际数据(跳过返回码和传输大小字节)
        data_start = 29
        value_bytes = resp[data_start:data_start+4]
        return struct.unpack('>f', value_bytes)[0]

    def _recv_tpkt(self):
        """接收完整 TPKT 报文"""
        header = self._recv_exact(4)
        if header[0] != 0x03:
            raise RuntimeError("无效 TPKT")
        length = struct.unpack('>H', header[2:4])[0]
        return header + self._recv_exact(length - 4)

    def _recv_exact(self, n):
        data = b''
        while len(data) < n:
            chunk = self.sock.recv(n - len(data))
            if not chunk:
                raise ConnectionError("连接中断")
            data += chunk
        return data

    def close(self):
        if self.sock:
            self.sock.close()

# 使用示例
if __name__ == '__main__':
    client = S7CommClient('192.168.0.1', rack=0, slot=1)
    try:
        client.connect()
        value = client.read_real(db_number=1, offset=0)
        print(f"DB1.DBD0 = {value:.4f}")
    finally:
        client.close()

运行与验证

  1. PLC 侧准备:在 TIA Portal 中创建 DB1,添加一个 REAL 变量(偏移 0),并启用“优化块访问”关闭(即使用标准访问),否则无法通过 S7comm 直接读取。在设备属性中勾选“允许从远程伙伴(PUT/GET 通信)访问”。
  2. 修改参数:将代码末尾的 192.168.0.1 替换为实际 PLC IP,确认 Rack/Slot 与硬件配置一致。
  3. 执行:python s7_read.py
  4. 通过条件:程序输出 DB1.DBD0 = 实际值,且该值与 TIA Portal 监控表中显示的 REAL 值一致(误差在浮点精度范围内)。若输出错误码或超时,检查网络连通性、PLC 权限设置及 DB 偏移量。

常见问题

  • 连接失败:确认 PLC IP 可达(ping),且机架/槽号正确。S7-1200 固件 V4.0 以上默认 Rack=0, Slot=1。
  • 读取返回错误:DB 号或偏移量越界、DB 为优化访问(需关闭)、PLC 未开启 PUT/GET 权限。
  • 字节序问题:S7comm 使用大端序,代码中已通过 struct.pack('>H') 和 '>f' 正确处理。