Modbus协议讲解

前言

这里讲解的Modbus协议主要是用于BMS和PCS之间,有涉及一部分CAN协议,CAN部分讲得比较浅。主要讲解ModbusRTU、ASCII和TCP之间的联系及区别,以及如何相互转换,还有一些小例子方便理解!


什么是工业通信协议?

简单说就是机器之间的“语言”。

就像人说话要有共同语言(中文、英文、日语、德语……)一样,BMS、逆变器、PLC、传感器、电机之间要交换数据,也得有统一的规则,这就是协议。

Modbus协议

一句话说清Modbus

主从问答式的通信方式。一问一答(主站问,从站答)

三种常见形式

  • Modbus RTU:用串口(RS232/RS485),最常用
  • Modbus ASCII:也用串口,但效率低,现在用得少
  • Modbus TCP:用网线,跑在以太网上

ASCII(1979,文本兼容) → RTU(1980s,效率优化) → TCP(1999,网络化)

标准Modbus RTU协议

数据发送
  • 主站(上位机/逆变器)以问答方式与从站(BMS)通讯,每帧报文的长度不超过255个字节。
  • 如果从站(BMS)收到的主站(上位机/逆变器)报文的地址、报文类型、数据和校验码都正确,则在500ms内以正常报文响应上位机。
  • 如果从站(BMS)收到的主站(上位机/逆变器)报文的地址或校验码不正确,则不回答或回答异常报文。主站(上位机/逆变器)判断超时后可继续后续的通讯。
  • 如果从站(BMS)收到的主站(上位机/逆变器)报文的地址和校验码正确,但报文类型或数据内容不正确,则应在500ms 内以异常报文回应上位机。
  • 96,N,8,1(波特率9600,无校验,8 位数据位,1 位起始位,1 位停止位)
  • 常用波特率:9600、19200、38400、57600、115200
数据格式
地址 功能码 数据区 CRC校验
1个字节 1个字节 N个字节 2 个字节(16 位循环冗余校验码)
地址

单台从机地址默认为“1”;

Modbus RTU 帧头只能是 1 字节的从站地址(0x00-0xF7)

功能码

功能码是每次通讯报文的第二个字节,功能码如下表:

功能码 定义
0x01 读取线圈状态(读取开关量)
0x02 读取输入状态
0x03 读取保持寄存器(读取模拟量)
0x05 写入单个线圈(控制开关)
0x06 写入单个寄存器
0x10 写入多个寄存器

其中0x03、0x06、0x10比较常见!

数据区

数据区的内容以 Big Endian 形式储存,通讯时先发高位字节,后发低位字节。 数据区的内容根据不同的功能码有不同的规定。

功能码详细说明:功能码 03:读寄存器

每个寄存器都是两个字节 (16 位二进制数据),高位字节在前,低位字节在后。每个寄存器表示的数据范围为-32768 到 32767,负数用补码 (two’s complement) 表示。 寄存器的地址编码,可以理解为地址为 0 的寄存器在数据区的第 1 个和第 2 个字节,地址为1 的寄存器在数据区的第 3 个和第 4 个字节,地址为 2 的寄存器在数据区的第 5 个和第 6 个 字节……

上位机发送的报文格式:
报文内容 字节数 说明
地址 1个字节 读取第几个机子内容
功能码 1个字节 0x03读取寄存器
起始寄存器地址 2个字节 从哪个地址的寄存器开始读取数据
寄存器个数 2个字节 读取几个寄存器的数据 (字节数=寄存器个数×2)
CRC校验 2个字节 地址、功能码、起始地址、寄存器个数的 CRC 校验码
下位机回复数据的报文格式:
报文内容 字节数 说明
地址 1个字节 读取的第几个机子的内容
功能码 1个字节 0x03读取寄存器
数据字节数 1个字节 数据字节数 N=寄存器个数×2
寄存器数据 N个字节 寄存器个数=数据字节数÷2 返回的第一个字节和第二个字节是第一个 (起始地址) 的寄存器 数据;返回的第三个字节和第四个字节是第二个 (起始地址+1) 的寄存器数据
CRC校验 2个字节 地址、功能码、数据字节数、寄存器数据的 CRC 校验码
报文示例:

