4. RTU帧结构详解:起始位/设备地址/功能码/数据区/CRC校验的字节排列规则、常见组包错误分析

RTU帧结构,说白了就是Modbus设备之间通信的「信封」。你写的每个字节、每个位,都必须按规矩放好。否则对方设备根本看不懂你在说什么。

我记得刚入行那会儿,调试一个变频器,死活收不到回应。折腾了一下午,最后发现是CRC校验字节的顺序搞反了。嗯,这种低级错误,我相信不少人都犯过。

4.1 RTU帧的完整结构

一个标准的RTU帧,由以下四个部分组成:

字段 长度 说明
设备地址 1字节 0x01~0xF7,0x00为广播地址
功能码 1字节 如0x03读保持寄存器,0x06写单个寄存器
数据区 N字节 根据功能码不同,长度可变
CRC校验 2字节 低字节在前,高字节在后

你想想看,整个帧没有起始位和停止位标记。那设备怎么知道一帧从哪里开始、到哪里结束?

答案很简单:靠时间间隔。RTU协议规定,如果总线空闲时间超过3.5个字符的传输时间,就认为上一帧结束,下一帧开始。我个人习惯把这个时间叫做「帧间隔」,在9600波特率下大约是3.5ms。

核心要点:RTU帧没有显式的起始/停止字节,完全靠静默时间来分割帧。这是初学者最容易忽略的地方。

4.2 设备地址:谁在说话?

设备地址占1个字节,取值范围是0x01到0xF7。0x00是广播地址,所有从站都要处理,但不回应。

我在项目中遇到过一个问题:一个从站设备地址设成了0x00,结果主站发什么它都响应,但响应内容全是乱的。查了半天才发现是地址冲突。

这里有个坑:地址0xF8~0xFF是保留地址,不要用。我曾经见过有人把地址设成0xFF,结果设备完全不工作——因为0xFF在某些实现中被当作「无效地址」处理。

4.3 功能码:你要干什么?

功能码告诉从站:你要读数据,还是要写数据?常用的就那么几个:

  • 0x01:读线圈状态
  • 0x02:读离散输入
  • 0x03:读保持寄存器(最常用)
  • 0x04:读输入寄存器
  • 0x05:写单个线圈
  • 0x06:写单个寄存器
  • 0x0F:写多个线圈
  • 0x10:写多个寄存器

如果从站返回的响应中,功能码的最高位被置1(比如0x83),那就表示发生了异常。这是Modbus的异常响应机制,说白了就是「你刚才的命令我没法执行」。

个人经验:调试时,先确认功能码对不对。我见过太多人把0x03和0x04搞混,读保持寄存器用了读输入寄存器的功能码,结果数据全是0。

4.4 数据区:具体怎么干?

数据区的格式取决于功能码。以最常用的0x03(读保持寄存器)为例:

请求帧:

设备地址: 0x01
功能码:   0x03
起始地址: 0x00 0x00  (高字节在前)
寄存器数量: 0x00 0x0A  (高字节在前,读10个寄存器)
CRC校验:  0x?? 0x??  (低字节在前)

响应帧:

设备地址: 0x01
功能码:   0x03
字节数:   0x14  (20个字节,10个寄存器×2字节)
数据:     0x?? 0x?? ... (每个寄存器高字节在前)
CRC校验:  0x?? 0x??

注意看:多字节数据都是大端模式,也就是高字节在前。这是Modbus协议的规定,和你的CPU是大端还是小端没关系。我在做嵌入式移植时,经常需要手动做字节序转换:

// 大端转小端示例
uint16_t modbus_to_host(uint8_t high, uint8_t low) {
    return (high << 8) | low;
}

4.5 CRC校验:最后的防线

CRC校验是整个帧的「防伪标签」。它计算从设备地址到数据区最后一个字节的所有内容,生成一个16位的校验值。

CRC的排列规则:

  • CRC占2个字节
  • 低字节在前,高字节在后
  • 例如:CRC计算结果是0x8001,则先发0x01,再发0x80

这个顺序和前面数据区的大端模式正好相反。我当年第一次写Modbus驱动时,就在这里栽了跟头。CRC算对了,但字节顺序放反了,结果从站一直报错。

重要提醒:CRC的字节顺序是低字节在前,和数据区的高字节在前完全相反。这是Modbus RTU最容易出错的地方,没有之一。

4.6 常见组包错误分析

这些年我调试过的Modbus设备少说也有上百种,总结下来,组包错误主要集中在以下几个方面:

错误一:CRC计算范围搞错

CRC只计算设备地址、功能码和数据区,不包含CRC本身。有人把CRC的两个字节也算进去了,结果永远对不上。

错误二:字节顺序搞反

数据区是大端(高字节在前),CRC是小端(低字节在前)。这两个规则记混了,数据就读不对。

错误三:帧间隔时间不对

RTU靠静默时间分割帧。如果两个字节之间的间隔超过了1.5个字符时间,从站会认为帧不完整,直接丢弃。我曾经在115200波特率下遇到过这个问题,间隔时间太短,稍微有点中断就超时了。

错误四:寄存器地址偏移

Modbus协议中,寄存器地址从0开始。但很多PLC的组态软件中,地址显示从1开始。比如你要读地址40001,实际发送的地址是0x0000。这个偏移量搞错了,读出来的数据驴唇不对马嘴。

避坑指南:我曾经调试一个温控器,读出来的温度一直是0。查了三天,最后发现是寄存器地址偏移了1。从那以后,我每次都会先读一个已知值的寄存器来验证地址映射是否正确。

4.7 一个完整的组包示例

假设我们要读取设备地址0x01的保持寄存器,从地址0x0000开始,读2个寄存器:

步骤1:组装数据
  设备地址: 0x01
  功能码:   0x03
  起始地址: 0x00 0x00
  寄存器数: 0x00 0x02

步骤2:计算CRC
  对 01 03 00 00 00 02 计算CRC16
  得到 CRC = 0xC40B

步骤3:按顺序发送
  01 03 00 00 00 02 0B C4
  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑
  地址 功能码 起始地址 数量 CRC低 CRC高

注意最后两个字节:CRC是0xC40B,先发低字节0x0B,再发高字节0xC4。这个顺序千万别搞反了。

4.8 知识体系结构图

下面这张图,把RTU帧的整个组包和解包流程画清楚了:

RTU帧结构组包/解包流程 RTU帧结构 设备地址 (1字节) 功能码 (1字节) 数据区 (N字节) CRC (2字节) 组包流程: 1. 确定设备地址 2. 选择功能码 3. 填充数据区 4. 计算CRC 解包流程: 1. 接收完整帧 2. 验证CRC 3. 检查地址 4. 解析数据 常见错误: • CRC计算范围错误 • 字节顺序搞反 • 帧间隔时间不对 • 寄存器地址偏移

这张图把组包和解包的流程都画出来了。左边是组包,右边是解包。你照着这个流程走,基本不会出错。

我的习惯:每次写完组包代码,我都会先用一个已知的Modbus调试工具(比如Modbus Poll)验证一下。发一个读命令,看看返回的数据对不对。这一步能省掉后面大量的排查时间。

RTU帧结构,说白了就是「地址+功能码+数据+CRC」这四个部分。只要把每个部分的字节顺序和计算规则记清楚,组包和解包其实很简单。嗯,多练几次就熟了。


公众号:蓝海资料掘金营,微信deep3321