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个字节,低字节在前。

计算步骤很简单:

  1. 初始化CRC寄存器为0xFFFF
  2. 对每个字节,与CRC寄存器低8位异或
  3. 右移1位,如果移出的位是1,则与0xA001异或
  4. 重复8次,处理完所有字节
  5. 最终结果就是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是典型的主从模式。总线上只有一个主站,多个从站。主站发起请求,从站响应。从站之间不能直接通信。

通信流程是这样的:

  1. 主站发送请求帧(包含目标从站地址)
  2. 所有从站都收到这个帧,但只有地址匹配的那个才会处理
  3. 从站执行操作,然后回复响应帧
  4. 如果从站没收到或CRC错误,就不回复
  5. 主站超时后,会重发请求(一般重试3次)

你想想看,这个机制简单到什么程度?主站就像老师点名,点到谁谁回答。没点到的人,只能听着。我见过有人想用RTU做多主站,结果总线乱成一锅粥,数据全错。

关键参数设置:

  • 波特率:保护装置常用9600或19200,也有用38400的
  • 数据位:8位
  • 校验位:无校验或偶校验
  • 停止位:1位或2位
  • 超时时间:一般设为100ms~500ms

我建议你,在调试阶段把超时设长一点,比如500ms。等通信稳定了,再缩短到100ms。这样可以避免因为响应慢而误判为通信故障。

好了,Modbus RTU的核心内容就这些。说白了,它就是一套“问-答”的规矩。你只要把帧结构搭对了,地址码写对了,CRC算对了,通信就能跑起来。下一章咱们聊聊Modbus TCP,看看它和RTU有什么不同。