1. 程式人生 > 其它 >Python通過snap7庫與西門子S7-1200建立S7通訊,讀寫儲存器資料,順便寫個流水燈

Python通過snap7庫與西門子S7-1200建立S7通訊,讀寫儲存器資料,順便寫個流水燈

1.snap7 簡介

snap7 是一個基於乙太網與S7系列的西門子PLC通訊的開源庫。

支援包括S7系列的S7-200、S7-200 Smart、S7-300、S7-400、S7-1200以及S7-1500的乙太網通訊。

適用系統
支援32/64位英特爾/ AMD的所有平臺。
例如:Windows ( 除了 windows Me和95);Linux和類Linux(樹莓派,UBeagleBone Black,DOO 等);BSD;Oracle Solaris ;Apple OSX

支援語言
Pascal;C#;C++;C;LabVIEW;Python;Node.js;Java,其中介紹比較多的是Python。

snap7官方網站

http://snap7.sourceforge.net/

https://pypi.org/project/python-snap7/

https://python-snap7.readthedocs.io/en/latest/

2.S7通訊

西門子S7系列PLC採用以下兩種通訊方式:
1) 開放式的TCP\IP,可以用於連線PLC與其他非西門子硬體
2) 西門子自己開發的S7 Protocol乙太網通訊協議,用於西門子內部硬體通訊

這兩者的傳輸報文是不一樣的,如下圖:

西門子數儲存到二進位制時方式是大端模式(BIG-Endian),而我們的普通電腦常常為小端模式(Liitle-Endian)。
大端模式是指資料的位儲存在記憶體的

地址中,而資料的高位儲存在記憶體的低地址中.
小端模式是指資料的位儲存在記憶體的地址中,而資料的高位儲存在記憶體的高地址中。
例如:雙字 DWORD 0X2F11214C
PLC

PC

所以資料需要進行轉換。

3.安裝snap7庫

pip install python-snap7

筆者使用的是64位Python3.6.4和python-snap7 1.1.0,安裝完成後,環境就算搭建好了。

對於32位Python,需要將Snap7官網下載的Win32目錄下的檔案,複製到Python的安裝根目錄下,如下圖所示:

https://sourceforge.net/projects/snap7/files/1.4.2/snap7-full-1.4.2.7z/download

通過一個連線測試程式碼試試,判斷下環境是否搭建正常。

注意自己新建的檔名不能是snap7,會和庫檔案衝突!

import snap7
client = snap7.client.Client()
client.connect('192.168.0.1', 0, 1)
client.disconnect()

如果是下圖提示,則環境正常(192.168.0.1的PLC不存在)。

如果是下圖提示,則環境異常(snap7庫安裝不正確)。

4.讀寫PLC

4.1配置S7-1200

環境搭建正常後,在正式建立通訊前PLC還需做些配置工作,主要是開發自身的讀寫許可權。具體參照下圖配置:

設定訪問級別 設定DB塊的屬性

通過上述配置,PLC可以正常通訊了。

4.2使用snap7讀寫儲存器

python-snap7重要的兩個方法是read_area和write_area,通過這兩個方法就能讀和寫PLC的對應儲存地址。

摘自client.py

def read_area(self, area: Areas, dbnumber: int, start: int, size: int) -> bytearray:
    """Reads a data area from a PLC
    With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters.

    Args:
        area: area to be read from.
        dbnumber: number of the db to be read from. In case of Inputs, Marks or Outputs, this should be equal to 0.
        start: byte index to start reading.
        size: number of bytes to read.

    Returns:
        Buffer with the data read.

    Raises:
        :obj:`ValueError`: if the area is not defined in the `Areas`

    Example:
        >>> import snap7
        >>> client = snap7.client.Client()
        >>> client.connect("192.168.0.1", 0, 0)
        >>> buffer = client.read_area(snap7.types.Areas.DB, 1, 10, 4)  # Reads the DB number 1 from the byte 10 to the byte 14.
        >>> buffer
        bytearray(b'\\x00\\x00')
    """
    if area not in Areas:
        raise ValueError(f"{area} is not implemented in snap7.types")
    elif area == Areas.TM:
        wordlen = WordLen.Timer
    elif area == Areas.CT:
        wordlen = WordLen.Counter
    else:
        wordlen = WordLen.Byte
    type_ = snap7.types.wordlen_to_ctypes[wordlen.value]
    logger.debug(
        f"reading area: {area.name} dbnumber: {dbnumber} start: {start}: amount {size}: wordlen: {wordlen.name}={wordlen.value}")
    data = (type_ * size)()
    result = self._library.Cli_ReadArea(self._pointer, area.value, dbnumber, start,
                                        size, wordlen.value, byref(data))
    check_error(result, context="client")
    return bytearray(data)