假设下位机的地址为 1,寄存器的数据如下:

地址 0x2800 0x2801 0x2802 0x2803 0x2804 0x2805 0x2806 0x2807
数据 3336 3336 3336 3336 3333 3335 3334 3337
地址 0x2808 0x2809 0x280A 0x280B 0x280C 0x280D 0x280E 0x280F
数据 3335 3336 3337 3337 3336 3336 3336 3335
读取单个寄存器:

查询地址0x2800 寄存器的数据:

1
2
[11:22:44.368]发→◇01 03 28 00 00 01 8D AA □
[11:22:44.401]收←◆01 03 02 0D 08 BD 12

上位机发送数据:01 03 28 00 00 01 8D AA

报文(0x) 字节数 内容
01 1个字节 第一个机子
03 1个字节 功能码 03 读取寄存器
28 00 2个字节 起始地址:2800,先发高位字节28,后发低位字节00
00 01 2个字节 读取1个寄存器的数据,先发高位字节00,后发低位字节01
8D AA 2个字节 01 03 28 00 00 01的CRC校验

下位机回应:01 03 02 0D 08 BD 12

报文(0x) 字节数 内容
01 1个字节 第一个机子
03 1个字节 功能码 03 读取寄存器
02 1个字节 接下来有2个字节,即1个寄存器的数据
0D 08 2个字节 读取单个寄存器的数据0x 0D08,转为十进制3336
BD 12 2个字节 01 03 02 0D 08的CRC校验
读取多个寄存器:

查询地址从 0x2800到 0x280F 的 16个寄存器的数据:

1
2
[11:22:48.188]发→◇01 03 28 00 00 10 4D A6 □
[11:22:48.225]收←◆01 03 20 0D 08 0D 08 0D 08 0D 08 0D 05 0D 07 0D 06 0D 09 0D 07 0D 08 0D 09 0D 09 0D 08 0D 08 0D 08 0D 07 FA 32

上位机发送数据:01 03 28 00 00 10 4D A6

报文(0x) 字节数 内容
01 1个字节 第一个机子
03 1个字节 功能码 03 读取寄存器
28 00 2个字节 起始地址:2800,先发高位字节28,后发低位字节00
00 10 2个字节 读取16个寄存器的数据,先发高位字节00,后发低位字节10
4D A6 2个字节 01 03 28 00 00 10的CRC校验

下位机回应:01 03 20 0D 08 0D 08 0D 08 0D 08 0D 05 0D 07 0D 06 0D 09 0D 07 0D 08 0D 09 0D 09 0D 08 0D 08 0D 08 0D 07 FA 32

报文(0x) 字节数 内容
01 1个字节 第一个机子
03 1个字节 功能码 03 读取寄存器
20 1个字节 接下来有0x20(36)个字节,即16个寄存器的数据
0D 08 0D 08 0D 08 0D 08 0D 05 0D 07 0D 06 0D 09 0D 07 0D 08 0D 09 0D 09 0D 08 0D 08 0D 08 0D 07 2个字节 返回16个寄存器的数据,从地址0x2800开始的后16个寄存器数据,依次为0D08(3336),0D08(3336),0D08(3336),0D08(3336),0D05(3333),0D07(3335),0D06(3332),0D 09(3337), 0D 07(3335),0D08(3336), 0D 09(3337),0D 09(3337),0D08(3336), 0D08(3336),0D08(3336),0D 07(3335)
FA 32 2个字节 01 03 02 0D 08的CRC校验
功能码详细说明:功能码 06:写单个寄存器
上位机发送的报文格式:
报文内容 字节数 说明
地址 1个字节 对第几个机子内容进行写入
功能码 1个字节 0x06写入单个寄存器
待写入寄存器地址 2个字节 需要对哪个寄存器进行写入操作
写入值 2个字节 需要写入的数值
CRC校验 2个字节 地址、功能码、寄存器地址、写入值的 CRC 校验码
下位机回复数据的报文格式:

与发送内容一致,否则代表写入失败。

