Python实现ModbusTCP客户端及详解Modbus协议

Modbus 协议是由 Modicon 公司(现在的施耐德电气 Schneider Electric )于1979年为使用可编程逻辑控制器(PLC)通信而推出,主要建立在物理串口、以太网 TCP/IP 层之上,目前已经成为工业领域通信协议的业界标准,广泛应用在工业电子设备之间的互联。

Modbus技术文档

1 网络模型

Modbus 是OSI模型第 7 层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。

Modbus 是一个请求/应答协议,并且提供功能码规定的服务。

2 Modbus 协议描述

Modbus 主要有 4 种通信模式:

Modbus 协议类型 描述
RTU 模式(串口) 二进制表示数据,采用循环冗余校验的校验和
ASCII 模式(串口) 采用人类可读的、冗长的表示输入,采用纵向冗余校验的校验和
TCP 模式(网口) 基于TCP通信,与Modbus RTU相似,取消循环冗余校验的校验和
UDP 模式(网口) 基于UDP通信,与Modbus RTU相似,取消循环冗余校验的校验和

协议格式:

Modbus 协议类型 协议格式
Modbus RTU [地址码] [功能码] [数据] [CRC校验码]
Modbus ASCII [起始冒号] [地址码] [功能码] [数据] [LRC校验码] [回车换行]
Modbus TCP [事务处理标识] [协议标识] [长度] [单元标识符] [功能码] [数据]
Modbus UDP [事务处理标识] [协议标识] [长度] [单元标识符] [功能码] [数据]

Modbus 协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。

PDU = [功能码] [数据] ,功能码为1字节,数据长度不定,由具体功能决定。

标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。

modbus-RTU (Remote Terminal Unit 远程终端单元)是数据在串口RS485等链路上传输的。

modbus-TCP 是使用以太网TCP网络进行通信的,使用502端口,客户端和服务端模式。

Modbus-RTU 数据格式

modbus-RTU 数据包格式=PDU+CRC

PDU由功能码+数据组成。功能码为1字节,数据长度不定,由具体功能决定。

CRC为校验码,为2个字节。

Modbus-TCP 数据格式

modbus-TCP 数据帧格式=MBAP+PDU

MBAP为报文头,长度为7字节,由事务处理标识+协议标识符+长度+单元标识符组成:

示例内容 长度 描述
事务处理标识 00 00 2字节 可以理解为报文的序列号,一般每次通信之后就要加1
协议标识符 00 00 2字节 00 00表示ModbusTCP协议
长度 00 06 2字节 表示接下来的数据长度,单位为字节
单元标识符 01 1字节 串行链路或其它总线上连接的远程从站的识别码

3 数据模型

Modbus 协议中一个重要的概念是寄存器,所有的数据均存放于寄存器中。最初Modbus协议借鉴了PLC中寄存器的含义,但是随着Modbus协议的广泛应用,寄存器的概念进一步泛化,不再是指具体的物理寄存器,也可能是一块内存区域。Modbus寄存器根据存放的数据类型以及各自读写特性,将寄存器分为4个部分,这4个部分可以连续也可以不连续,由开发者决定。

寄存器种类 数据类型 访问类型 功能码 含义
线圈状态(Coil Status) 读写 0x01 0x05 0x0F PLC的输出位,开关量 0x
离散输入状态(Input Status) 只读 0x02 PLC的输入位,开关量 1x
输入寄存器(Holding Register) 只读 0x04 PLC中只能从模拟量输入端改变的寄存器 4x
保持寄存器(Input Register) 读写 0x03 0x06 0x10 PLC中用于输出模拟量信号的寄存器 3x

4 功能码

Modbus 功能码分三类:公共功能码、用户定义功能码、保留功能码。

Modbus 的常用公共功能码有:

功能码 名称 英文名 位操作/字操作 操作数量
0x01 读线圈状态 READ COIL STATUS 位操作 单个或多个
0x02 读离散输入状态 READ INPUT STATUS 位操作 单个或多个
0x03 读保持寄存器 READ HOLDING REGISTER 字操作 单个或多个
0x04 读输入寄存器 READ INPUT REGISTER 字操作 单个或多个
0x05 写单个线圈状态 WRITE SINGLE COIL 位操作 单个
0x06 写单个保持寄存器 WRITE SINGLE REGISTER 字操作 单个
0x0F 写多个线圈 WRITE MULTIPLE COIL 位操作 多个
0x10 写多个保持寄存器 WRITE MULTIPLE REGISTER 字操作 多个

字节序及数据类型

字节序分类:

