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校验的计算过程:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器低8位异或
- 右移一位,如果移出的位是1,则与多项式异或
- 重复8次,处理完所有字节
- 最终结果取反,得到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通讯就掌握了八成。