您现在的位置是:首页 > 技术教程 正文

Modbus协议及基于Python的ModbusTCP客户端实现

admin 阅读: 2024-03-22
后台-插件-广告管理-内容页头部广告(手机)

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 002字节可以理解为报文的序列号,一般每次通信之后就要加1
协议标识符00 002字节00 00表示ModbusTCP协议
长度00 062字节表示接下来的数据长度,单位为字节
单元标识符011字节串行链路或其它总线上连接的远程从站的识别码

3 数据模型

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

寄存器种类数据类型访问类型功能码含义
线圈状态(Coil Status)读写0x01 0x05 0x0FPLC的输出位,开关量0x
离散输入状态(Input Status)只读0x02PLC的输入位,开关量1x
输入寄存器(Input Register)只读0x04PLC中只能从模拟量输入端改变的寄存器4x
保持寄存器(Holding Register)读写0x03 0x06 0x10PLC中用于输出模拟量信号的寄存器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位的,常用数据类型及长度:

FormatC TypePython type字节数PLC 寄存器数量大端模式
hshortinteger21AB
Hunsigned shortinteger21AB
iintinteger21AB
Iunsigned intinteger21AB
llonglong42AB CD
Lunsigned longinteger42AB CD
ffloatfloat42AB CD
ddoublefloat84AB 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