报文内容 字节数 说明
地址 1个字节 对第几个机子内容进行写入
功能码 1个字节 0x06写入单个寄存器
待写入寄存器地址 2个字节 需要对哪个寄存器进行写入操作
写入值 2个字节 需要写入的数值
CRC校验 2个字节 地址、功能码、寄存器地址、写入值的 CRC 校验码
报文示例:
1
2
[14:55:05.373] [发送]: 01 06 20 E7 03 CF 72 99
[14:55:05.454] [接收]: 01 06 20 E7 03 CF 72 99

上面报文为写入BMS SOC值(97.5%),这协议精度为0.1,偏移为0:

报文(0x) 字节数 内容
01 1个字节 第一个机子
06 1个字节 功能码0x06写入单个寄存器
20 E7 2个字节 寄存器地址:0x20E7,先发高位字节20,后发低位字节E7
03 CF 2个字节 写入97.5%(975的16进制0x03CF),先发高位字节03,后发低位字节CF
72 99 2个字节 01 06 20 E7 03 CF的CRC校验
功能码详细说明:功能码 10:写多个寄存器
上位机发送的报文格式:
报文内容 字节数 说明
地址 1个字节 写入第几个机子内容
功能码 1个字节 0x10写多个寄存器
起始寄存器地址 2个字节 从哪个地址的寄存器开始写入
寄存器个数 2个字节 对后面几个寄存器进行写入
数据字节数 1个字节 写入寄存器的数据的字节数,即接下来的报文的字节数,数据字节数 N=寄存器个数×2
写入的数据 N个字节 写入的第一个字节和第二个字节是第一个 (起始地址) 的寄存器数据;写入的第三个字节和第四个字节是第二个 (起始地址+1) 的寄存 器数据
CRC校验 2个字节 地址、功能码、起始地址、寄存器个数、字节数、数据的 CRC
下位机回复数据的报文格式:
报文内容 字节数 说明
地址 1个字节 对第几个机子内容进行写入
功能码 1个字节 0x10写入多个寄存器
起始地址 2个字节 从哪个地址的寄存器开始写入
寄存器个数 2个字节 对几个寄存器进行写入
CRC校验 2个字节 地址、功能码、起始地址、寄存器个数的 CRC 校验码
报文示例:

写入SN号:BLD15105NB470379

ASCII在线转16进制网站:https://ltkdriver.freedash.top/Tool/ASCIIAndHexConvert

将ASCII码转为16进制(待写入):42 4C 44 31 35 31 30 35 4E 42 34 37 30 33 37 39

1
2
3
4
5
6
7
写入:
[15:45:02.002]发→◇01 10 21 91 00 08 10 42 4C 44 31 35 31 30 35 4E 42 34 37 30 33 37 39 D6 48 □
[15:45:02.056]收←◆01 10 21 91 00 08 1E 9A

读取:
[15:45:13.626]发→◇01 03 21 91 00 08 1F DD □
[15:45:13.672]收←◆01 03 10 42 4C 44 31 35 31 30 35 4E 42 34 37 30 33 37 39 FC F3
报文(0x) 字节数 内容
01 1个字节 第一个机子
10 1个字节 功能码 10 写入多个寄存器
21 91 2个字节 起始地址:0x2191,先发高位字节21,后发低位字节91
00 08 2个字节 写入8个寄存器的数据
10 1个字节 数据的字节数:0x10(16)个字节,包括8个寄存器的数据
42 4C 44 31 35 31 30 35 4E 42 34 37 30 33 37 39 N个字节 数据:BLD15105NB470379ASCII码的16进制
72 3E 2个字节 除了后两个byte的CRC校验

下位机回应:01 10 21 91 00 08 1E 9A

报文(0x) 字节数 内容
01 1个字节 第一个机子
10 1个字节 功能码0x10写多个寄存器
21 91 1个字节 起始寄存器地址
00 08 2个字节 写入8个寄存器的数据
1E 9A 2个字节 01 10 21 91 00 08的CRC校验

