一、初识Modbus
Modbus 是应用层的协议,需要借助其他协议,如物理层协议UART、RS485等协议传输,或者传输层TCPIP协议传输。
协议分支有:
Modbus RTU (最根本的协议)Modbus ASCII 、Modbus TCP/IP 、Modbus over TCP/IP 、Modbus UDP
常用接口(物理层):
-
RS232通讯
全双工,这个基本用在点对点通信场景下,不适合多点拓扑连接,共模电平编码,一般需要Rxd/Txd/Gnd三根线连接。 三/四线
-
RS485通讯
半双工,这是最为常用的modbus物理层,差分电平编码,一对双绞线,抗干扰性能也不错 两线
-
RS422通讯
全双工,这种物理层也有比较多的应用,差分电平编码,两对双绞线,抗干扰性能也不错。与RS-485相比,其优势在于可以实现全双工,通信的效率高些,所需要的代价就是现场布线需要两对双绞线,增加了一定的成本。 四线
-
以太网通讯(越来越多)
通用、便携、兼容
-
其他:串口、无线、蓝牙
二、初识寄存器
简单介绍:Modbus的寄存器存储区概念来自于PLC。线圈代表单片机的可位寻址操作,寄存器则不可位寻址。
四种寄存器:线圈状态寄存器(0 Coil Status)、离散线圈输入状态寄存器(1 Input Status)、保持寄存器(4 Holding Register)、输入寄存器(3 Input Register)。
其中数字(0、1、4、3)是代表存储区编号,注意没 2,与协议没关系,但是能由编号得出该寄存器的十进制基地址。操作不同的存储区需要用不同的功能码。
1、线圈状态寄存器
2、离散线圈输入状态寄存器
3、保持寄存器
4、输入寄存器
三、PLC的地址
寄存器 | 访问长度 | R/W | 寄存器编号 | 十进制地址范围 | 协议地址范围 | 可操作功能码 | 说明 | |
---|---|---|---|---|---|---|---|---|
线圈状态寄存器(Coil Status) | 位bit | RW | 0 | 00001-09999 | 0-FFFF | 01H、05H、0FH | ||
离散线圈输入状态寄存器(Input Status) | 位bit | R | 1 | 10001-19999 | 0-FFFF | 02H | ||
保持寄存器(Holding Register) | 字word(2Byte) | RW | 4 | 40001-49999 | 0-FFFF | 03H、06H、10H | ||
输入寄存器(Input Register) | 字word(2Byte) | R | 3 | 30001-39999 | 0-FFFF | 04H |
四、Modbus协议
一个完整的数据帧包括:地址 + 功能码 + 数据 + CRC校验
一个数据包最大256个字节。读最多个操作和写最多个操作都可能满
读操作规律:
读操作都发送 2Byte 地址 + 2Byte 数量(数量代表:线圈个数或寄存器个数)。
读操作都返回 1Byte 字节计数 + 计数数量的N个字节(N字节代表:线圈返回字节数 = 读线圈数/8 向上取整
, 寄存器返回字节数 = 读取寄存器数*2
)
写操作规律:
所有的写单个数据包,返回数据包与请求数据包相同。
写多个需要
1、地址
十进制范围 | 十六进制范围 | 作用 |
---|---|---|
0 | 0 | 广播地址,所有的从设备必须处理广播报文,但是不用回应 |
1-247 | 0x01 - 0xF7 | 从设备地址,主设备是没有地址的,这一点需要注意 |
248-255 | 0xF8 - 0xFF | 预留地址 |
2、功能码
范围:0x00 - 0x7F (1-127)
地址范围 | 作用 |
---|---|
111-127 | 公共功能码 |
100-110 | 用户定义功能码 |
73-99 | 公共功能码 |
64-72 | 用户定义功能码 |
1-64 | 公共功能码 |
请求数据包发送功能码,回应数据包发送响应码。正常回应的回应码等于功能码,异常回应的异常功能码为功能码+0x80
。
异常响应时,在功能码后加异常码。
常见功能码 Modbus功能码列表(全)
功能码 | 解释 | 作用 |
---|---|---|
0x01 | Read Coils 读线圈状态 | 读取远程设备中1到2000个连续的线圈的状态 |
0x02 | Read Discrete Inputs 读离散输入状态 | 读取远程设备中1到2000个连续的离散输入的状态 |
0x03 | Read Holding Registers 读保持寄存器内容 | 读取远程设备中1到125个连续的保持寄存器的内容 |
0x04 | Read Input Registers 读输入寄存器内容 | 读取远程设备中1到125个连续的输入寄存器的内容 |
0x05 | Write Single Coil 写单个线圈 | 在远程设备中把单个线圈状态改变为打开或关闭的状态 |
0x06 | Write Single Register 写单个保持寄存器 | 在远程设备中写入单个保持寄存器 |
0x07 | Read Exception Status (Serial Line only) 读取异常状态(仅限串行线路) | 读取远程设备中八个异常状态输出的内容 |
0x08 | Diagnostics (Serial Line only) 通信系统诊断(仅限串行线路) | |
0x0B | Get Comm Event Counter (Serial Line only) 获取通讯事件计数器(仅限串行线路) | 从远程设备的通信事件计数器获取状态字和事件计数 |
0x0C | Get Comm Event Log (Serial Line only) 获取通讯事件日志(仅限串行线路) | 从远程设备获取状态字、事件计数、消息计数和事件字节字段 |
0x0F | Write Multiple Coils 写多个线圈 | 强制远程设备中线圈序列中的每个线圈接通或断开 1-1968 个 |
0x10 | Write Multiple registers 写多个保持寄存器 | 在远程设备中写入连续寄存器块 1-123个 |
0x11 | Report Slave ID (Serial Line only) 报导从机信息(仅限串行线路) | 读取远程设备特有的类型、当前状态和其他信息的说明。数据内容特定于每种类型的设备 |
0x14 | Read File Record 读取文件记录 | |
0x15 | Write File Record 写文件记录 | |
0x16 | Mask Write Register 带屏蔽字写入寄存器 | |
0x17 | Read/Write Multiple registers 读、写多个寄存器 | 执行一次连续写和连续读,写入操作在读取之前执行 |
0x18 | Read FIFO Queue 读取先进先出队列 | |
0x2B | Encapsulated Interface Transport 封装接口传输 |
异常功能码
异常功能码 = 普通功能码 + 0x80
如:读保持寄存器异常,从机返回主机数据帧 Rx: 01 83 01 80 F0
3、数据
数据可能的几种格式
1、线圈状态寄存器
2、离散线圈输入状态寄存器
3、保持寄存器
4、输入寄存器
异常码
代码 HEX | 名称 | 含义 |
---|---|---|
01 | 非法功能 | 对于服务器(或从站)来说,询问中接收到的功能码是不可允许的操作。这也许是因为功能码仅仅适用于新设备而在被选单元中是不可实现的。同时,还指出服务器(或从站)在错误状态中处理这种请求,例如:因为它是未配置的,并且要求返回寄存器值。 |
02 | 非法数据地址 | 对于服务器(或从站)来说,询问中接收到的数据地址是不可允许的地址。特别是,参考号和传输长度的组合是无效的。对于带有 100 个寄存器的控制器来说,带有偏移量 96 和长度 4 的请求会成功,带有偏移量 96 和长度 5 的请求将产生异常码 02。 |
03 | 非法数据值 | 对于服务器(或从站)来说,询问中包括的值是不可允许的值。这个值指示了组合请求剩余结构中的故障,例如:隐含长度是不正确的。并不意味着,因为MODBUS 协议不知道任何特殊寄存器的任何特殊值的重要意义,寄存器中被提交存储的数据项有一个应用程序期望之外的值。 |
04 | 从站设备故障 | 当服务器(或从站)正在设法执行请求的操作时,产生不可重新获得的差错。 |
05 | 确认 | 与编程命令一起使用。服务器(或从站)已经接受请求,并且正在处理这个请求,但是需要长的持续时间进行这些操作。返回这个响应防止在客户机(或主站)中发生超时错误。客户机(或主站)可以继续发送轮询程序完成报文来确定是否完成处理。 |
06 | 从属设备忙 | 与编程命令一起使用。服务器(或从站)正在处理长持续时间的程序命令。当服务器(或从站)空闲时,用户(或主站)应该稍后重新传输报文。 |
0A | 不可用网关路径 | 与网关一起使用,指示网关不能为处理请求分配输入端口至输出端口的内部通信路径。通常意味着网关是错误配置的或过载的。 |
0B | 网关目标设备响应失败 | 与网关一起使用,指示没有从目标设备中获得响应。通常意味着设备未在网络中。 |
4、CRC
CRC 计算是包括字节:1Byte 地址、有效载荷(1Byte 功能码+ NByte 数据)的所有字节,即CRC前的所有数据。
1、多项式 0X8005
2、初始异或值 0xFFFF
3、数据反转:输入反转 要,输出反转 要
4、Modbus 是低地址字节先发,比如单片机计算结果是uint_16 0xCDC5,打包发送时是C5 CD
例子:01 03 00 00 00 0A C5 CD
拓展:
按照以上的计算,网页输出是 CDC5,因为网页输出的是uint_16 是大端在前,小端在后。
若是YModem协议,数据包结尾就是直接 MSB的CRC16 格式,但是MODEMBUS 是 LSB 的格式,所以是C5CD,YModem 的CRC只包括有效数据段,不包括前面1B功能码2B数据包计数。
CRC 计算代码
//modbus 可直接使用的
uint16_t calculate_crc(uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF; // CRC 初始值
for (int i = 0; i < len; i++) {
crc ^= (uint16_t)data[i]; // 与数据异或
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001; // 生成多项式 0xA001
} else {
crc >>= 1;
}
}
}
return crc;
}
5、 数据包实例
最常用的8种,其中 4个读取四种寄存器 + 2个写单个寄存器 + 2个写多个寄存器。未添加异常数据包。
1、读线圈状态 01
1、主机发送 Tx: 01 01 00 00 00 06 BC 08
空 | 地址 | 功能码 | 线圈地址 | 数量(线圈) | CRC |
---|---|---|---|---|---|
数据包实例 | 01 | 01 | 00 00 | 00 06 | BC 08 |
可选范围HEX | 01-F7 | 略 | 0-9999/65535 | 1 - 07D0(1-2000) | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 读取单个线圈 | 从地址0x0000 | 读取6个线圈 | 空 |
2、从机回应 Rx: 01 01 01 32 D0 5D
空 | 地址 | 功能码 | 字节数 | 数据(字节中一位代表一个线圈) | CRC |
---|---|---|---|---|---|
数据包实例 | 01 | 01 | 01 | 32 | D0 5D |
读取范围HEX | 略 | 略 | 1-FA(1-250) | 空 | 空 |
所占字节 | 1Byte | 1Byte | 1Byte | 1-250 | 2Byte |
解释 | 从机地址01 | 读取单个线圈 | 数据字节数 | 高地址-低地址:0011 0010 | 空 |
读取的六个地址是从低地址0x0000 开始读取的,读到0x0006。没读的高位补 0 凑一个字节返回。
2、读离散(输入)线圈输入状态 02
1、主机发送 Tx: 01 02 00 00 00 06 F8 08
空 | 地址 | 功能码 | 地址 | 数量(线圈) | CRC |
---|---|---|---|---|---|
数据包实例 | 01 | 02 | 00 00 | 00 06 | F8 08 |
可选范围HEX | 略 | 略 | 略 | 同线圈状态 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 读取离散(输入)线圈 | 从地址0x0000 | 读取6个线圈 | 空 |
2、从机回应 Rx: 01 02 01 24 A1 93
地址 | 功能码 | 字节数 | 数据(字节中一位代表一个线圈) | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 02 | 01 | 24 | A1 93 |
可选范围HEX | 略 | 略 | 1-FA(1-250) | 空 | 空 |
所占字节 | 1Byte | 1Byte | 1Byte | 1-250(读取线圈数/8向上取整) | 2Byte |
解释 | 从机地址01 | 正常读取 | 数据字节数 | 高地址-低地址:0010 0100 | 空 |
3、读保持寄存器 03
1、主机发送 Tx: 01 03 00 00 00 06 C5 C8
地址 | 功能码 | 地址 | 数量(取寄存器) | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 03 | 00 00 | 00 06 | C5 08 |
可选范围HEX | 略 | 略 | 略 | 1-007D(1-125) | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 读保持寄存器 | 从地址0x0000 | 读取6个寄存器 | 空 |
2、从机回应 Rx: 01 03 0C 00 46 00 50 00 5A 00 00 00 00 00 00 DE 3F
地址 | 功能码 | 数据字节计数 | 数据 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 03 | 0C | 00 46 00 50 00 5A 00 00 00 00 00 00 | BC 08 |
可选范围HEX | 略 | 略 | 1-FA(1-250) | 空 | 空 |
所占字节 | 1Byte | 1Byte | 1Byte | 1-250(?6*2 = 12Byte) | 2Byte |
解释 | 从机地址01 | 正常读取 | 后面有12个Byte的数据 | 十进制:70 80 90 00 00 00 | 空 |
4、读输出寄存器 04
1、主机发送 Tx: 01 04 00 00 00 05 30 09
地址 | 功能码 | 地址 | 读取数量 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 04 | 00 00 | 00 05 | 30 09 |
可选范围HEX | 略 | 略 | 略 | 1-007D(1-125) | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 读取输出寄存器 | 从地址0x0000 | 读取5个寄存器 | 空 |
2、从机回应Rx: 01 04 0A AB CD 12 34 23 45 00 00 00 02 10 C4
地址 | 功能码 | 字节计数 | 数据 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 04 | 0A | AB CD 12 34 23 45 00 00 00 02 | 10 C4 |
可选范围HEX | 略 | 略 | 1-FA(1-250) | 空 | 空 |
所占字节 | 1Byte | 1Byte | 1Byte | 1-250 ?5*2 = 10Byte | 2Byte |
解释 | 从机地址01 | 正常读取 | 后面有10个Byte的数据 | 0xABCD、0x1234、0x2345、 0x0000、0x0002 | 空 |
5、写单个线圈状态 05
1、主机发送 Tx: 01 05 00 00 FF 00 8C 3A
地址 | 功能码 | 地址 | 值 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 05 | 00 00 | FF 00 | 8C 3A |
可选范围HEX | 略 | 略 | 略 | 真FF00 假0000 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 写取单个线圈 | 从地址0x0000 | 对某一位置一 | 空 |
2、从机回应 Rx: 01 05 00 00 FF 00 8C 3A 同请求
地址 | 功能码 | 地址 | 值 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 05 | 00 00 | FF 00 | 8C 3A |
可选范围HEX | 略 | 略 | 略 | 真FF00 假0000 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 正常写入 | 从地址0x0000 | 置一成功 | 空 |
6、写单个保持寄存器 06
1、主机发送 Tx: 01 06 00 00 04 D2 0B 57
地址 | 功能码 | 地址 | 值 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 06 | 00 00 | 04 D2 | 0B 57 |
可选范围HEX | 略 | 略 | 略 | 空 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 写单个保持寄存器 | 从地址0x0000 | 写1个线圈 | 空 |
2、从机回应 Rx: 01 06 00 00 04 D2 0B 57 同请求
地址 | 功能码 | 地址 | 值 | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 06 | 00 00 | 04 D2 | 0B 57 |
可选范围HEX | 略 | 略 | 略 | 空 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 正常写入 | 从地址0x0000 | 读得十进制1234 | 空 |
7、写多个线圈 15
1、主机发送 Tx: 01 0F 00 00 00 0A 02 07 00 E7 08
地址 | 功能码 | 线圈地址 | 写数量(线圈) | 数据计数 | 数据 | CRC | |
---|---|---|---|---|---|---|---|
数据包实例 | 01 | 0F | 00 00 | 00 0A | 02 | 07 00 | E7 08 |
可选范围HEX | 略 | 略 | 略 | 1-07B0(1-1968) | 1-F6 | 空 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 1Byte | (1-246) | 2Byte |
解释 | 从机地址01 | 写多个线圈 | 从地址0x0000 | 请求写10个线圈 | 数据计数 | 0000000111 | 空 |
2、从机回应 Rx: 01 0F 00 00 00 0A D5 CC
地址 | 功能码 | 线圈地址 | 写数量(线圈) | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 0F | 00 00 | 00 0A | D5 CC |
可选范围HEX | 略 | 略 | 略 | 1-07B0(1-1968) | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 正常写入 | 从地址0x0000 | 写10个线圈成功 | 空 |
8、写多个保持寄存器 16
1、主机发送 Tx: 01 10 00 00 00 0A 14 12 34 09 29 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1A 85 A3 7E
地址 | 功能码 | 线圈地址 | 写数量(寄存器) | 数据计数 | 数据 | CRC | |
---|---|---|---|---|---|---|---|
数据包实例 | 01 | 10 | 00 00 | 00 0A | 14 | – | A3 7E |
可选范围HEX | 略 | 略 | 略 | 1-007B(1-123) | 1-F6 | 空 | 空 |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 1Byte | 1-246 | 2Byte |
解释 | 从机地址01 | 写多个保持寄存器 | 从地址0x0000 | ? | – | 空 |
2、从机回应 Rx: 01 10 00 00 00 0A 40 0E
地址 | 功能码 | 线圈地址 | 写数量(寄存器) | CRC | |
---|---|---|---|---|---|
数据包实例 | 01 | 10 | 00 00 | 00 0A | 40 0E |
可选范围HEX | 略 | 略 | 略 | 空 | |
所占字节 | 1Byte | 1Byte | 2Byte | 2Byte | 2Byte |
解释 | 从机地址01 | 正常写入 | 从地址0x0000 | ? | 空 |
REF:
Modbus通讯协议从一窍不通到原来如此_modbus从站主动上报-CSDN博客ModbusRTU的几种常用功能码介绍及使用_modbus功能码-CSDN博客