3. Modbus RTU帧结构:地址域、功能码、数据域、CRC校验详解

好,咱们今天来聊聊Modbus RTU的帧结构。说白了,这就是设备之间通信的“信封”。你写一封信,总得有收件人地址、信件内容、还有签名吧?Modbus RTU帧也是一样的道理。

我刚开始接触Modbus时,总觉得这东西很神秘。后来拆开一看,其实就四个部分:地址域、功能码、数据域、CRC校验。嗯,就这么简单。但简单归简单,每个字节的位置、含义、顺序,错一个都不行。我在现场调试时,就吃过这个亏。

3.1 地址域:谁在说话?

地址域是帧的第一个字节。它告诉总线上的所有设备:“这包数据是发给谁的”。

  • 范围:1 ~ 247(0是广播地址,248~255保留)
  • 长度:1个字节
  • 作用:从机地址,唯一标识一个设备

你想想看,一条RS485总线上可能挂着几十个设备。主站发一个请求,所有从站都能收到。但只有地址匹配的那个从站,才会响应。其他设备?就当没看见。

重要提示:地址0是广播地址。所有从站都会执行命令,但不会回复。我建议你慎用广播,尤其是在写操作时。我曾经在一个项目中,用广播同时复位了20台变频器,结果现场一片混乱……

3.2 功能码:要干什么?

功能码是帧的第二个字节。它告诉从站:“你要执行什么操作”。

常用的功能码就那么几个,我列个表给你看:

功能码 名称 作用
0x01 读线圈 读取DO(数字量输出)
0x02 读离散输入 读取DI(数字量输入)
0x03 读保持寄存器 读取AO(模拟量输出)或参数
0x04 读输入寄存器 读取AI(模拟量输入)
0x05 写单个线圈 控制一个DO点
0x06 写单个寄存器 设置一个AO或参数
0x0F 写多个线圈 批量控制DO点
0x10 写多个寄存器 批量设置AO或参数

我个人习惯,先把0x03和0x06搞熟。这两个功能码覆盖了80%以上的应用场景。读参数、写参数,全用它们。

小技巧:如果从站返回的功能码最高位是1(比如0x83),说明出错了。这叫异常响应。后面我们会专门讲异常码。

3.3 数据域:具体内容

数据域是帧的第三部分,长度可变。它包含具体的操作参数,比如起始地址、数据数量、或者要写入的值。

举个例子,读保持寄存器的请求帧:

从站地址: 0x01
功能码:   0x03
起始地址: 0x00 0x00  (高字节在前)
寄存器数: 0x00 0x0A  (读10个寄存器)
CRC校验:  0xXX 0xXX

你看,数据域里就是“从哪里读”和“读多少”。很简单吧?

再比如写单个寄存器的请求帧:

从站地址: 0x01
功能码:   0x06
寄存器地址: 0x00 0x01
写入值:   0x03 0xE8  (1000)
CRC校验:  0xXX 0xXX

这里要注意,Modbus RTU的数据都是大端模式(Big-Endian)。也就是高字节在前,低字节在后。我见过不少新手在这个地方栽跟头。明明写的是1000,结果设备收到的是3.88……

避坑指南:我曾经调试一个温控器,怎么都写不进参数。折腾了半天,发现是字节序搞反了。从那以后,我每次写代码都会在注释里标清楚:“高字节在前,低字节在后”。

3.4 CRC校验:防伪签名

CRC校验是帧的最后两个字节。它用来检测数据在传输过程中有没有被干扰。

为什么需要CRC?因为RS485总线在工业现场,电磁干扰很常见。一个比特翻转,数据就全错了。CRC就是用来发现这种错误的。

Modbus RTU用的是CRC-16,多项式是0x8005。计算过程是这样的:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC寄存器的低8位异或
  3. 右移一位,如果移出的位是1,则与0xA001异或
  4. 重复8次,处理完一个字节
  5. 所有字节处理完后,CRC寄存器取反
  6. 低字节在前,高字节在后(注意!这是小端模式)

我给你一个C语言的实现,这是我一直在用的:

unsigned int crc16_modbus(unsigned char *data, unsigned int len)
{
    unsigned int crc = 0xFFFF;
    unsigned int i, j;
    
    for (i = 0; i < len; i++)
    {
        crc ^= data[i];
        for (j = 0; j < 8; j++)
        {
            if (crc & 0x0001)
                crc = (crc >> 1) ^ 0xA001;
            else
                crc = crc >> 1;
        }
    }
    
    return crc;
}

嗯,这里要注意。CRC校验在帧里是低字节在前,高字节在后。这和前面的数据域正好相反。为什么?因为Modbus协议就是这么规定的。别问为什么,记住就行。

关键点:发送时,先发CRC低字节,再发CRC高字节。接收时,也要按这个顺序解析。我见过有人把CRC高低字节搞反,结果通信死活不通。

3.5 完整的帧结构图

下面我用一张SVG图,把整个帧结构串起来。你一看就明白了。

Modbus RTU 帧结构 地址域 1字节 功能码 1字节 数据域 N字节(可变) CRC校验 2字节 从站地址 1~247 操作类型 如0x03读寄存器 具体参数 地址、数量、数值 错误检测 CRC-16 注意:CRC在帧中为低字节在前,高字节在后 数据域为大端模式(高字节在前)

3.6 实际报文解析

光说不练假把式。咱们来看一个真实的报文:

请求:01 03 00 00 00 0A C5 CD
响应:01 03 14 00 01 00 02 ... 00 0A XX XX

我来拆解一下:

  • 01:从站地址,1号设备
  • 03:功能码,读保持寄存器
  • 00 00:起始地址,从0号寄存器开始
  • 00 0A:读10个寄存器
  • C5 CD:CRC校验(低字节C5在前,高字节CD在后)

响应报文:

  • 01:从站地址
  • 03:功能码
  • 14:数据字节数(20个字节,因为10个寄存器×2字节)
  • 00 01 00 02 ... 00 0A:寄存器值
  • XX XX:CRC校验

你看,是不是一目了然?

实用建议:调试时,我习惯先用串口助手抓一下报文。看看地址对不对,CRC对不对。很多时候,问题就出在这些细节上。

好了,Modbus RTU的帧结构就讲到这里。记住四个部分:地址域、功能码、数据域、CRC校验。每个部分都有它的规矩,错一个字节都不行。但只要你理解了,其实很简单。

专注资料整理