存储模式 data_fromat前缀 说明
大端模式 > 数据的高字节保存在寄存器的低地址中,数据的低字节保存在寄存器的高地址中
小端模式 < 数据的高字节保存在寄存器的高地址中,而数据的低字节保存在寄存器的低地址中

Modbus采用大端字节序进行报文传输,字节序不正确则对多字节数据无法解析和组拼。

每个寄存器有两个字节,第一个字节包括高位比特,并且第二个字节包括低位比特。

Modbus 每个寄存器地址是16位的,常用数据类型及长度:

Format C Type Python type 字节数 PLC 寄存器数量 大端模式
h short integer 2 1 AB
H unsigned short integer 2 1 AB
i int integer 2 1 AB
I unsigned int integer 2 1 AB
l long long 4 2 AB CD
L unsigned long integer 4 2 AB CD
f float float 4 2 AB CD
d double float 8 4 AB CD EF GH

1个字节是8个比特,即:1byte = 8bit

Modbus 以16位为一个字进行编址,寄存器是16位的,可以存放两个字节 ,即1寄存器 = 2字节

data_format:对读写数据进行格式化,示例:

>f 中的 > 表示大端模式,f表示1个float,共有4个字节,占用2个寄存器。

>dd 中的 > 表示大端模式,dd表示两个double,共有16个字节,占用8个寄存器。

5 Python示例

Java 的 modbus4j、Python 的 modbus_tk 等第三方库对 modbus 做了很好的封装,开发者通常不需要关注请求、响应、错误的报文解析,第三方库已经根据功能码、数据类型、数据数量等对报文进行了解析。

下面是 Python3 的 modbus 使用示例:

import modbus_tk.modbus_tcp as mt
import modbus_tk.defines as cst


