使用的PLC是目前主流的西门子S7-1200. 它支持的网络标准/协议很多,比如PROFINET, PROFIBUS等,还可以间接连接Modbus设备。每个标准下都有很多服务/协议,详情可以参考 Communication with SIMATIC 。但这些标准有些是用于西门子的设备互联的,不一定适用于PC。

下图是 TIA Portal V14 中通信相关的指令,也可以作为线索。

图:PLC通信指令

和PC的通信,一种方式是使用OPC server,但它是基于OLE/COM的,只能用于Windows。有些软件比如LabView提供了和西门子PLC通信的支持。跨平台的开源的方案,有一个是Snap7。我们可以先试试这个。另外可以尝试最原始的TCP协议。

Snap7

Snap7 是针对西门子S7协议的。PLC不需任何配置就是S7的server,而我们只需要利用Snap7 lib,就可以让PC作为S7 client,读/写服务器端的数据块。

数据块映射

数据块分为输入区(DI, AI),输出区(DQ, AQ),程序数据块(DB)等等。下图中,DB3是测试程序的数据块。

Python版的Snap7

有时使用脚本语言会更方便一些。 python-snap7 就是一个Snap7 lib的Python封装。因为只是接口层的封装,对速度的影响很小。

安装时需要先安装Snap7的库,再用pip安装python-snap7。有些平台没有现成的Snap7的库,需要自己编译。反正树莓派上我是自己编译的。 实测Python2和Python3都可以工作。

核心调用代码如下。因为I/O只有两字节,就直接读/写两字节了。

import snap7
from snap7.snap7types import S7AreaDB, S7AreaPA, S7AreaPE
class S7Client:
    def __init__(self, ip, slot=1, rack=0):
        self.client = snap7.client.Client()
        self.client.connect(ip, rack, slot)
    def readDI(self):
        area = S7AreaPE
        db = 0
        start = 0
        amount = 2
        ba = self.client.read_area(area, db, start, amount)
        d = ba[1]
        d <<= 8
        d |= ba[0]
        return d
    def writeDQ(self, data):
        area = S7AreaPA
        db = 0
        start = 0
        amount = 2
        ba = bytearray(amount)
        ba[0] = data & 0xff
        ba[1] = data >> 8
        self.client.write_area(area, db, start, ba)

循环读/写DQ,看看总耗时。示意代码如下:

    def testWriteLoop(self, count):
        d = 0
        self.log.info("Write DQ from: %04x", d)
        t1 = time.time()
        while d < count:
            self.plc.writeDQ(d)
            d += 1
        t2 = time.time()
        self.log.info("Write DQ till: %04x. Average: %.2fms", 
            (d - 1), (t2 - t1) * 1000 / d)

可以看到单次读/写的平均时间略高于9ms.

如果PC做Snap7的服务器,则PLC需要使用GET/PUT指令读/写PC端的数据。既然都是S7协议,我们假设它的速度和正向是相当的,暂且跳过,先试试另一类型的通信。

原始的TCP通信

S7-1200支持开放式用户通信,即基于TCP,但不属于任何标准应用层协议的,完全由用户自己定义的协议。

  • PC端作为服务器:实际测试使用树莓派充当PC的角色。
  • PLC端发送数据:由一个DI来触发数据发送。
  • 树莓派开启数据发送:通过控制一个GPIO来开关继电器,进而改变PLC端的DI(信号1);
  • 树莓派在收到数据后,改变另一个GPIO的状态(信号2)作为标志;
  • 比较信号2和信号1的时间差。
  • self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.bind(('', self.args.port)) self.sock.listen(1) self.conn, addr = self.sock.accept() data = self.conn.recv(32) self.log.info("Received: %02X %02X", data[0], data[1])

    可以用 netcat 测试这个服务器程序。

    图:PLC循环时间

    这说明循环时间并不是瓶颈。而且反过来,循环时间比通信时间还短(即使输入滤波器为6.4ms,通信时间9ms时,循环时间依然是1~2ms),这说明通信和循环似乎是分头执行的。

    本来还想试一下中断执行方式的,但把通信程序块放到中断响应里执行并没有成功。考虑到对于PLC的百兆网口,3ms已经够快了,就没再折腾了。

    还试验了一下,在PLC上单纯地增加一个计数器或反复翻转输出电平,每次操作耗时大约也是3ms。

    顺便说一句,在PLC的数字输出上,却看不到电平的翻转(看到的总是高电平)。前面有一张“递增写DQ时DQ0.0的波形”图,18ms的周期,基本上已看不到电平下降到0了。感觉PLC的输出频率并不高,甚至可能有高频滤波。

    从PLC的众多网络通信方式中,本文试验了简单易行并且跨平台的两种方式,用来和PC通信。

  • 使用基于S7协议的Snap7库,在读写PLC时大约耗时9ms.
  • 使用开放式的TCP协议,PLC向PC发送数据最快不到3ms.
  • 考虑到S7-1200只是百兆网络,这个速度应该是不错的,可以满足大部分需要。