CRC16 计算方法

  1. 预置 1 个 16 位的寄存器为十六进制的 FFFF(即全为 1);称此寄存器为 CRC 寄存器。
  2. 把第一个 8 位二进制数据(即通讯信息帧的第一个字节)与 16 位的 CRC 寄存器的低 8位相异或,把结果存放在 CRC 寄存器。
  3. 把 CRC 寄存器的内容右移一位(朝低位)用 0 填补最高位,并检查右移后的移出位。
  4. 如果移出位为 0:重复第 3 步(再次右移 1 位);如果移出位为 1:CRC 寄存器与多项 式 A001(1010 0000 0000 0001)进行异或。
  5. 重复步骤 3 和 4,直到右移 8 次,这样整个 8 位数据全部进行了处理。
  6. 重复步骤 2 到步骤 5,进行通讯信息帧下一个字节的处理。
  7. 将通讯信息帧的所有字节按上述步骤计算完成后,得到 16 位 CRC 寄存器的高,低字节交换。
  8. 最后得到的 CRC 寄存器内容即为:CRC 码。
C语言CRC运算源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned short ModBusCRC16(const void *s, int n)
{
unsigned short crc = 0xFFFF;
const unsigned char *data = (const unsigned char *)s;

for (int i = 0; i < n; i++) {
crc ^= data[i]; // 与当前字节异或

for (int j = 0; j < 8; j++) {
if (crc & 0x0001) { // 检查最低位是否为1
crc = (crc >> 1) ^ 0xA001; // 右移并与多项式异或
} else {
crc = crc >> 1; // 只是右移
}
}
}

return (crc << 8) | (crc >> 8); // 高低字节交换
}
C#CRC运算源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static ushort ModBusCRC16(byte[] pucFrame, int usLen)   //计算CRC校验
{
const ushort POLYNOMIAL = 0xA001; // CRC-16/MODBUS多项式(0x8005的反向表示)
ushort crc = 0xFFFF; // 初始值

for (int i = 0; i < usLen; i++)
{
crc ^= pucFrame[i]; // 异或当前字节

for (int j = 0; j < 8; j++)
{
bool lsb = (crc & 0x0001) != 0; // 检查最低位
crc >>= 1;

if (lsb)
{
crc ^= POLYNOMIAL; // 如果最低位是1,异或多项式
}
}
}
return crc;
}
ModBusCRC16在线计算网站:

https://ltkdriver.freedash.top/Tool/ModbusRTUCRC16

非Modbus RTU协议

跟Modbus协议完全不一样,完全由自己规定。

自定义升级协议:
1
2
3
4
5
[13:57:41.464] [发送]: D0 01 A0 00 C4 00 00 00 00 30 BA 00 20 C1 29 02 08 1F 31 02 08 21 31 02 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 25 31 02 08 00 00 00 00 00 00 00 00 27 31 02 08 29 31 02 08 CB 31 02 08 ED 29 02 08 9B 32 02 08 EF 29 02 08 F1 29 02 08 83 32 02 08 8B 32 02 08 93 32 02 08 F3 29 02 08 7B 32 02 08 F5 29 02 08 F7 29 02 08 F9 29 02 08 EB 31 02 08 FB 29 02 08 F5 31 02 08 FF 31 02 08 FD 29 02 08 FF 29 02 08 85 52 02 08 01 2A 02 08 CD 31 02 08 E9 31 02 08 03 2A 02 08 05 2A 02 08 07 2A 02 08 09 2A 02 08 5D 32 02 08 67 32 02 08 71 32 02 08 0B 2A 02 08 00 00 64 00 72 EC
[13:57:41.588] [接收]: D0 FF FF FF 05 05 C0 00 00 00 04 81

