4、Modbus RTU协议:帧结构、地址码、功能码、数据区、CRC校验,主从通信机制
好,咱们今天聊聊Modbus RTU。这个协议在电力保护装置里,可以说是最常用的通信方式之一了。我最早接触它是在一个变电站的改造项目里,当时老设备只支持RTU模式,新设备却默认走TCP,折腾了好几天才把两边对上话。说白了,RTU就是串行链路上的“硬汉”,简单、可靠、不花哨。
4.1 帧结构——报文长什么样?
Modbus RTU的报文,没有起始位,也没有结束位。它靠的是“静默时间”来区分每一帧。你想想看,总线上大家都在听,谁先开口谁就是主站。从站收到完整的一帧后,必须在一个很短的间隔内回复。
一个标准的RTU帧,结构是这样的:
| 地址码 | 功能码 | 数据区 | CRC校验 |
| 1字节 | 1字节 | N字节 | 2字节 |
嗯,这里要注意:所有多字节数据,都是低字节在前,高字节在后。这个叫小端序。我见过不少新手在这里栽跟头,明明发了0x1234,对方却收到了0x3412,一脸懵。
帧间隔时间要求:
- 帧与帧之间:至少3.5个字符时间的静默
- 帧内字节之间:不超过1.5个字符时间
这两个时间参数,是RTU协议的命门。我曾经在一个项目中,因为从站的晶振偏差太大,导致字节间隔超时,通信时断时续。排查了整整两天,最后换了个晶振就解决了。
4.2 地址码——谁在说话?
地址码占1个字节,取值范围是0x01到0xF7(1~247)。0x00是广播地址,所有从站都要响应,但不需要回复。
我个人习惯,在配置保护装置时,会把地址码和屏柜编号对应起来。比如1号保护屏就用0x01,2号屏用0x02。这样调试时一眼就能看出来是哪个设备在通信。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00 | 广播地址 | 所有从站接收,不回复 |
| 0x01 ~ 0xF7 | 单播地址 | 唯一标识一个从站 |
| 0xF8 ~ 0xFF | 保留 | 协议保留,不要使用 |
注意:地址码0x00虽然可以广播,但很多保护装置并不支持广播写操作。我建议你只在读取参数时用广播,写操作一定要用单播,否则后果自负。
4.3 功能码——你想干什么?
功能码告诉从站:主站想读什么、写什么。常用的就那么几个,我列出来:
| 功能码 | 名称 | 作用 |
|---|---|---|
| 0x01 | 读线圈状态 | 读取DO(数字输出) |
| 0x02 | 读离散输入 | 读取DI(数字输入) |
| 0x03 | 读保持寄存器 | 读取AO或参数(最常用) |
| 0x04 | 读输入寄存器 | 读取AI(模拟量输入) |
| 0x05 | 写单个线圈 | 控制单个DO |
| 0x06 | 写单个寄存器 | 设置单个参数 |
| 0x0F | 写多个线圈 | 批量控制DO |
| 0x10 | 写多个寄存器 | 批量设置参数 |
你想想看,在保护装置里,最常用的就是0x03和0x06。读遥测、读遥信用0x03,写定值用0x06或0x10。功能码选错了,从站会返回异常码,告诉你“我不懂你在说什么”。
4.4 数据区——具体内容是什么?
数据区跟在功能码后面,长度可变。对于读操作,数据区包含起始地址和读取数量。对于写操作,数据区包含地址、数量和具体数值。
举个例子,我要读从站地址0x01的保持寄存器,从地址0x100开始,读2个寄存器:
请求帧:01 03 01 00 00 02 CRC_LO CRC_HI
| | | | |
地址 功能 起始地址高 起始地址低 数量高 数量低
从站回复:
响应帧:01 03 04 12 34 56 78 CRC_LO CRC_HI
| | | | |
地址 功能 字节数 寄存器1值 寄存器2值
这里寄存器1的值是0x1234,寄存器2的值是0x5678。注意,每个寄存器占2个字节,所以字节数=寄存器数量×2。
我的经验:在保护装置里,寄存器地址通常不是从0开始的。比如南瑞的装置,遥测地址可能从0x1000开始,遥信从0x2000开始。一定要看厂家的点表,别想当然。
4.5 CRC校验——数据可靠吗?
CRC校验是RTU协议的“守门员”。它用CRC-16算法,校验范围是从地址码到数据区的最后一个字节。校验码占2个字节,低字节在前。
计算步骤很简单:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器低8位异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次,处理完所有字节
- 最终结果就是CRC校验码
我一般用查表法,速度快很多。给你一个C语言的实现片段:
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;
}
避坑指南:我曾经遇到过一个问题,从站总是返回CRC错误。查了半天,发现是主站发送时,CRC字节的顺序搞反了。记住:RTU是低字节在前,高字节在后。如果你用现成的库,一定要确认它输出的字节序对不对。
4.6 主从通信机制——谁说了算?
Modbus RTU是典型的主从模式。总线上只有一个主站,多个从站。主站发起请求,从站响应。从站之间不能直接通信。
通信流程是这样的:
- 主站发送请求帧(包含目标从站地址)
- 所有从站都收到这个帧,但只有地址匹配的那个才会处理
- 从站执行操作,然后回复响应帧
- 如果从站没收到或CRC错误,就不回复
- 主站超时后,会重发请求(一般重试3次)
你想想看,这个机制简单到什么程度?主站就像老师点名,点到谁谁回答。没点到的人,只能听着。我见过有人想用RTU做多主站,结果总线乱成一锅粥,数据全错。
关键参数设置:
- 波特率:保护装置常用9600或19200,也有用38400的
- 数据位:8位
- 校验位:无校验或偶校验
- 停止位:1位或2位
- 超时时间:一般设为100ms~500ms
我建议你,在调试阶段把超时设长一点,比如500ms。等通信稳定了,再缩短到100ms。这样可以避免因为响应慢而误判为通信故障。
好了,Modbus RTU的核心内容就这些。说白了,它就是一套“问-答”的规矩。你只要把帧结构搭对了,地址码写对了,CRC算对了,通信就能跑起来。下一章咱们聊聊Modbus TCP,看看它和RTU有什么不同。