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。计算过程是这样的:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器的低8位异或
- 右移一位,如果移出的位是1,则与0xA001异或
- 重复8次,处理完一个字节
- 所有字节处理完后,CRC寄存器取反
- 低字节在前,高字节在后(注意!这是小端模式)
我给你一个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图,把整个帧结构串起来。你一看就明白了。
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校验。每个部分都有它的规矩,错一个字节都不行。但只要你理解了,其实很简单。