@error_wrap
def write_area(self, area: Areas, dbnumber: int, start: int, data: bytearray) -> int:
    """Writes a data area into a PLC.

    Args:
        area: area to be write.
        dbnumber: number of the db to be write to. In case of Inputs, Marks or Outputs, this should be equal to 0.
        start: byte index to start writting.
        data: buffer to be write.

    Returns:
        Snap7 error code.

    Exmaple:
        >>> import snap7
        >>> client = snap7.client.Client()
        >>> client.connect("192.168.0.1", 0, 0)
        >>> buffer = bytearray([0b00000001])
        >>> client.write_area(snap7.types.Areas.DB, 1, 10, buffer)  # Writes the bit 0 of the byte 10 from the DB number 1 to TRUE.
    """
    if area == Areas.TM:
        wordlen = WordLen.Timer
    elif area == Areas.CT:
        wordlen = WordLen.Counter
    else:
        wordlen = WordLen.Byte
    type_ = snap7.types.wordlen_to_ctypes[WordLen.Byte.value]
    size = len(data)
    logger.debug(f"writing area: {area.name} dbnumber: {dbnumber} start: {start}: size {size}: "
                 f"wordlen {wordlen.name}={wordlen.value} type: {type_}")
    cdata = (type_ * len(data)).from_buffer_copy(data)
    return self._library.Cli_WriteArea(self._pointer, area.value, dbnumber, start,
                                       size, wordlen.value, byref(cdata))

《SIMATIC S7-1200 可程式設計控制器系統手冊》節4.2.1有如下描述:

PLC的資料儲存通過“變數”的形式與儲存區間關聯,分為輸入(I)、輸出(O)、位儲存(M)和資料塊(DB)。程式在訪問對應(I/O)儲存區時,是通過訪問CPU的過程映像對相應地址進行操作的。具體對應關係如下:

故python-snap7中定義的Areas含義為

'PE': 0x81, #input

'PA': 0x82, #output

'MK': 0x83, #bit memory

'DB': 0x84, #DB

'CT': 0x1C, #counters

'TM': 0x1D, #Timers

現在離讀寫PLC還差最後一步,就是起始地址如何確定呢?

對於M3.4,對應的就是M(0x83),起始地址是3,對應bit位是4。

4.3資料儲存地址

https://support.industry.siemens.com/cs/document/57374718

1、BIT        :位是儲存空間的最小單位;
2、BYTE    :位元組,由 8 個位組成;
3、WORD  :字,由2個位元組組成,共16個位。
4、DWORD:雙字,由2個字組成,共32個位。
第二:絕對地址定址(同一儲存空間)
M0.0 位   :     M            0         .    0
                   儲存區  位元組地址     位號
MB0 位元組:     M            B              0
                    儲存區  位元組尋地     位元組起始地址
                    含 M0.0-M0.7 共 8個位
MW0   字:      M           W              0
                    儲存區    字尋地      位元組起始地址
                    含MB0、MB1, 即M0.0-M0.7以及 M1.0-M1.7 共 16個位
MD0 雙字:     M           D              0
                    儲存區    雙字尋地      位元組起始地址
                    含MB0、MB1、MB2、MB3, 即M0.0-M3.7共 32 個位

4.4讀寫示例

import struct
import time
import snap7

def plc_connect(ip, rack=0, slot=1):
    '''
    連線初始化
    :param ip:
    :param rack: 通常為0
    :param slot: 根據plc安裝,一般為0或1
    :return:
    '''
    client = snap7.client.Client()
    client.connect(ip, rack, slot)
    return client

def plc_con_close(client):
    """
    連線關閉
    :param client:
    :return:
    """
    client.disconnect()

def test_mk10_1(client):
    """
    測試M10.1
    :return:
    """
    area = snap7.types.Areas.MK
    dbnumber = 0
    start = 10
    amount = 1
    print('初始值',end='')
    mk_data = client.read_area(area, dbnumber, start, amount)
    print(mk_data)#struct.unpack('!c', mk_data)
    print('置1')
    client.write_area(area, dbnumber, start, b'\x01')
    print('當前值',end='')
    mk_cur = client.read_area(area, dbnumber, start, amount)
    print(mk_cur)

