Modbus协议及基于Python的ModbusTCP客户端实现
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 |
| 输入寄存器(Input Register) | 字 | 只读 | 0x04 | PLC中只能从模拟量输入端改变的寄存器 | 4x |
| 保持寄存器(Holding 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:对数据进行格式化 >表示大端模式, 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)
本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/86f50460e5.html
