Modbus协议讲解
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 | [11:22:44.368]发→◇01 03 28 00 00 01 8D AA □ |
上位机发送数据: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 | [11:22:48.188]发→◇01 03 28 00 00 10 4D A6 □ |
上位机发送数据: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 | [14:55:05.373] [发送]: 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 | 写入: |
| 报文(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 个 16 位的寄存器为十六进制的 FFFF(即全为 1);称此寄存器为 CRC 寄存器。
- 把第一个 8 位二进制数据(即通讯信息帧的第一个字节)与 16 位的 CRC 寄存器的低 8位相异或,把结果存放在 CRC 寄存器。
- 把 CRC 寄存器的内容右移一位(朝低位)用 0 填补最高位,并检查右移后的移出位。
- 如果移出位为 0:重复第 3 步(再次右移 1 位);如果移出位为 1:CRC 寄存器与多项 式 A001(1010 0000 0000 0001)进行异或。
- 重复步骤 3 和 4,直到右移 8 次,这样整个 8 位数据全部进行了处理。
- 重复步骤 2 到步骤 5,进行通讯信息帧下一个字节的处理。
- 将通讯信息帧的所有字节按上述步骤计算完成后,得到 16 位 CRC 寄存器的高,低字节交换。
- 最后得到的 CRC 寄存器内容即为:CRC 码。
C语言CRC运算源码:
1 | unsigned short ModBusCRC16(const void *s, int n) |
C#CRC运算源码:
1 | public static ushort ModBusCRC16(byte[] pucFrame, int usLen) //计算CRC校验 |
ModBusCRC16在线计算网站:
https://ltkdriver.freedash.top/Tool/ModbusRTUCRC16
非Modbus RTU协议
跟Modbus协议完全不一样,完全由自己规定。
自定义升级协议:
1 | [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 |
升级协议解析:
上位机报文发送:
| 报文(0x) | 字节数 | 内容 |
|---|---|---|
| D0 | 1个字节 | 协议版本号 |
| 01 A0 (A001) (1010000000000001) (bit15、bit0、bit13) |
2个字节 | 目标设备 ID(小端模式) 当 bit15 广播标识为 0 时: 1. Bit0 2. Bit7 3. Bit12 当 bit15 广播标识不为 0 时: 1. Bit0 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 | 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 | 01 + 03 + 28 + 00 + 00 + 01 + D3 |
256 的低 8 位为 0,校验正确。
三、 完整 ASCII 帧
1 | :010328000001D3\r\n |
Modbus TCP协议
使用IP地址+端口号
把 RTU 帧去掉 CRC 校验,然后在前面加个 7 字节的 MBAP 报文头,最后走 TCP/IP 网络发出去。
一个完整的 Modbus TCP 帧 = **MBAP 头 (7字节) + RTU 数据区 (去掉了CRC)**。
1 | [ TCP/IP 头 ] + [ MBAP 头 (7字节) ] + [ 功能码 + 数据区 ] |
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 | MBAP头: 00 01 00 00 00 06 01 |
同样的响应帧:
1 | MBAP头: 00 01 00 00 00 05 01 |
TCP流:
1 | 请求: 000100000006010328000001 |
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 | [18:30:59.114]发→◇1,TEMP?□ |
以上是读取当前温度的指令。返回数据为:当前温度,目标温度,最高温度限制,最低温度限制。

Modbus Over CAN:
“Modbus Over CAN”是一种将Modbus应用层报文封装在CAN数据链路层上进行传输的协议映射技术。 它并非一个像Modbus RTU或TCP那样的原生、标准化的独立协议,而是一种协议适配或网关技术的常见实现方式。
其核心思想是:利用CAN总线的高可靠性和实时性作为“公路”,来运输Modbus的“货物”(即Modbus报文)。简单说就是使用CAN来传输Modbus RTU报文。
结语
大家看到这里应该对Modbus有一个基本的了解了!