def test_mk_w201(client):
    """
    測試MW201,資料型別為word
    :param client:
    :return:
    """
    area = snap7.types.Areas.MK
    dbnumber = 0
    amount = 2
    start = 201
    print(u'初始值')
    mk_data = client.read_area(area, dbnumber, start, amount)
    print(struct.unpack('!h', mk_data))
    print(u'置12')
    client.write_area(area, dbnumber, start, b'\x12')
    print(u'當前值')
    mk_cur = client.read_area(area, dbnumber, start, amount)
    print(struct.unpack('!h', mk_cur))
    time.sleep(3)
    print(u'置3')
    client.write_area(area, dbnumber, start, b'\x02')
    print(u'當前值')
    mk_cur = client.read_area(area, dbnumber, start, amount)
    print(struct.unpack('!h', mk_cur))

def test_q0_0(client):
    """
    測試Q0.0,會使其輸出高電平
    :return:
    """
    area = snap7.types.Areas.PA
    dbnumber = 0
    start = 0
    amount = 1
    print('初始值',end='')
    mk_data = client.read_area(area, dbnumber, start, amount)
    print(mk_data)#struct.unpack('!c', mk_data)
    print('置1')
    client.write_area(area, dbnumber, start, b'\x01')
    print('當前值',end='')
    mk_cur = client.read_area(area, dbnumber, start, amount)
    print(mk_cur)

def test_MD1012(client):
"""
測試MD1012,資料型別為real
:param client:
:return:
"""
area = snap7.types.Areas.MK
dbnumber = 0 # 讀取的DB塊序號。當讀取的地址為I/Q/M時,序號為0
amount = 4
start = 1012
mk_data = client.read_area(area, dbnumber, start, amount)
print(struct.unpack('>f', mk_data)[0])
if __name__ == "__main__":
    client_fd = plc_connect('192.168.0.2')
    # test_mk10_1(client_fd)
    # test_mk_w201(client_fd)
    test_q0_0(client_fd)
    plc_con_close(client_fd)


'''
'PE': 0x81, #input, I
'PA': 0x82, #output, Q
'MK': 0x83, #bit memory, M
'DB': 0x84, #DB, DBX
'CT': 0x1C, #counters
'TM': 0x1D, #Timers
'''

也可通過db_read()和db_write()讀寫DB塊

import snap7
client = snap7.client.Client()
client.connect('192.168.0.2', 0, 1)
plc_db1 = client.db_read(1, 0, 3)  # 讀取資料塊db1,起始位元組,讀取長度
print(plc_db1) # hex(plc_db1[0])
client.db_write(1, 0, b'\x11') # 寫入資料塊db1,起始位元組,資料hex
plc_db1 = client.db_read(1, 0, 3)  # 讀取資料塊db1,起始位元組,讀取長度
print(plc_db1)
client.disconnect()

讀寫MW變數時,要在變量表中先建立變數,並燒錄進S7-1200

5.流水燈

import struct
import time
import snap7

def plc_connect(ip, rack=0, slot=1):
    '''
    連線初始化
    :param ip:
    :param rack: 通常為0
    :param slot: 根據plc安裝,一般為0或1
    :return:
    '''
    client = snap7.client.Client()
    client.connect(ip, rack, slot)
    return client

def plc_con_close(client):
    """
    連線關閉
    :param client:
    :return:
    """
    client.disconnect()

def ledtrip(client):
    """
    跑馬燈,使Q0.0~5迴圈亮起
    :return:
    """
    area = snap7.types.Areas.PA
    dbnumber = 0
    start = 0
    delayTime = 0.5
    for i in range(10):
        client.write_area(area, dbnumber, start, bytearray([0b00000001]))
        time.sleep(delayTime)
        client.write_area(area, dbnumber, start, bytearray([0b00000010]))
        time.sleep(delayTime)
        client.write_area(area, dbnumber, start, bytearray([0b00000100]))
        time.sleep(delayTime)
        client.write_area(area, dbnumber, start, bytearray([0b00001000]))
        time.sleep(delayTime)
        client.write_area(area, dbnumber, start, bytearray([0b00010000]))
        time.sleep(delayTime)
        client.write_area(area, dbnumber, start, bytearray([0b00100000]))
        time.sleep(delayTime)
    client.write_area(area, dbnumber, start, b'\x00')


if __name__ == "__main__":
    client_fd = plc_connect('192.168.0.2')
    ledtrip(client_fd)
    plc_con_close(client_fd)

參考

https://pypi.org/project/python-snap7/

https://python-snap7.readthedocs.io/en/latest/

https://blog.csdn.net/zxpbuct/article/details/80079698

https://blog.csdn.net/lcb411/article/details/101147181

https://www.toutiao.com/a6589203413941092868

http://www.6dm.club/index.php/2018/04/07/