3. Modbus RTU协议详解:协议栈结构、帧格式、功能码与CRC校验
各位工程师朋友,咱们今天来啃一块硬骨头——Modbus RTU协议。说实话,这协议在工业现场太常见了,PLC、变频器、智能仪表、温控器……十台设备里至少有七八台支持它。我调试过的项目里,Modbus RTU占了将近一半的通讯故障。不是协议本身难,而是很多人没把细节吃透。
好,咱们不绕弯子,直接开讲。
3.1 Modbus协议栈结构
Modbus协议栈其实很简单,就三层:物理层、数据链路层、应用层。你想想看,它不像TCP/IP那么复杂,没有会话层、表示层那些花里胡哨的东西。
- 物理层:RS-232、RS-485、RS-422。现场用得最多的是RS-485,半双工,差分信号,抗干扰能力强。我见过不少新手把RS-485的A/B线接反,结果通讯死活不通——嗯,这个坑我踩过。
- 数据链路层:负责帧的封装、地址识别、差错校验。Modbus RTU在这一层用的是主从模式,一个主机,最多247个从机。
- 应用层:定义功能码和数据格式。说白了,就是告诉从机你要读什么、写什么。
我个人习惯把Modbus协议栈画成下面这张图,方便理解:
3.2 RTU帧格式
Modbus RTU的帧格式,说白了就是一段二进制数据流。每个帧由四部分组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从机地址,范围1~247,0为广播地址 |
| 功能码 | 1字节 | 指示操作类型(读/写/诊断等) |
| 数据区 | N字节 | 寄存器地址、数量、数据值等 |
| CRC校验 | 2字节 | 低字节在前,高字节在后 |
这里有个关键点:帧与帧之间必须有至少3.5个字符时间的静默间隔。为什么?因为RTU没有帧头帧尾标记,全靠时间间隔来区分。我曾经遇到一个项目,上位机发帧太快,间隔只有1.5个字符时间,从机直接把两帧当成一帧处理了——数据全乱套。
RTU帧示例(读保持寄存器):
从机地址: 0x01
功能码: 0x03
起始地址: 0x00 0x6B (高字节在前)
寄存器数: 0x00 0x03 (读3个寄存器)
CRC校验: 0x76 0x87 (低字节在前)
完整帧:01 03 00 6B 00 03 76 87
3.3 功能码详解
功能码是Modbus协议的核心。我调试时最常用的就是01~06和15、16这八个。咱们一个一个过。
3.3.1 功能码01:读线圈状态
读取从机的DO(数字量输出)状态。返回的数据按位打包,一个字节8个线圈。举个例子,读地址0~7的线圈,返回0x55表示线圈0、2、4、6为ON,1、3、5、7为OFF。
3.3.2 功能码02:读离散输入
读取从机的DI(数字量输入)状态。和01码类似,但只能读不能写。我在调试一个流水线项目时,传感器信号就是通过02码读上来的——嗯,当时有个传感器接线虚焊,读回来的数据忽高忽低,排查了半天。
3.3.3 功能码03:读保持寄存器
这个最常用。读取从机的保持寄存器(可读可写),比如设定温度、频率、压力值等。数据格式:每个寄存器2字节,高字节在前。
3.3.4 功能码04:读输入寄存器
读取从机的输入寄存器(只读),比如当前温度、电压、电流等测量值。和03码的区别在于:03读的是可写的参数,04读的是只读的测量值。
3.3.5 功能码05:写单个线圈
控制单个DO输出。数据区固定为2字节:0xFF00表示ON,0x0000表示OFF。注意:不是0x0001!我见过有人写0x0001,结果从机不认——协议就是这么规定的,没办法。
3.3.6 功能码06:写单个寄存器
写入单个保持寄存器。数据区2字节,就是要写的值。比如把地址0x006B的寄存器设为100,帧就是:01 06 00 6B 00 64 CRC。
3.3.7 功能码15:写多个线圈
批量写DO。数据区包含:起始地址、线圈数量、字节数、数据。比如写地址0~7的线圈为0x55,帧就是:01 0F 00 00 00 08 01 55 CRC。
3.3.8 功能码16:写多个寄存器
批量写保持寄存器。数据区包含:起始地址、寄存器数量、字节数、数据值。这个在配置变频器参数时特别有用,一次可以写多个参数。
我的调试小技巧: 刚开始调试时,先用功能码03把所有寄存器读一遍,确认通讯正常。然后再试写操作。这样能快速定位问题是出在通讯上还是逻辑上。
3.4 CRC校验计算
CRC校验是Modbus RTU的保命符。没有它,数据传错了你都不知道。CRC-16/MODBUS的算法如下:
- 预置一个16位寄存器为0xFFFF
- 取第一个字节与寄存器低8位异或
- 右移1位,高位补0。如果移出的最低位为1,则与0xA001异或
- 重复步骤3,直到8位全部处理完
- 取下一个字节,重复步骤2~4
- 所有字节处理完后,寄存器取反,得到CRC值
- 发送时低字节在前,高字节在后
C语言实现示例:
unsigned int crc16_modbus(unsigned char *buf, int len)
{
unsigned int crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc ^= buf[i];
for (int j = 0; j < 8; j++)
{
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
注意: 很多现成的CRC计算工具算出来的结果,和实际通讯抓包的数据可能不一致。原因往往是字节序搞反了。记住:RTU帧里CRC是低字节在前,高字节在后。比如计算结果是0x8776,帧里要写成76 87。
我个人习惯在调试时先用串口助手抓一帧正常通讯的数据,然后用CRC工具验证一下。如果对不上,八成是字节序或者多项式搞错了。Modbus RTU用的多项式是0x8005(实际计算用0xA001),别搞混了。
好,Modbus RTU协议的核心内容就这些。功能码和CRC校验是调试中最容易出问题的地方。你只要把帧格式吃透,功能码记牢,CRC会算,现场调试基本就能搞定八成以上的问题。
公众号:蓝海资料掘金营,微信deep3321