if __name__ == '__main__':
    master = mt.TcpMaster('127.0.0.1', 502)
    master.set_timeout(5)
    # 参数说明
    # slave: Modbus从站地址. from 1 to 247. 0为广播所有的slave
    # function_code:功能码
    # starting_address:寄存器起始地址
    # quantity_of_x:寄存器读写的数量,写寄存器时数量可为 0,读寄存器时数量至少为 1; 一个寄存器=2字节, 1字节=8位
    # output_value:输出内容,读操作无效,写操作是一个整数或可迭代的list值:1 / [1,1,1,0,0,1] / xrange(12)
    # data_format:对数据进行格式化 >表示大端模式, <表示小端模式, H表示unsigned short无符号整形(2字节), h表示short有符号整型(2字节), l表示long长整型(4字节), f表示float浮点型(4字节), d表示double双精度浮点型(8字节)
    # expected_length

    try:
        '''
        寄存器类型:线圈状态
        访问类型:读写
        功能码:0x01、0x05、0x0F
        '''

        '''
        0x05功能码:写线圈状态为 开ON(0xff00)/关OFF(0), output_value不为0时都会置为0xff00
        ON的output_value可以设置为 0xff00、True、非0值; OFF的output_value 可以设置为 0x0000、False、0
        返回结果:tuple(地址, 值) ,写成功:如写入开则返回0xff00的十进制格式65280,写入关则返回0x0000的十进制格式0
        '''
        ''' 0x05功能码: 写单个线圈状态 ON '''
        single_coil_value = master.execute(slave=1, function_code=cst.WRITE_SINGLE_COIL, starting_address=0, output_value=1)     # 写单个线圈状态为 ON
        print('0x05 WRITE_SIGNLE_COIL: ', single_coil_value)

        ''' 0x01功能码:读线圈状态 '''
        coils_value = master.execute(slave=1, function_code=cst.READ_COILS, starting_address=0, quantity_of_x=1)    # 读线圈状态
        print('0x01 READ_COILS: ', coils_value)

        ''' 0x05功能码: 写单个线圈状态 OFF '''
        single_coil_value = master.execute(slave=1, function_code=cst.WRITE_SINGLE_COIL, starting_address=1, output_value=0)    # 写单个线圈状态为 OFF
        print('0x05 WRITE_SIGNLE_COIL: ', single_coil_value)
        coils_value = master.execute(slave=1, function_code=cst.READ_COILS, starting_address=1, quantity_of_x=1)    # 读线圈状态
        print('0x01 READ_COILS: ', coils_value)

        ''' 0x0F功能码:写多个线圈状态 '''
        multiple_coils_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_COILS, starting_address=2, quantity_of_x=4, output_value=[1, 1, 1, 1])  # 写多个线圈
        print('0x0F WRITE_COILS_REGISTER: ', multiple_coils_value)
        coils_value = master.execute(slave=1, function_code=cst.READ_COILS, starting_address=2, quantity_of_x=4)    # 读线圈状态
        print('0x01 READ_COILS: ', coils_value)

        '''
        寄存器类型:离散输入状态
        访问类型:只读
        功能码:0x02
        '''
        # 0x02功能码:读离散输入状态
        discrete_value = master.execute(slave=1, function_code=cst.READ_DISCRETE_INPUTS, starting_address=0, quantity_of_x=5)
        print('0x02 READ_DISCRETE_INPUTS: ', discrete_value)

        '''
        寄存器类型:输入寄存器
        访问类型:只读
        功能码:0x04
        '''
        # 0x04功能码:读输入寄存器
        input_value = master.execute(slave=1, function_code=cst.READ_INPUT_REGISTERS, starting_address=0, quantity_of_x=5)
        print('0x04 READ_INPUT_REGISTERS: ', input_value)

        '''
        寄存器类型:保持寄存器
        访问类型:读写
        功能码:0x03、0x06、0x10
        '''
        # 0x06功能码:写单个保持寄存器
        single_register_value = master.execute(slave=1, function_code=cst.WRITE_SINGLE_REGISTER, starting_address=0, output_value=666)
        print('0x06 WRITE_SINGLE_REGISTER: ', single_register_value)

        # 0x03功能码:读保持寄存器
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=0, quantity_of_x=1)
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)

        # 0x10功能码:写多个保持寄存器
        multiple_registers_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_REGISTERS, starting_address=1, quantity_of_x=3, output_value=[777, 777, 777])
        print('0x10 WRITE_MULTIPLE_REGISTERS: ', multiple_registers_value)
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=1, quantity_of_x=3)
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)


        # 数据类型
        # 写单个寄存器:无符号整数
        single_register_value = master.execute(slave=1, function_code=cst.WRITE_SINGLE_REGISTER, starting_address=0, output_value=4097)
        print('0x06 WRITE_SINGLE_REGISTER: ', single_register_value)
        # 写单个寄存器:有符号整数
        single_register_value = master.execute(slave=1, function_code=cst.WRITE_SINGLE_REGISTER, starting_address=1, output_value=-1234)
        print('0x06 WRITE_SINGLE_REGISTER: ', single_register_value)
        # 写多个寄存器:有符号整数 (根据列表长度来判读写入个数)
        multiple_registers_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_REGISTERS, starting_address=2, output_value=[1, -2], data_format='>hh')
        print('0x10 WRITE_MULTIPLE_REGISTERS: ', multiple_registers_value)
        # 读寄存器
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=0, quantity_of_x=4, data_format='>hhhh')
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)

        # 写多个寄存器: 浮点数float(float长度为4个字节,占用2个寄存器)
        # 起始地址为8的保持寄存器,操作寄存器个数为 4 ,一个浮点数float 占两个寄存器;
        # 写浮点数时一定要加 data_format 参数,两个ff 表示要写入两个浮点数,以此类推
        # 我这里模拟的是大端模式,具体可参考 struct 用法。和数据源保持一致即可。 <表示小端,>表示大端
        multiple_registers_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_REGISTERS, starting_address=8, output_value=[1.0, -6.4], data_format='>ff')
        print('0x10 WRITE_MULTIPLE_REGISTERS: ', multiple_registers_value)
        # 读对应的 4个寄存器(2个float),指定数据格式
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=8, quantity_of_x=4, data_format='>ff')
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)

        # 写多个寄存器:长整型数据long(long长度为4字节,占用2个寄存器)
        multiple_registers_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_REGISTERS, starting_address=12, output_value=[111111, -222222], data_format='>ll')
        print('0x10 WRITE_MULTIPLE_REGISTERS: ', multiple_registers_value)
        # 读对应的 4个寄存器(2个double),指定数据格式
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=12, quantity_of_x=4, data_format='>ll')
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)

        # 写多个寄存器:双精度浮点数double(double长度为8个字节,占用4个寄存器)
        multiple_registers_value = master.execute(slave=1, function_code=cst.WRITE_MULTIPLE_REGISTERS, starting_address=16, output_value=[1, -6.4], data_format='>dd')
        print('0x10 WRITE_MULTIPLE_REGISTERS: ', multiple_registers_value)
        # 读对应的 4个寄存器(2个double),指定数据格式
        holding_value = master.execute(slave=1, function_code=cst.READ_HOLDING_REGISTERS, starting_address=16, quantity_of_x=8, data_format='>dd')
        print('0x03 READ_HOLDING_REGISTERS: ', holding_value)
    except Exception as e:
        print('error: %s' % e)

物联沃分享整理
物联沃-IOTWORD物联网 » Python实现ModbusTCP客户端及详解Modbus协议

发表评论