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协议栈画成下面这张图,方便理解:

Modbus协议栈结构 应用层 功能码 + 数据对象(线圈、寄存器、离散输入等) 功能码:01/02/03/04/05/06/15/16 数据链路层 帧封装:地址码 + 功能码 + 数据 + CRC校验 主从模式,地址范围:1~247 物理层 RS-232 / RS-485 / RS-422 波特率:9600、19200、38400、115200等

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的算法如下:

  1. 预置一个16位寄存器为0xFFFF
  2. 取第一个字节与寄存器低8位异或
  3. 右移1位,高位补0。如果移出的最低位为1,则与0xA001异或
  4. 重复步骤3,直到8位全部处理完
  5. 取下一个字节,重复步骤2~4
  6. 所有字节处理完后,寄存器取反,得到CRC值
  7. 发送时低字节在前,高字节在后

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