Modbus-RTU 数据帧格式 、报文解析
Modbus 协议详解:报文结构、功能码与通信示例
一、Modbus 协议简介
1.1 什么是 Modbus?
Modbus 可以从名称上拆分为 Mod 和 Bus 两部分。其中,Bus 表示总线,意味着多个设备可以通过同一种通信协议进行数据交换。
Modbus 最初由 Modicon 公司开发,主要用于 PLC(可编程逻辑控制器)之间以及 PLC 与外围设备之间的通信。后来,Modicon 被施耐德电气(Schneider Electric)收购,Modbus 协议也逐渐得到广泛应用,成为工业自动化领域常见的通信协议之一。
目前,Modbus 广泛应用于 PLC、HMI、传感器、变频器、电力设备以及各类嵌入式控制器中。
1.2 Modbus 的通信特点
Modbus 支持多种传输方式,常见的有:
- Modbus RTU:通常通过 RS-485 或 RS-232 等串行接口传输,使用二进制编码。
- Modbus ASCII:通过串行接口传输,使用 ASCII 字符表示报文。
- Modbus TCP:通过 TCP/IP 网络传输,通常使用以太网接口。
在传统的 Modbus 串行通信中,通常采用主从通信模式:
- 主站负责发起请求。
- 从站接收请求,并根据功能码执行相应操作。
- 从站通常不会主动向主站发送数据,而是在收到请求后返回响应。
- 一条串行总线上通常只有一个主站,可以连接多个从站。
Modbus 串行规范使用 1~247 作为常规从站地址,因此最多可以寻址 247 个常规从站。不过,实际可连接设备数量还受到收发器负载能力、总线布线、通信距离等因素的限制。
需要注意的是,Modbus TCP 的通信模型与传统串行主从通信有所不同,通常使用客户端和服务器的概念。
二、Modbus 报文模型
2.1 基本报文结构
一个 Modbus 报文可以理解为一次完整的通信请求或响应。不同传输方式的报文结构有所区别。
对于 Modbus RTU,一帧报文通常由以下部分组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 指定目标从站 |
| 功能码 | 1 字节 | 指定需要执行的操作 |
| 数据 | 不定长 | 包含起始地址、数量、寄存器值等信息 |
| CRC 校验 | 2 字节 | 用于检测报文传输错误 |
对于 Modbus TCP,报文前部使用 MBAP(Modbus Application Protocol)报文头,后面跟随功能码和数据,不使用 RTU 格式的 CRC 校验字段。
2.2 字节序与位序
Modbus 寄存器数据采用 大端字节序(Big Endian):对于一个 16 位寄存器,先传输高字节,再传输低字节。
例如,寄存器值为 0x1234,其数据部分为:
12 34
对于线圈和离散量输入,数据则按照位进行打包。每个数据字节的最低有效位(LSB)对应较小的线圈地址,后续位依次对应递增的地址。
例如,数据字节 0xCD 的二进制表示为:
1100 1101
对应的线圈状态如下:
| 位 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|---|---|
| 状态 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
如果该字节表示从线圈地址 20 开始的状态,则:
| 线圈地址 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 |
|---|---|---|---|---|---|---|---|---|
| 状态 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
也就是说,线圈地址 20 对应 Bit 0,线圈地址 27 对应 Bit 7。
三、常用功能码
Modbus 通过功能码区分不同的读写操作。
以下是常见的标准功能码:
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
0x01 |
读线圈 | Coil | 读取一个或多个线圈状态 |
0x02 |
读离散量输入 | Discrete Input | 读取一个或多个离散输入状态 |
0x03 |
读保持寄存器 | Holding Register | 读取一个或多个保持寄存器 |
0x04 |
读输入寄存器 | Input Register | 读取一个或多个输入寄存器 |
0x05 |
写单个线圈 | Coil | 写入一个线圈状态 |
0x06 |
写单个寄存器 | Holding Register | 写入一个保持寄存器 |
0x0F |
写多个线圈 | Coil | 一次写入多个线圈状态 |
0x10 |
写多个寄存器 | Holding Register | 一次写入多个保持寄存器 |
其中,线圈通常用于表示开关量输出,离散量输入用于表示开关量输入;保持寄存器和输入寄存器则用于保存或提供 16 位数据。
需要注意:
0x03和0x04的区别在于寄存器类型,而不只是名称不同。0x06、0x10是标准功能码;0x41、0x43等属于设备或厂商可能自行定义的扩展功能码,并非通用标准功能码。- 原始 Modbus 协议并不规定 PLC 中
X、Y、M、D等设备地址的具体含义。这些通常是特定 PLC 厂商定义的内部元件名称,需要由设备手册说明其与 Modbus 地址的映射关系。
四、读线圈与读离散量输入(0x01、0x02)
功能码 0x01 用于读取线圈状态,功能码 0x02 用于读取离散量输入状态。
两者的报文结构基本相同,主要区别在于访问的对象不同。
4.1 请求报文
请求报文由从站地址、功能码、起始地址、读取数量和 CRC 校验组成。
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 0x01 或 0x02 |
| 起始地址 | 2 字节 | 要读取的第一个线圈或输入地址 |
| 读取数量 | 2 字节 | 要读取的位数 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
其中,起始地址和读取数量均采用高字节在前的顺序。
4.2 响应报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 响应设备地址 |
| 功能码 | 1 字节 | 与请求功能码相同 |
| 字节计数 | 1 字节 | 后续状态数据的字节数 |
| 状态数据 | N 字节 | 按位打包的线圈或输入状态 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
状态数据的字节数为:
[
N=\left\lceil\frac{Q}{8}\right\rceil
]
其中,(Q) 为请求读取的位数。
如果读取数量不是 8 的整数倍,最后一个数据字节中未使用的高位填充为 0。
4.3 示例:读取 19 个线圈
假设从站地址为 0x01,需要从线圈地址 0x0013 开始读取 19 个线圈。
请求报文的数据部分如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 01 |
| 起始地址高字节 | 00 |
| 起始地址低字节 | 13 |
| 读取数量高字节 | 00 |
| 读取数量低字节 | 13 |
| CRC 校验 | 根据前面的报文字节计算 |
由于需要读取 19 位状态,因此需要:
[
N=\left\lceil\frac{19}{8}\right\rceil=3
]
响应中的三个数据字节可以为:
CD 6B 05
对应关系如下。
第一个字节 0xCD:
| 位 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|---|---|
| 线圈地址 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 |
| 状态 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
第二个字节 0x6B:
| 位 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|---|---|
| 线圈地址 | 34 | 33 | 32 | 31 | 30 | 29 | 28 | 27 |
| 状态 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
第三个字节 0x05:
0x05 = 0000 0101
| 位 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
|---|---|---|---|---|---|---|---|---|
| 线圈地址 | 未使用 | 未使用 | 未使用 | 未使用 | 未使用 | 38 | 37 | 36 |
| 状态 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
这里读取的 19 个线圈地址为 19~37。第三个字节中只有三个有效位,其余五个高位补 0。
因此,完整响应的数据部分为:
03 CD 6B 05
其中,03 表示后续状态数据占 3 字节。
注意:上面的报文仅展示关键字段,未列出实际 CRC 校验值。
五、读寄存器(0x03、0x04)
功能码 0x03 用于读取保持寄存器,功能码 0x04 用于读取输入寄存器。
每个寄存器占用 16 位,即 2 字节。
5.1 请求报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 0x03 或 0x04 |
| 起始地址 | 2 字节 | 第一个寄存器的地址 |
| 寄存器数量 | 2 字节 | 要读取的寄存器数量 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
5.2 响应报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 响应设备地址 |
| 功能码 | 1 字节 | 与请求功能码相同 |
| 字节计数 | 1 字节 | 后续寄存器数据的总字节数 |
| 寄存器数据 | 2 × N 字节 | 每个寄存器占 2 字节 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
其中,(N) 为读取的寄存器数量。
响应的数据字节数为:
[
B=2N
]
5.3 示例:读取寄存器 108~110
假设从站地址为 0x01,从寄存器地址 0x006B 开始读取 3 个寄存器。
请求报文的关键字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 03 |
| 起始地址高字节 | 00 |
| 起始地址低字节 | 6B |
| 寄存器数量高字节 | 00 |
| 寄存器数量低字节 | 03 |
注意:这里的 0x006B 是十进制 107。如果设备手册使用从 1 开始的寄存器编号,那么它可能对应文档中的第 108 个寄存器。这取决于设备采用的地址编号规则。
假设读取到的数据为:
02 2B 00 00 00 64
响应字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 03 |
| 字节计数 | 06 |
| 寄存器 108 的数据 | 02 2B |
| 寄存器 109 的数据 | 00 00 |
| 寄存器 110 的数据 | 00 64 |
将每两个字节组合为一个 16 位无符号整数,得到:
| 寄存器 | 内容值(十六进制) | 内容值(十进制) |
|---|---|---|
| 寄存器 108 | 0x022B |
555 |
| 寄存器 109 | 0x0000 |
0 |
| 寄存器 110 | 0x0064 |
100 |
在实际开发中,解析寄存器数据时应先组合高、低字节,再根据设备协议将结果解释为无符号整数、有符号整数、浮点数或其他数据类型。
六、写单个线圈(0x05)
功能码 0x05 用于写入单个线圈状态。
6.1 请求报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 固定为 0x05 |
| 线圈地址 | 2 字节 | 要写入的线圈地址 |
| 写入值 | 2 字节 | 0xFF00 表示 ON,0x0000 表示 OFF |
| CRC 校验 | 2 字节 | RTU 帧校验 |
写入值的定义如下:
| 写入值 | 含义 |
|---|---|
FF 00 |
置为 ON,即 1 |
00 00 |
置为 OFF,即 0 |
6.2 响应报文
正常情况下,从站返回与请求报文内容相同的响应,包括从站地址、功能码、线圈地址、写入值以及重新计算的 CRC。
6.3 示例:将线圈地址 172 置为 ON
线圈地址 172 的十六进制表示为 0x00AC。
请求报文的关键字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 05 |
| 线圈地址高字节 | 00 |
| 线圈地址低字节 | AC |
| 写入值高字节 | FF |
| 写入值低字节 | 00 |
响应报文的上述字段与请求相同。
七、写单个寄存器(0x06)
功能码 0x06 用于向一个保持寄存器写入 16 位数据。
7.1 请求报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 固定为 0x06 |
| 寄存器地址 | 2 字节 | 目标寄存器地址 |
| 写入值 | 2 字节 | 要写入的 16 位数据 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
7.2 响应报文
正常情况下,从站返回与请求内容相同的响应。
7.3 示例:向寄存器地址 2 写入 0x0003
请求报文的关键字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 06 |
| 寄存器地址高字节 | 00 |
| 寄存器地址低字节 | 02 |
| 写入值高字节 | 00 |
| 写入值低字节 | 03 |
从站成功执行写入后,返回对应的响应报文。
需要注意,0x41 并不是标准的单寄存器写入功能码。如果某个设备使用 0x41 完成类似操作,应以该设备的通信协议文档为准。
八、写多个线圈(0x0F)
功能码 0x0F 用于一次写入多个线圈状态。
8.1 请求报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 固定为 0x0F |
| 起始地址 | 2 字节 | 第一个待写入的线圈地址 |
| 线圈数量 | 2 字节 | 待写入的线圈总数 |
| 字节计数 | 1 字节 | 后续线圈数据的字节数 |
| 线圈数据 | N 字节 | 按位打包的线圈状态 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
如果要写入 (Q) 个线圈,需要的数据字节数为:
[
N=\left\lceil\frac{Q}{8}\right\rceil
]
8.2 响应报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 响应设备地址 |
| 功能码 | 1 字节 | 固定为 0x0F |
| 起始地址 | 2 字节 | 本次写入的起始地址 |
| 线圈数量 | 2 字节 | 本次写入的线圈数量 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
与请求报文不同,正常响应不会再次携带全部线圈状态数据。
8.3 示例:从线圈地址 19 开始写入 10 个线圈
假设要写入的状态数据为:
CD 01
二进制表示为:
1100 1101 0000 0001
由于只写入 10 个线圈,实际使用前 10 个有效位,剩余 6 位不参与本次写入。
对应的线圈状态如下:
| 数据位 | Bit 0 | Bit 1 | Bit 2 | Bit 3 | Bit 4 | Bit 5 | Bit 6 | Bit 7 | Bit 0 | Bit 1 |
|---|---|---|---|---|---|---|---|---|---|---|
| 线圈地址 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 状态 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 |
请求报文的关键字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 0F |
| 起始地址高字节 | 00 |
| 起始地址低字节 | 13 |
| 线圈数量高字节 | 00 |
| 线圈数量低字节 | 0A |
| 字节计数 | 02 |
| 第一个数据字节 | CD |
| 第二个数据字节 | 01 |
正常响应会返回起始地址 0x0013 和写入数量 0x000A,不包含 CD 01 这两个数据字节。
九、写多个寄存器(0x10)
功能码 0x10 用于一次写入多个保持寄存器。
每个寄存器占用 2 字节,因此写入 (N) 个寄存器时,数据部分需要 (2N) 个字节。
9.1 请求报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 目标设备地址 |
| 功能码 | 1 字节 | 固定为 0x10 |
| 起始地址 | 2 字节 | 第一个待写入的寄存器地址 |
| 寄存器数量 | 2 字节 | 待写入的寄存器数量 |
| 字节计数 | 1 字节 | 后续寄存器数据的字节数 |
| 寄存器数据 | 2 × N 字节 | 每个寄存器占 2 字节 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
其中:
[
B=2N
]
9.2 响应报文
| 字段 | 长度 | 说明 |
|---|---|---|
| 从站地址 | 1 字节 | 响应设备地址 |
| 功能码 | 1 字节 | 固定为 0x10 |
| 起始地址 | 2 字节 | 本次写入的起始地址 |
| 寄存器数量 | 2 字节 | 本次写入的寄存器数量 |
| CRC 校验 | 2 字节 | RTU 帧校验 |
正常响应只确认本次写入的起始地址和寄存器数量,不会再次返回所有寄存器数据。
9.3 示例:连续写入两个寄存器
假设从寄存器地址 0x0001 开始,依次写入以下两个值:
- 第一个寄存器:
0x000A - 第二个寄存器:
0x0102
请求报文的关键字段如下:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 10 |
| 起始地址高字节 | 00 |
| 起始地址低字节 | 01 |
| 寄存器数量高字节 | 00 |
| 寄存器数量低字节 | 02 |
| 字节计数 | 04 |
| 第一个寄存器高字节 | 00 |
| 第一个寄存器低字节 | 0A |
| 第二个寄存器高字节 | 01 |
| 第二个寄存器低字节 | 02 |
因此,请求的数据部分为:
00 0A 01 02
正常响应中的关键字段为:
| 字段 | 十六进制 |
|---|---|
| 从站地址 | 01 |
| 功能码 | 10 |
| 起始地址高字节 | 00 |
| 起始地址低字节 | 01 |
| 寄存器数量高字节 | 00 |
| 寄存器数量低字节 | 02 |
十、Modbus 开发中的注意事项
在嵌入式设备、PLC、HMI 或上位机程序中实现 Modbus 通信时,需要特别注意以下问题。
10.1 寄存器地址与文档编号的区别
部分设备手册使用从 1 开始的寄存器编号,而 Modbus 报文中的起始地址字段通常以 0 为基准。
例如,文档中的第 108 个寄存器可能对应报文地址 107,即十六进制 0x006B。
但不同设备的文档约定可能不同,不能简单地对所有地址统一减 1,应优先查阅设备手册。
10.2 寄存器字节序与多寄存器数据顺序
Modbus 标准规定每个 16 位寄存器内部采用高字节在前的顺序,但对于 32 位整数、浮点数等跨多个寄存器的数据,寄存器之间的排列顺序可能由设备厂商定义。
例如,浮点数可能采用:
寄存器 1:高 16 位
寄存器 2:低 16 位
也可能采用其他字序排列。因此,解析跨寄存器的数据时,应根据设备协议确定具体的字节序和字序。
10.3 CRC 校验
Modbus RTU 使用 CRC-16 校验。CRC 计算结果为 16 位,发送时通常先发送低字节,再发送高字节。
这与寄存器数据采用高字节在前的顺序不同。
实现时,应按照 Modbus RTU 规定的 CRC 算法计算校验值,并对接收报文进行校验。
10.4 超时与异常响应
实际通信中,从站可能由于地址错误、寄存器不存在、数量超出限制或设备忙碌等原因,无法正常执行请求。
Modbus 异常响应通常将原功能码的最高位置 1,并在数据字段中返回异常码。例如,功能码 0x03 对应的异常响应功能码为 0x83。
常见异常码包括:
| 异常码 | 名称 | 说明 |
|---|---|---|
0x01 |
Illegal Function | 不支持该功能码 |
0x02 |
Illegal Data Address | 请求的数据地址无效 |
0x03 |
Illegal Data Value | 请求中的数据值不合法 |
0x04 |
Server Device Failure | 从站执行操作时发生故障 |
对于没有响应的情况,主站还应实现接收超时、有限次数重试和通信恢复机制。
10.5 标准功能码与厂商扩展功能码
开发通用 Modbus 主站时,应优先实现标准功能码,例如 0x01、0x03、0x05、0x06、0x0F 和 0x10。
如果设备使用 0x41、0x43 等自定义功能码,则应按照厂商提供的协议文档实现对应的报文格式、数据解释和异常处理逻辑。
十一、总结
Modbus 是工业自动化领域常见的通信协议,具有结构简单、实现方便、设备兼容性较好等特点。
掌握 Modbus 的关键在于理解以下内容:
- 报文结构:从站地址、功能码、数据和校验字段的作用。
- 数据编码:寄存器的字节序,以及线圈数据的位打包规则。
- 功能码:不同功能码对应的读写对象和操作方式。
- 地址映射:设备手册中的寄存器编号与报文起始地址之间的关系。
- 异常处理:CRC 校验、超时重试和异常响应的处理方式。
在实际开发中,只要明确设备的通信方式、寄存器映射关系以及数据格式,就可以基于这些基础知识实现 Modbus RTU 或 Modbus TCP 的主站通信功能。
- 点赞
- 收藏
- 关注作者
评论(0)