Modbus协议,首先从字面理解它包括Mod和Bus两部分,首先它是一种bus,即总线协议,总线就意味着有主机,有从机,这些设备在同一条总线上。
Modbus支持单主机,多个从机,最多支持247个从机设备。关于Mod,因为这种协议最早被用在PLC控制器中,准确的说是Modicon公司的PLC控制器,这也是Modbus名称的由来。后来Modicon被施耐德电器收购,Modbus协议广泛应用在工业控制器、HMI和传感器上,逐渐被其他厂商所接受,成为工业领域通信协议的业界标准,并且现在是工业电子设备之间常用的连接方式。
每种设备(PLC、HMI、控制面板、驱动程序、动作控制、输入/输出设备)都能使用 Modbus协议来启动远程操作。
在基于串行链路和以太 TCP/IP 网络的 Modbus 上可以进行相互通信。
一些网关允许在几种使用 Modbus 协议的总线或网络之间进行通信
术语说明
HDLC 高级数据链路控制
HMI 人机界面
IETF 因特网工程工作组
I/O 输入/输出设备
IP 互连网协议
MAC 介质访问控制
MB Modbus 协议
MBAP Modbus 协议
关于总线补充说明
总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。在计算机系统中,各个部件之间传送信息的公共通路叫总线,微型计算机是以总线结构来连接各个功能部件的。
Modbus 协议目前分别定义了基于串口()和以太网传输数据的协议,其中串口(RS232,RS485,RS422,光纤,无线等)数据传输协议分为 Modbus RTU 和 Modbus ASCII串行链路协议,以太网数据传输协议一种 Modbus TCP/IP协议。
其中,Modbus RTU是一种紧凑的,采用二进制表示数据的方式,Modbus ASCII是一种人类可读的,冗长的表示方式。
Modbus 协议采用主从(Master/Salve)通信方式,TCP/IP传输模式下,主节点也叫客户机,从节点也叫服务器。连接到总线上的所有主机节点中有且只有一个 Master 节点(主节点),其余为 Slave 节点(从节点,可选地址为1~247,每个Slave节点的地址必须唯一)。
主从通信以 请求/应答 为主,每次通讯都是主节点先发送请求(采用广播模式、单播模式),从节点响应指令,并按要求应答,或者报告异常。当主节不发送请求时,从节不会自己发出数据,从节点之间不能相互通讯(也就是说从节点之间不能相互发送请求)。
无论主节点发送的是广播指令还是单播指令,实际上所有从节点都会完整接收指令。但发送单播指令时,只有地址和指令中中指定地址相同的从节点才会执行及回应指令,其它从节点将忽略收到的指令,而广播请求所有收到指令的设备都会执行指令,但不会给主机回应指令。
Modbus 由于请求/应答机制所以不能同步通信(同步通信需要收发双方以相同的节奏发送和接收数据),总线上每次只有一帧数据进行传输,属于半双工通信。
Modbus 没有支持繁忙机制处理,例如主机给从机发送命令, 如果从机正在处理其他任务,此时从机将无法响应主机,所以需要通过软件的方式来判断是否正常接收。
Modbus 协议定义了一个与基础通信层无关的简单协议数据单元(PDU -> 功能码 + 数据 部分)。特定总线或网络上
的 MODBUS 协议映射能够在应用数据单元(ADU -> 地址域 + 功能码 + 数据 + 差错校验)上引入一些附加域
地址域 | 功能码 | 数据 | 差错校验码 |
---|---|---|---|
1字节 | 1字节 | N字节 | CRC: 16字节 LRC: 1字节 |
说明:每个划分字段都用16进制表示
地址域
从机设备地址,通常1-247为有效地址,0为广播地址(用于接收主机的广播数据),每个从机在总线上地址必须唯一,只有与主机发送的地址码相符的从机才能响应返回数据。
主节点通过将要联络的从节点的地址放入消息中的地址域来选取需要通信的从设备。当从节点发送回应消息时,需要把自己的地址放入回应的地址域中,以便主节点知道是哪一个设备作出的回应。
功能码
表明主节点请求数据的类型。
当主节点向从设备发送消息时,功能码将告诉从设备需要执行哪些行为。例如去读取输入的开关状态,读一组寄存器的数据内容等。
数据
包含寄存器地址和寄存器数据等
差错校验
对数据进行冗余校验的结果,CRC、LRC
其中事务处理正常时,客户机向服务器发送请求,在功能码中填充功能码代号,说明服务器需要执行的动作,在数据码区填充具体的要求,比如读寄存器的地址和数量,通信正常时服务器会在返回的通信帧的功能码区中填充一个操作码,该操作码=功能码,在通讯帧的数据区填充返回的采样数据。
当出现事务处理异常时,服务器会在返回的通讯帧的功能码中填充一个差错码,该差错码 = 功能码 + 0x80,即将功能码的最高位置1代表出现错误。并在后面的数据段中填充错误码,用来指示本次通信的错误具体内容。
起始位(😃 + ADU + 结束符
起始位 | 地址域 | 功能码 | 数据 | LRC | 结束符 |
---|---|---|---|---|---|
: | 1字节 | 1字节 | N字节 | 1字节 | 2个字符 |
说明:消息以 :
冒号字符(ASCII 码16进制表示 3A
)开始,以回车换行符(CR LF
, ASCII 码16进制表示 0D
,0A
)结束。
一个典型 ASCII 消息帧如下
起始位 | 地址域 | 功能码 | 数据 | LRC | 结束符 |
---|---|---|---|---|---|
: | 2个字符 | 2个字符 | 0 到 2x252 字符 | 2个字符 | 2个字符 |
地址域 | 功能码 | 数据 | CRC低字节 | CRC高字节 |
---|---|---|---|---|
1字节 | 1字节 | 0 到 252 字节 | 1字节 | 1字节 |
说明:RTU 通信模式下,其发送的字节数据即为原始字节数据,接收端接收后无需再次转换。
注意:
字符时间
所谓字符时间指的是传输一个 ASCII 字符需要花费的时间,一个 ASCII 字符包含 1 个字节(8 bits),所以传输一个字符需要花费传输 8 个数据位的时间(所以这里字符传输时间指代传输1字节数据消耗的时间)。
然而实际上传输 1 个字节数据需要花费的时间并不只 8 个位时间,因为除了传输固有的 1 字节数据,还需要传输一些辅助功能位。例如发送 1 个字节需要固定起始位 1 位,数据位 8 位,校验位 1 位(可选的),停止位 1 位,其中 8 位数据位才是真正的有效数据,所以有如下公式来计算字符时间。
字符时间=1s / 波特率 × 字符的字节总位数。
例如:固定起始位 1 位,数据位 8 位,奇/偶校验位 1 位,停止位 1 位,波特率为9600 bps,计算单个字符传输时间为:
字符时间 = 1000 ms / 9600 × ( 1 + 8 + 1 + 1 ) = 1.145833 ms
MBAP 报文头 + PDU(此处PDU来自数据链路层的PDU)
事务元标识符 | 协议标识符 | 长度 | 单元标识符 | 功能码 | 数据 |
---|---|---|---|---|---|
2字节 | 2字节 | 2字节 | 1字节 | 1字节 | N字节 |
MBAP 报文头包括下列域:
事务元标识符
Modbus 请求/响应事务处理的识别,可以理解为报文的序列号,一般每次通信之后就要加 1 以区别不同的通信数据报文
协议标识符
00
00
表示Modbus 协议 客户机启动
长度
表示接下来的数据长度,包括单元标识符和数据域
单元标识符
串行链路或其它总线上连接的远程从节点的识别码,可以理解为从节点地址
MODBUS 使用一个Big-Endian
(低地址位存放最高有效字节) 表示地址和数据项。这意味着当发射多个字节时,首先发送最高有效位。
例如:
寄存器大小 值
16 0x1234 发送的第一字节为 0x12, 然后 0x34
参考阅读:Big Endian和Little Endiand的区别
为了抽象 PLC 中可访问的数据,Modbus 协议定义了 数据模型 概念,数据模型定义了四种可访问的数据类型:
类型 | 大小 | 访问权限 | 元素地址前缀编码 | 元素地址范围(0~65535) | 元素地址范围(1~9999) |
---|---|---|---|---|---|
输出线圈(Coils) | 1 Bit | 可读可写 | 0 | 000000~065535 | 00000~09999 |
输入离散量(Discrete Input) | 1 Bit | 只读 | 1 | 100000~165535 | 10000~19999 |
输入寄存器(Input Registers) | 16 Bit | 只读 | 3 | 300000~365535 | 30000~39999 |
保持寄存器(Holding Registers) | 16 Bit | 可读可写 | 4 | 400000~465535 | 40000~49999 |
实际上以上的数据类型都属于可编程逻辑控制器(PLC)中的术语,可以简单理解为用来存放数据的容器,线圈通常用于表示开关状态(如继电器的通或断),而寄存器通常用于存储线性或非线性的数值数据。
数据模型中的每一种数据类型都最多允许有 65536 个元素,元素的地址编号从 0 开始,因此地址的范围为:0-65535。
需要说明的是:65536 是 Modbus 协议允许的最大元素范围,实际应用中一般不需要这么大的存储区,因此 PLC 厂家普遍采用的是 10000 以内的地址范围。
引入元素地址前缀编码,是为了简化数据模型与设备存储区的对应关系。
参考链接:https://blog.csdn.net/jf_52001760/article/details/130192127
数据模型是一种抽象,在实际使用时必须将其映射到真实的物理存储区才能被访问。
Modbus 协议允许设备将四种数据类型分别映射到不同的存储区块中,各个区块之间相互独立,使用不同的功能码可读取到不同的数值,如下图所示
带有多个独立块的设备
仅有1个块的设备
功能码整体可以分成三类:
常用功能码
功能码 | 名称 | 操作类型 | 功能描述 | |
---|---|---|---|---|
01 | 读线圈状态 | 位操作 | 读位(读 N 个 bit)读从机线圈寄存器 | |
02 | 读输入离散量 | 位操作 | 读位(读 N 个 bit)读离散输入寄存器 | |
03 | 读保持寄存器 | 字节操作 | 读整型,字符型,状态字,浮点型(读 N 个 word)读保持寄存器 | |
04 | 读输入寄存器 | 字节操作 | 读整型,状态字,浮点型(读 N 个word)读输入寄存器 | |
05 | 写单个线圈 | 位操作 | 写位(写 1 个 bit)写线圈寄存器 | |
06 | 写单个保持寄存器 | 字节操作 | 写整型,字符型,状态字,浮点型(写一个 word)写保持寄存器, | |
0F | 写多个线圈 | 位操作 | 写位(写 N 个 bit)强置一串连续逻辑线圈的通断 | |
10 | 写多个保持寄存器 | 位操作 | 写整形,字符型,状态字,浮点型(写 N 个 word)把具体的二进制值装入一串连续的保持寄存器 |
示例1:写单个寄存器。向01地址设备0x0105保持寄存器写入1个数据:0x0190
主机发送: 01 06 01 05 01 90 99 CB
从机回复: 01 06 01 05 01 90 99 CB
说明:01表示从机地址,06功能码表示写单个保持寄存器,01 05表示寄存器地址,01 90 表示写入寄存器的数值,99 CB为CRC校验值。
可以看出,当写1个寄存器数据时,从机响应的数据帧和主机发送的数据帧完成一致。
附:CRC(循环冗余校验)在线计算地址:http://www.ip33.com/crc.html
CRC-16代码实现
# -*- coding:utf-8 -*-
'''生成 CRC 的过程为:
1.将一个 16 位寄存器装入十六进制 FFFF,将之称作 CRC 寄存器.
2.将报文的第一个8位字节与上述 CRC 寄存器的低字节异或,结果置于 CRC 寄存器.
3.将 CRC 寄存器右移 1 位 (向 LSB(Least Significant Bit,最低有效位) 方向), MSB(Most Significant Bit,最高有效位) 充零。提取并检测 LSB。
4.如果 LSB 为 0,则重复步骤 3 (另一次移位).
如果 LSB 为 1: 对 CRC 寄存器异或多项式值 0xA001 (对应16位二进制:1010 0000 0000 0001)
5.重复步骤 3 和 4,直到完成 8 次移位。当做完此操作后,将完成对 8 位字节的完整操作。
6. 对报文中的下一个字节重复步骤 2 到 5,继续此操作直至所有报文被处理完毕。
7. CRC 寄存器中的最终内容为 CRC 值.
8. 当放置 CRC 值于报文时,需要交换CRC高低字节。
'''
def hex_char_to_int(hex_char):
'''
:param hex_char: 16进制表示的字符
:return: 字节
'''
return "0123456789ABCDEF".find(hex_char)
def hex_string_to_bytes(hex_string):
'''
16进制字符串转为字节数组
:param hex_string: 16进制表示的字符串
:return: 字节数组
'''
hex_string = hex_string.strip()
hex_string_len = len(hex_string)
if not hex_string_len:
return
byte_array = [] # 存放最终结果
hex_string = hex_string.upper()
for i in range(int(hex_string_len/2)):
high_digit = hex_char_to_int(hex_string[2*i]) # 获取高4位的10进制表示
low_digit = hex_char_to_int(hex_string[2*i+1]) # 获取低4位的10进制表示
# high_digit 左移4位,形成字节高4位
byte_array.append(high_digit << 4 | low_digit) # 通过异或运算,得到字节
if hex_string_len % 2 == 1: # 不能整除时,最后一个1个字符的处理
byte_array.append(0x00 | hex_char_to_int(hex_string[hex_string_len-1]))
return byte_array
def bytes_to_hex_string(byte_list):
'''
字节转为16进制字符串
:param byte_list: 字节数组
:return: 字节数组对应的大写16进制字符串表示
'''
hex_string = ""
for i in range(len(byte_list)):
# [2:] 去掉 '0x'
# zfill(2) 1字节需要2位16进制字符来表示,不够时填充0
hex_string += hex(byte_list[i] & 0xFF)[2:].zfill(2)
return hex_string.upper()
def calculate_crc(data):
reg_crc = 0xFFFF
for byte in data:
reg_crc ^= byte
for _ in range(8):
if reg_crc & 0x0001: # 这里直接通过与运算,就可以判断最低有效位是否为0,和步骤3操作等价的
reg_crc = (reg_crc >> 1) ^ 0xA001
else:
reg_crc >>= 1
return reg_crc.to_bytes(2, 'big') # 按大端模式返回代表整数的2字节数据
# 测试
string_data = '01 06 01 05 01 90'
string_data = string_data.replace(' ', '')
byte_list = bytes(string_data, encoding='utf-8') # 注意,这里不能用代码:实现,这样是错误的,获取不到正确结果
byte_list = hex_string_to_bytes(string_data)
crc_value = calculate_crc(byte_list)
print(crc_value, bytes_to_hex_string(crc_value)) # 输出:b'\xcb\x99' CB99
final_crc = ''
for byte in crc_value:
final_crc = str(hex(byte)).lstrip('0x') + ' ' + final_crc
print(final_crc.upper()) # 输出:99 CB
示例2:写多个寄存器。向01地址设备0x0105、0x0106、0x0107地址保持寄存器,写入3个寄存器数据:0x1102, 0x0304, 0x0566
主机发送:01 10 01 05 00 03 06 11 02 03 04 05 66 4A 12
从机回复:01 10 01 05 00 03 91 F5
说明:01从机地址,10功能码表示写多个保持寄存器,01 05表示起始地址,00 03表示写3个寄存器,06表示数据量为6个字节,11 02/03 04/05 66分别表示写入3个寄存器的数值,4A 12 表示CRC校验数值。
示例3:读单个寄存器。读01地址设备0x0105保持寄存器数据。
主机发送:01 03 01 05 00 01 95 F7
从机回复:01 03 02 56 78 87 C6
说明:
03表示读多个寄存器,0105表示起始地址,00 01表示读1个寄存器
02表示2个字节,56 78表示寄存器的数据。
示例4:读多个寄存器。读01地址设备0x0105、0x0106、0x0107地址保持寄存器,共3个寄存器数据。
主机发送:01 03 01 05 00 03 14 36
从机回复:01 03 06 11 22 33 44 55 66 2A 18
说明:
03表示读多个寄存器,01 05表示起始地址,00 03 表示读3个寄存器
06表示6个字节,11 22 33 44 55 66表示寄存器的数据。
例子:向地址为0x01的从设备的0x0405地址,写入数值0x1234,报文如下:
主机发送请求: :01 06 04 05 12 34 AA <CR><LF>
说明:01表示设备地址,06表示写单个保持寄存器。04 05 表示寄存器地址,12 34 表示数据,AA 表示LRC校验值。实际进行校验的数据不包含起始符(:)和结束符(<CR><LF>
)。
附:LRC校验(纵向冗余校验)在线计算地址:http://www.ip33.com/lrc.html
LRC代码实现
string = '01 06 04 05 12 34'
total = 0
for item in string.split(' '):
total += int(item, 16)
result = total % 256
hex_lrc_vale = hex(256 - result)
常用错误码如下表所示。
异常码 | 名称 | 描述 |
---|---|---|
01 (01H) | 非法功能 | 在请求中接收的功能代码不是从设备的一个授权操作。从设备可能处于错误状态,无法处理特定请求。 |
02 (02H) | 非法数据地址 | 从设备接收的数据地址不是从设备的一个授权地址 |
03 (03H) | 非法数据值 | 指定的数据超过范围或者不允许使用。 |
04 (04H) | 从站设备故障 | 从设备未能执行一个请求的操作,因为出现了一个无法修复的错误 |
05 (05H) | 确认 | 确认 从站设备已经接受请求,并且正在处理这个请求,但是需要长持续时间进行这些操作,返回这个响应防止在客户机(或主站)中发生超时错误,客户机(或主机)可以继续发送轮询程序完成报文来确认是否完成处理 |
06 (06H) | 从站设备忙 | 从设备忙于处理另一个命令。主设备必须在从设备空闲后发送请求 |
07 (07H) | 否定确认 | 从站设备无法执行主站设备发送的请求 |
08 (08H) | 存储奇偶性差错 | 从设备在尝试读取扩展存储器的时候从存储器中检测到一个奇偶校验错误 |
10 (0AH) | 不可用的网关路径 | 与网关一起使用,指示网关不能为处理请求分配输入端口值输出端口的内部通信路径。通常意味着网关是错误配置的或过载的 |
11 (0BH) | 网关目标设备响应失败 | 与网关一起使用,指示没有从目标设备中获得响应,通常意味着设备不在网络中 |