提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
一、MODBUS RTU是什么?
MODBUS RTU 是一种工业通信协议,广泛应用于工业自动化领域,用于连接各种电子设备;
为什么要用MODBUS RTU
- MODBUS协议简单、可靠、易于实现的特点
- 标准化的需求:
在工业自动化领域,标准化的通信协议对于提高系统的互操作性、降低开发成本、加快项目进度具有重要意义。MODBUS RTU协议作为一种广泛采用的工业标准,为不同厂商的设备之间的互连互通提供了有力的支持。
二、MODBUS RTU 通信格式
整体格式
- 起始位:消息开始前的静默期(至少3.5个字符时间)。
- 地址域:1字节,表示目标设备的地址(0x01到0xFF)。
- 功能码:1字节,表示要执行的操作(例如0x03表示读取保持寄存器)。
- 数据域:可变长度,包含实际的数据。
- 校验域:2字节,使用CRC(循环冗余校验)。
- 停止位:消息结束后的静默期(至少3.5个字符时间)。
+-------------------+-------------------+-------------------+-------------------+-------------------+
| 起始位 | 地址域 | 功能码 | 数据域 | 校验域 |
| (至少3.5字符时间) | (1字节) | (1字节) | (可变长度) | (2字节) |
+-------------------+-------------------+-------------------+-------------------+-------------------+
| | | | | |
| | 从0x01到0xFF | 例如0x03 | 例如0x0001, 0x0002 | 例如0xA0B1 |
| | | | | |
+-------------------+-------------------+-------------------+-------------------+-------------------+
常用几个指令如 MODBUS调试助手 exe 所示
详细格式
1.起始位:
在消息开始之前,必须有一个至少3.5个字符时间的静默期。这是为了确保接收方能够正确识别消息的开始。
2.地址域:
在MODBUS RTU协议中,地址域用于标识目标设备的地址。地址域占用1个字节,范围从0x00到0xFF。不同的地址范围有不同的用途,特别是对于单播(一对一)和广播(一对多)通信。
注意事项
- 广播消息不接收响应:广播消息不会收到任何响应,因此不适用于需要应答的请求。
- 设备地址唯一性:在同一个网络中,每个从设备的地址必须是唯一的,以避免地址冲突。
- 地址0xFF:虽然0xFF在某些实现中可能被用作特殊用途,但根据MODBUS标准,它不是合法的设备地址
单播地址(Unicast Addresses)
范围:0x01 到 0xFE
用途:用于标识网络中的单个设备。每个设备都有一个唯一的地址,主设备通过这个地址与特定的从设备通信。
示例:
设备地址 0x01:表示地址为1的设备。
设备地址 0x0A:表示地址为10的设备。
设备地址 0xFE:表示地址为254的设备。
示例:单播请求
假设我们要从地址为0x01的设备读取保持寄存器0x0001到0x0002的值。
+-------------------+-------------------+-------------------+-------------------+-------------------+
| 起始位 | 地址域 | 功能码 | 数据域 | 校验域 |
| (至少3.5字符时间) | 0x01 | 0x03 | 0x00 0x01 0x00 0x02 | 0x1C 0x0F |
+-------------------+-------------------+-------------------+-------------------+-------------------+
- 地址域:0x01(目标设备地址)
- 功能码:0x03(读保持寄存器)
- 数据域:0x00 0x01(起始地址0x0001),0x00 0x02(读取2个寄存器)
- 校验域:0x1C 0x0F(CRC校验值)
广播地址(Broadcast Address)
范围:0x00
用途:用于向网络中的所有设备发送广播消息。广播消息不会收到任何响应,因此不适用于需要应答的请求。
示例:
广播地址 0x00:表示向所有设备发送消息。
示例:广播请求
假设我们要向所有设备发送一个复位命令(假设功能码0x06用于复位)。
+-------------------+-------------------+-------------------+-------------------+-------------------+
| 起始位 | 地址域 | 功能码 | 数据域 | 校验域 |
| (至少3.5字符时间) | 0x00 | 0x06 | 0x00 0x00 0x00 0x00 | 0xXX 0XX |
+-------------------+-------------------+-------------------+-------------------+-------------------+
- 地址域:0x00(广播地址)
- 功能码:0x06(假设用于复位)
- 数据域:0x00 0x00(寄存器地址),0x00 0x00(寄存器值)
- 校验域:0xXX 0XX(CRC校验值)
3.功能码:
常用的功能码
- 0x01 - 读线圈状态 (Read Coils)
描述: 从设备读取一组线圈(离散输出)的状态。
数据域:起始地址(2字节)、线圈数量(2字节)。
响应:线圈状态(1位/线圈,按字节对齐)。 - 0x02 - 读离散输入状态 (Read Discrete Inputs)
描述:从设备读取一组离散输入的状态。
数据域:起始地址(2字节)、输入数量(2字节)。
响应:输入状态(1位/输入,按字节对齐)。 - 0x03 - 读保持寄存器 (Read Holding Registers)
描述:从设备读取一组保持寄存器的值。
数据域:起始地址(2字节)、寄存器数量(2字节)。
响应:寄存器值(2字节/寄存器)。 - 0x04 - 读输入寄存器 (Read Input Registers)
描述:从设备读取一组输入寄存器的值。
数据域:起始地址(2字节)、寄存器数量(2字节)。
响应:寄存器值(2字节/寄存器)。 - 0x05 - 写单个线圈 (Write Single Coil)
描述:向设备写入单个线圈的状态。
数据域:线圈地址(2字节)、线圈状态(2字节,0x0000表示OFF,0xFF00表示ON)。
响应:线圈地址(2字节)、线圈状态(2字节)。 - 0x06 - 写单个保持寄存器 (Write Single Register)
描述:向设备写入单个保持寄存器的值。
数据域:寄存器地址(2字节)、寄存器值(2字节)。
响应:寄存器地址(2字节)、寄存器值(2字节)。 - 0x0F - 写多个线圈 (Write Multiple Coils)
描述:向设备写入多个线圈的状态。
数据域:起始地址(2字节)、线圈数量(2字节)、字节数(1字节)、线圈状态(N字节,1位/线圈)。
响应:起始地址(2字节)、线圈数量(2字节)。 - 0x10 - 写多个保持寄存器 (Write Multiple Registers)
描述:向设备写入多个保持寄存器的值。
数据域:起始地址(2字节)、寄存器数量(2字节)、字节数(1字节)、寄存器值(N字节,2字节/寄存器)。
响应:起始地址(2字节)、寄存器数量(2字节)。
其他功能码
除了上述常用的功能码,MODBUS还定义了一些其他功能码,用于更高级的通信和诊断:
9. 0x11 - 报告从设备标识 (Report Slave ID)
描述:请求从设备报告其标识信息。
数据域:无。
响应:设备标识信息。
10. 0x16 - 遮蔽写寄存器 (Mask Write Register)
描述:对指定的寄存器进行位操作。
数据域:寄存器地址(2字节)、AND屏蔽码(2字节)、OR屏蔽码(2字节)。
响应:寄存器地址(2字节)、AND屏蔽码(2字节)、OR屏蔽码(2字节)。
11. 0x17 - 读写多个寄存器 (Read/Write Multiple Registers)
描述:同时读取和写入多个寄存器。
数据域:读起始地址(2字节)、读寄存器数量(2字节)、写起始地址(2字节)、写寄存器数量(2字节)、字节数(1字节)、写寄存器值(N字节)。
响应:读寄存器值(2字节/寄存器)。
错误码
如果从设备无法执行请求的操作,它会返回一个异常响应,其中包含一个错误码。常见的错误码包括:
0x01 - 非法功能码 (Illegal Function)
0x02 - 非法数据地址 (Illegal Data Address)
0x03 - 非法数据值 (Illegal Data Value)
0x04 - 从设备故障 (Slave Device Failure)
0x05 - 从设备忙 (Acknowledge)
0x06 - 从设备内存区满 (Slave Device Busy)
0x07 - 负载设备故障 (Negative Acknowledge)
0x08 - 存储区检验失败 (Memory Parity Error)
0x0A - 闸道路径不可用 (Gateway Path Unavailable)
0x0B - 闸道目标设备未响应 (Gateway Target Device Failed to Respond)
4.数据域:
可变长度,包含实际的数据。数据的具体格式和长度取决于功能码。例如,读保持寄存器的功能码0x03可能包含起始地址和寄存器数量。
5.校验域:
- 提取数据部分:从接收到的报文中提取地址域、功能码和数据域,形成一个字节数组。
- 计算CRC:使用相同的CRC-16算法计算该字节数组的校验值。
- 比较校验值:将计算得到的CRC校验值与报文中的校验域进行比较。如果两者一致,则认为数据传输无误;否则,认为数据传输有误。
计算步骤
1.提取数据部分
假设接收到的报文如下:
+-------------------+-------------------+-------------------+-------------------+-------------------+
| 起始位 | 地址域 | 功能码 | 数据域 | 校验域 |
| (至少3.5字符时间) | 0x01 | 0x03 | 0x00 0x01 0x00 0x02 | 0x0F 0x1C |
+-------------------+-------------------+-------------------+-------------------+-------------------+
提取的数据部分为:
uint8_t received_data[] = {0x01, 0x03, 0x00, 0x01, 0x00, 0x02};
2.计算CRC
使用CRC-16算法计算提取的数据部分的校验值。
#include <stdint.h>
uint16_t modbus_crc16(const uint8_t *data, size_t length)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; ++i)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; ++j)
{
if (crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
3. 比较校验值
void VerifyModbusCRC( uint8_t *received_data, uint8_t length)
{
// 计算CRC
uint16_t calculated_crc = modbus_crc16(received_data, length - 2);
// 提取报文中的CRC
uint16_t received_crc = (received_data[length - 2] << 8) | received_data[length - 1];
// 比较CRC
if (calculated_crc == received_crc)
{
// CRC校验通过
printf("CRC check passed.\n");
}
else
{
// CRC校验失败
printf("CRC check failed. Calculated: 0x%04X, Received: 0x%04X\n", calculated_crc, received_crc);
}
}
注意事项
校验域的顺序:MODBUS RTU协议规定CRC校验值的低字节在前,高字节在后。
CRC初始值:MODBUS RTU使用的CRC-16算法的初始值为0xFFFF。
多项式:MODBUS RTU使用的CRC-16多项式为0xA001。
校验算法
6.停止位:
在消息结束之后,必须有一个至少3.5个字符时间的静默期。这是为了确保接收方能够正确识别消息的结束。