4、Modbus RTU帧结构:地址码、功能码、数据区、CRC校验详解

各位工程师朋友,咱们今天来聊聊Modbus RTU的帧结构。说实话,这玩意儿是现场总线里最基础、也最容易被忽视的部分。我见过不少项目,通讯不稳定,查到最后发现是帧结构没搞对。嗯,咱们今天就把它彻底讲透。

4.1 地址码:谁在说话?

地址码是帧的第一个字节,范围从1到247。0是广播地址,248到255保留。说白了,就是给每个从站发个身份证号。

地址码的作用很简单:

  • 主站发数据时,告诉所有从站「我在跟谁说话」
  • 从站回复时,告诉主站「是谁在回话」

关键点:地址码0是广播地址,所有从站都会接收,但不会回复。我刚开始做项目时,想用广播写参数,结果从站没反应,查了半天才发现广播模式下从站是不回应的。

你想想看,如果总线上挂了32个从站,地址设重了会怎样?轻则数据错乱,重则总线冲突。所以我的习惯是:设备上电后先读一遍所有地址,确认没有冲突再开始通讯。

4.2 功能码:要干什么?

功能码是第二个字节,告诉从站要执行什么操作。常用的就那几个:

功能码 含义 我常用的场景
01 读线圈状态 读取继电器输出
02 读离散输入 读取按钮状态
03 读保持寄存器 读取模拟量、参数
04 读输入寄存器 读取AD转换值
05 写单个线圈 控制单个继电器
06 写单个寄存器 设置参数
15 写多个线圈 批量控制输出
16 写多个寄存器 批量设置参数

我记得有一次,客户说设备偶尔会误动作。排查到最后,发现是功能码用错了——他想写单个线圈,结果用了功能码15(写多个线圈),数据格式不对,导致写到了别的地址上。所以啊,功能码选错了,后果很严重。

我的建议:写代码时,把功能码定义成宏或枚举,别直接写数字。比如 #define FUNC_READ_HOLDING_REG 0x03,这样一眼就能看出意图。

4.3 数据区:具体内容

数据区跟在功能码后面,长度可变。具体包含什么,取决于功能码。

举个例子,读保持寄存器(功能码03):

  • 起始地址:2字节,告诉从站从哪个寄存器开始读
  • 寄存器数量:2字节,告诉从站读多少个

比如要读地址100开始的5个寄存器,数据区就是:00 64 00 05。00 64是100的十六进制,00 05是5。

再举个例子,写单个寄存器(功能码06):

  • 寄存器地址:2字节
  • 要写的值:2字节

数据区长度不是固定的。读操作的数据区一般是4字节(地址+数量),写操作的数据区会包含要写的数据。我见过有人把数据区长度算错了,导致CRC校验怎么都对不上。

注意:Modbus RTU的数据区是大端模式(Big-Endian),高字节在前,低字节在后。比如要写值0x1234到寄存器,数据区是12 34,不是34 12。这个坑我踩过,当时用示波器抓了半天才发现字节顺序反了。

4.4 CRC校验:数据对不对?

CRC校验是帧的最后两个字节,用来检查数据在传输过程中有没有出错。Modbus RTU用的是CRC-16,多项式是0x8005。

CRC校验的计算过程:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC寄存器低8位异或
  3. 右移一位,如果移出的位是1,则与多项式异或
  4. 重复8次,处理完所有字节
  5. 最终结果取反,得到CRC值

嗯,听起来有点绕。直接看代码吧:

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

这段代码我用了快十年了,从来没出过问题。注意多项式0xA001是0x8005的反转,这是Modbus的标准做法。

重要:CRC校验是低字节在前,高字节在后。比如CRC算出来是0x1234,帧里先发0x34,再发0x12。这个顺序跟数据区正好相反,别搞混了。

4.5 完整的帧结构

把上面这些拼起来,一个完整的Modbus RTU帧就是:

字段 长度 说明
地址码 1字节 从站地址,1-247
功能码 1字节 操作类型
数据区 N字节 具体数据
CRC低字节 1字节 校验码低8位
CRC高字节 1字节 校验码高8位

举个例子,读地址1的从站,从寄存器0开始读2个寄存器:

01 03 00 00 00 02 C4 0B

01是地址,03是功能码,00 00是起始地址,00 02是数量,C4 0B是CRC。

调试技巧:我习惯用串口助手先发一帧数据,看从站有没有回复。如果没回复,先检查CRC对不对。很多在线CRC计算工具可以帮你验证,省得自己手算。

4.6 避坑指南

做Modbus RTU通讯,有几个坑我踩过,分享给大家:

  • 帧间隔时间:RTU要求帧与帧之间至少有3.5个字符时间的空闲。如果间隔太短,从站可能把两帧当成一帧处理。我曾经遇到过,把波特率设高了,帧间隔没调,结果数据全乱了。
  • 字节间间隔:同一帧内的字节间隔不能超过1.5个字符时间。如果超过,从站会认为帧结束了。这个在中断处理里要特别注意,别被高优先级任务打断了。
  • CRC校验不能省:有人觉得通讯环境好,就不做CRC校验。千万别!电磁干扰、线缆老化,什么情况都可能发生。CRC是最后一道防线。

好了,关于Modbus RTU帧结构就讲这么多。说白了,就是地址码告诉谁在说话,功能码告诉要干什么,数据区是具体内容,CRC保证数据没错。把这四个部分搞清楚了,Modbus RTU通讯就掌握了八成。