[13:57:41.709] [发送]: D0 01 A0 00 C4 C0 00 00 00 20 03 84 03 D4 03 E8 03 F6 FF 00 00 32 00 64 00 96 00 C8 00 FA 00 C2 01 F4 01 26 02 30 02 00 00 00 00 4E 00 AB 00 AB 00 A0 01 A0 01 08 02 04 01 AB 00 00 00 00 00 4E 00 AB 00 AB 00 A0 01 A0 01 08 02 04 01 AB 00 00 00 00 00 4E 00 AB 00 AB 00 A0 01 A0 01 08 02 04 01 AB 00 00 00 00 00 4E 00 AB 00 AB 00 A0 01 A0 01 08 02 04 01 AB 00 00 00 00 00 4E 00 AB 00 AB 00 A0 01 A0 01 08 02 04 01 AB 00 00 00 00 00 32 00 64 00 C8 00 2C 01 90 01 E8 03 00 00 CA FE D4 FE 38 FF 6A FF 9C FF CE FF 00 00 32 00 90 01 F4 01 26 02 30 02 00 00 00 00 68 00 68 00 A0 01 A0 01 A0 01 D8 02 6C 01 9C 00 65 9D
[13:57:41.835] [接收]: D0 FF FF FF 05 05 80 01 00 00 04 C5
升级协议解析:
上位机报文发送:
报文(0x) 字节数 内容
D0 1个字节 协议版本号
01 A0
(A001)
(1010000000000001)
(bit15、bit0、bit13)
2个字节 目标设备 ID(小端模式)
当 bit15 广播标识为 0 时:
1. Bit0bit6 从控层级 id
2. Bit7
11主控层级 id
3. Bit1214 总控层级 id
当 bit15 广播标识不为 0 时:
1. Bit0
6 广播的设备类型,从控主控总控都为1。
2. Bit12~14 需要广播的设备层级(从控 1、主控 2、总控 3)
00 1个字节 0:在线升级
1:日志读写
2:调试
C4(192+4) 1个字节 数据长度;
除 数 据 流 头(前5个字节) 和CRC之外的数据长度
00 00 00 00 4个字节 偏移值;
bin 文件当前数据相对于文件头的偏移字节数
30 BA …… 64 00 192个字节 bin 文件数据采用多包发送,建议以 192 字节为单位进行传输,四字节对齐,不足补 0
72 EC 2个字节 CRC检验
下位机报文响应:

D0 FF FF FF 05 05 C0 00 00 00 04 81

报文(0x) 字节数 内容
D0 1个字节 协议版本号
FF FF 2个字节 目标设备ID,默认返回0xFFFF
FF 1个字节 消息类型,默认返回0xFF
05 1个字节 消息长度
日志消息头到CRC之前的数据长度(不包含CRC)
05 1个字节 设备地址
C0 00 00 00
(00 00 00 C0)
4个字节 小端模式
04 81 2个字节 CRC校验

Modbus ASCII协议

Modbus ASCII与Modbus RTU的传输格式有所不同,以下是格式区别:

协议 开始标记 结束标记 校验 传输效率 程序处理
ASCII :(英文冒号) CR,LF
(\r\n)
LRC 直观,简单,易调试
RTU CRC 稍复杂
模式 发送帧 (Hex) 发送帧 (可视字符) 可读性
RTU 01 03 28 00 00 01 84 0A ☺ ♥ ☺ ♦(乱码) 极差,全是不可见字符
ASCII :010328000001D3\r\n :010328000001D3\r\n 极好,直接看懂含义

ASCII(1979,文本兼容) → RTU(1980s,效率优化) → TCP(1999,网络化)

所以一般不使用ASCII进行传输。早期设备处理能力弱,复杂的 CRC 校验计算负担重,而 ASCII 使用的 LRC(纵向冗余校验)计算简单,适合当时的 CPU。

LRC计算:

LRC(Longitudinal Redundancy Check,纵向冗余校验)的计算逻辑非常简单,就是把帧里所有字节加起来,取结果的低 8 位,然后求其二进制补码

计算:010328000001的LRC

一、 逐步计算
1. 提取有效数据字节

去掉帧头 :,将每两个字符转换为一个十六进制字节:

1
01`, `03`, `28`, `00`, `00`, `01
2. 转换为十进制并求和
1
2
3
4
5
6
0x01 = 1
0x03 = 3
0x28 = 40
0x00 = 0
0x00 = 0
0x01 = 1

求和1 + 3 + 40 + 0 + 0 + 1 = 45

3. 取低 8 位

45 小于 256,低 8 位仍是 45

