3. Modbus RTU帧结构:地址码、功能码、数据区、CRC校验的详细拆解
各位工程师朋友,咱们今天来聊聊Modbus RTU的帧结构。说白了,这就是一串字节在线上怎么排兵布阵的问题。我当年刚接触时,觉得这东西不就是几个字节嘛,有啥好研究的?结果第一次调试就栽了跟头——CRC算错了,设备死活不响应。嗯,从那以后,我老老实实把每个字节都掰开揉碎了看。
3.1 帧结构总览:一条消息的完整骨架
一个完整的Modbus RTU帧,就像一列火车。车头是地址码,车厢是功能码和数据区,车尾是CRC校验。少了哪一节,这趟车都开不了。
标准帧结构(共N字节):
| 地址码 (1字节) | 功能码 (1字节) | 数据区 (N-4字节) | CRC校验 (2字节) |
最小帧: 4字节(地址+功能+CRC,无数据)
最大帧: 256字节(RTU协议规定)
我个人习惯把帧结构画成一张图,这样看着更直观。下面这张SVG图,就是我当年做项目时手绘的电子版——它把帧的每个部分都标得清清楚楚。
3.2 地址码:谁在说话?
地址码占1个字节,取值范围是0~247。但这里有个坑——0是广播地址,所有从机都得听,但不准回复。我见过有人把从机地址设成0,结果主机收不到任何响应,查了半天才发现是地址冲突。
实用建议:
- 从机地址范围:1~247(0是广播,248~255保留)
- 同一个总线上,地址必须唯一。我曾经在项目里用DIP开关设置地址,结果两个设备都拨到了5,数据全乱套了。
- 地址0的广播帧,从机只收不回。你想想看,如果所有从机同时回复,总线不就炸了吗?
3.3 功能码:你想干啥?
功能码也是1个字节。它告诉从机:你要读还是写?读什么?写什么?常用的就那么几个,我列个表给你看。
| 功能码 | 名称 | 作用 | 我常用的场景 |
|---|---|---|---|
| 0x01 | 读线圈 | 读取DO状态 | 控制继电器反馈 |
| 0x02 | 读离散输入 | 读取DI状态 | 读取按钮、限位开关 |
| 0x03 | 读保持寄存器 | 读取AI/参数 | 读取电表电压、电流 |
| 0x04 | 读输入寄存器 | 读取AI(只读) | 读取传感器原始值 |
| 0x05 | 写单个线圈 | 控制单个DO | 启动/停止电机 |
| 0x06 | 写单个寄存器 | 设置单个参数 | 修改设定值 |
| 0x0F | 写多个线圈 | 批量控制DO | 一键复位所有输出 |
| 0x10 | 写多个寄存器 | 批量设置参数 | 下载配置表 |
注意: 功能码0x03和0x04容易搞混。我刚开始做项目时,用0x03去读一个只读的输入寄存器,从机返回了异常码0x01(非法功能码)。后来才明白——0x04才是专门读输入寄存器的。记住了:保持寄存器可读可写,输入寄存器只读。
3.4 数据区:干货都在这里
数据区是帧里最灵活的部分。它可以是0字节(比如只读一个状态),也可以长达252字节。数据区的格式取决于功能码。
举个例子,用功能码0x03读保持寄存器时,数据区是这样的:
请求帧:| 地址 0x01 | 功能 0x03 | 起始地址高 0x00 | 起始地址低 0x6E | 寄存器数高 0x00 | 寄存器数低 0x02 | CRC低 | CRC高 |
响应帧:| 地址 0x01 | 功能 0x03 | 字节数 0x04 | 数据高 0x12 | 数据低 0x34 | 数据高 0x56 | 数据低 0x78 | CRC低 | CRC高 |
你注意看,请求帧里数据区是4个字节(起始地址+寄存器数量),响应帧里数据区是5个字节(字节数+数据)。这就是为什么我说数据区长度是动态的——它取决于你要读多少数据。
避坑指南: 我曾经在读取电表数据时,一次读了120个寄存器。结果从机返回了240字节的数据,加上地址码、功能码、CRC,总帧长达到了244字节。差点就超了256字节的上限。所以,批量读取时一定要算好总长度。
3.5 CRC校验:最后的守护神
CRC校验占2个字节,采用CRC-16算法。它是整个帧的「指纹」——只要有一个bit传错了,CRC就对不上,从机就会丢弃这帧数据。
CRC的计算过程是这样的:
- 初始化CRC寄存器为0xFFFF
- 对每个字节(地址码、功能码、数据区)进行异或和移位操作
- 最后得到的16位值,低字节在前,高字节在后
我给你一个C语言的CRC计算函数,这是我项目里一直在用的,经过无数次验证:
unsigned int crc16_modbus(unsigned char *buf, int len) {
unsigned int crc = 0xFFFF;
for (int i = 0; i < len; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
重要提醒: CRC在帧里是低字节在前,高字节在后。比如计算出的CRC是0x1234,那么帧里先发0x34,再发0x12。这个顺序搞反了,设备就不认。我当年第一次写驱动时,就栽在这个小细节上,折腾了一整天。
3.6 帧间间隔:别让数据「粘」在一起
RTU协议没有起始位和停止位,它靠时间间隔来区分帧。标准规定:帧间间隔必须大于等于3.5个字符时间。
什么是3.5个字符时间?以9600波特率、8数据位、1停止位、无校验为例:
1个字符时间 = (1起始位 + 8数据位 + 1停止位) / 9600 = 1.04ms
3.5个字符时间 ≈ 3.64ms
也就是说,如果总线上超过3.64ms没有数据,从机就认为上一帧结束了,新来的数据是新的一帧。
我曾经踩过的坑: 有一次用USB转485的廉价转换器,它的发送间隔不稳定,有时候只有2ms。结果从机把两帧数据当成一帧处理,CRC校验当然过不了。后来换了工业级的转换器,问题就解决了。所以,硬件质量真的很重要。
3.7 异常响应帧:出错了怎么办?
如果从机收到一个错误的请求(比如功能码不支持、地址越界),它会返回一个异常帧。异常帧的格式是:
| 地址码 | 功能码 + 0x80 | 异常码 | CRC |
常见的异常码有:
| 异常码 | 含义 | 常见原因 |
|---|---|---|
| 0x01 | 非法功能码 | 从机不支持该功能 |
| 0x02 | 非法数据地址 | 寄存器地址超出范围 |
| 0x03 | 非法数据值 | 写入的数据超出范围 |
| 0x04 | 从机设备故障 | 硬件异常 |
你想想看,如果主机发了0x03读寄存器,从机回复0x83 + 0x02,意思就是:「你让我读的地址不存在,我办不到。」这时候你就得去查查寄存器地址表了。
好了,Modbus RTU帧结构的每个部分,咱们都拆解了一遍。从地址码到CRC,从正常帧到异常帧,该说的我都说了。记住一句话:帧结构是协议的基础,基础不牢,地动山摇。下次调试时,拿个串口抓包工具,对着帧结构一个一个字节看,你会有新的收获。
公众号:蓝海资料掘金营,微信deep3321