4. 计算二进制补码

公式:LRC = (0x100 - sum) & 0xFF

计算:256 - 45 = 211

5. 转为十六进制
1
211`的十六进制是 `0xD3

最终 LRC 结果:**D3**

二、 验证

计算包括 LRC 在内的所有字节和:

1
2
3
01 + 03 + 28 + 00 + 00 + 01 + D3
= 1 + 3 + 40 + 0 + 0 + 1 + 211
= 256

256 的低 8 位为 0,校验正确。

三、 完整 ASCII 帧
1
:010328000001D3\r\n

Modbus TCP协议

使用IP地址+端口号

把 RTU 帧去掉 CRC 校验,然后在前面加个 7 字节的 MBAP 报文头,最后走 TCP/IP 网络发出去。

一个完整的 Modbus TCP 帧 = **MBAP 头 (7字节) + RTU 数据区 (去掉了CRC)**。

1
2
3
[ TCP/IP 头 ] + [ MBAP 头 (7字节) ] + [ 功能码 + 数据区 ]
|------------------| |-----------------|
新增的 TCP 头 熟悉的 RTU 数据

Modbus TCP与Modbus RTU的区别:

方面 RTU 的思维 TCP 的调整
连接 打开串口,设置 9600,N,8,1 建立 TCP Socket,连接 IP:端口号
地址 从站地址 (1-247) IP地址为主,单元标识符为辅
校验 自己算 CRC16 不用算,TCP 保证可靠性
多设备 485总线轮询 每个设备一个 TCP 连接
调试 串口助手,看 Hex Wireshark,看 TCP 流

Modbus RTU报文转为TCP报文:

需要转换的RTU报文:01 03 28 00 00 01 CRC

1、去掉CRC校验:

01 03 28 00 00 01

2、构造 MBAP 头

需要 4 个字段,按需填写:

字段 值 (Hex) 说明
事务标识符 00 01 任意值,用于匹配请求响应(常用递增)
协议标识符 00 00 固定 0,表示 Modbus 协议
数据长度 00 06 关键:后面还有 6 字节 (01+03+28+00+00+01)
单元标识符 01 对应 RTU 的从站地址

所以 MBAP 头 = 00 01 00 00 00 06 01

3、拼接为TCP帧
1
2
3
4
MBAP头: 00 01 00 00 00 06 01
RTU数据: 01 03 28 00 00 01

TCP请求: 00 01 00 00 00 06 01 03 28 00 00 01

同样的响应帧:

1
2
3
4
MBAP头: 00 01 00 00 00 05 01
RTU数据: 01 03 02 0D 08

TCP响应: 00 01 00 00 00 05 01 03 02 0D 08
TCP流:
1
2
请求: 000100000006010328000001
响应: 0001000000050103020d08

Modbus RTU、ASCII、TCP比较

格式 报文 连接方式 是否16进制
RTU 01 03 28 00 00 01 XX XX(XX为CRC) 串口(COM)
ASCII :010328000001XX\r\n(XX为LRC) 串口(COM)
TCP 00 01 00 00 00 06 01 03 28 00 00 01 以太网(TCP IP)

Modbus RTU、ASCII、TCP读取示例

Modbus RTU:

Modbus ASCII:

Modbus TCP:

其他协议

广五所(GWS)温箱协议:

1
2
[18:30:59.114]发→◇1,TEMP?□
[18:30:59.148]收←◆24.8,65.0,100.0,-40.0

以上是读取当前温度的指令。返回数据为:当前温度,目标温度,最高温度限制,最低温度限制。

Modbus Over CAN:

“Modbus Over CAN”是一种将Modbus应用层报文封装在CAN数据链路层上进行传输的协议映射技术。 它并非一个像Modbus RTU或TCP那样的原生、标准化的独立协议,而是一种协议适配或网关技术的常见实现方式。

其核心思想是:利用CAN总线的高可靠性和实时性作为“公路”,来运输Modbus的“货物”(即Modbus报文)。简单说就是使用CAN来传输Modbus RTU报文。

结语

大家看到这里应该对Modbus有一个基本的了解了!