3. Modbus RTU协议详解:帧结构、功能码、CRC校验与从站配置

各位同学,今天我们进入Modbus RTU协议的核心部分。说实话,Modbus RTU在工业现场的地位,就像普通话在中国人里的地位——不是最先进的,但绝对是最通用的。我做了十几年自动化,从PLC到CNC,从变频器到传感器,几乎每个项目都离不开它。

3.1 Modbus帧结构——数据怎么打包?

Modbus RTU的帧结构其实很简单。它不像TCP/IP那么复杂,也没有那么多层。说白了,就是一串字节按固定格式排好队。

一个完整的Modbus RTU帧长这样:

| 从站地址 (1字节) | 功能码 (1字节) | 数据区 (N字节) | CRC校验 (2字节) |

嗯,这里要注意:数据区长度是可变的,具体取决于功能码。CRC校验是低字节在前,高字节在后。我刚开始学的时候,经常把CRC的字节顺序搞反,结果通信死活不通。后来养成了习惯,每次写完CRC都拿计算器验证一遍。

关键点:Modbus RTU是主从架构,一个主站最多带247个从站(地址1-247)。地址0是广播地址,所有从站都会接收,但不回复。

3.2 功能码详解——你让设备干什么?

功能码就是指令。你告诉从站:读这个、写那个。我按常用程度给大家排个序。

3.2.1 位操作类功能码

功能码 名称 作用 最大读写数量
01 (0x01) 读线圈状态 读取DO(数字量输出) 2000个位
02 (0x02) 读离散输入 读取DI(数字量输入) 2000个位
05 (0x05) 写单个线圈 控制单个DO 1个位
15 (0x0F) 写多个线圈 批量控制DO 1968个位

功能码01和02,我经常用在读取CNC的开关量信号上。比如机床的急停状态、门锁信号、润滑泵状态等。功能码05和15则用于控制——比如启动主轴、打开冷却液。

我的经验:写多个线圈(功能码15)时,数据区是按字节打包的。比如你要写10个线圈,数据区占2个字节(16位),多余的位填0。我曾经有个项目,因为没注意字节对齐,导致第9个线圈写不进去,排查了整整一下午。

3.2.2 字操作类功能码

功能码 名称 作用 最大读写数量
03 (0x03) 读保持寄存器 读取AO或参数(可读写) 125个寄存器
04 (0x04) 读输入寄存器 读取AI(只读) 125个寄存器
06 (0x06) 写单个寄存器 设置单个参数 1个寄存器
16 (0x10) 写多个寄存器 批量设置参数 123个寄存器

功能码03和04是我用得最多的。CNC的坐标位置、主轴转速、进给倍率,这些数据都通过03功能码读取。功能码06和16则用来修改参数——比如调整PID系数、修改加速度设置。

为什么会分03和04?你想想看,保持寄存器是可读可写的,输入寄存器是只读的。说白了,03是RAM,04是ROM。我在项目中遇到过有人用04功能码去写数据,结果设备没反应——因为输入寄存器根本不让写。

3.3 CRC校验计算——通信的“防伪码”

CRC校验是Modbus RTU的保命符。没有它,数据在传输过程中被干扰了都不知道。CRC-16/MODBUS的算法,说白了就是多项式除法。

标准多项式:0x8005(反转后是0xA001

计算步骤:

  1. 初始化CRC寄存器为0xFFFF
  2. 取第一个字节与CRC低8位异或
  3. 右移1位,如果移出的位是1,则与0xA001异或
  4. 重复8次(一个字节处理完)
  5. 处理下一个字节,直到所有字节处理完
  6. 最终CRC寄存器取反,得到校验值

我给大家一个C语言实现,这是我用了十年的代码,从来没出过问题:

unsigned int crc16_modbus(unsigned char *buf, int len) {
    unsigned int crc = 0xFFFF;
    int i, j;
    
    for (i = 0; i < len; i++) {
        crc ^= buf[i];
        for (j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}

避坑指南:我曾经在一个项目中,用查表法计算CRC,结果查表数据是从网上复制过来的,有一个字节错了。导致通信时好时坏,排查了三天。后来我改用实时计算法,虽然慢一点,但绝对可靠。对于PLC这种实时性要求高的场合,建议用查表法,但表一定要自己生成验证过。

3.4 从站地址配置——谁是谁?

从站地址是Modbus通信的身份证。每个从站必须有唯一的地址,范围1-247。地址0是广播,248-255保留。

配置方式一般有三种:

  • 拨码开关:最传统,最可靠。我习惯用二进制拨码,比如拨到1、3、5代表地址1+4+16=21。
  • 软件配置:通过面板或上位机设置。方便,但掉电后要确认是否保存。
  • 自动分配:有些新设备支持,但我个人不太信任,万一冲突了很难排查。

嗯,这里有个细节:地址冲突是Modbus通信最常见的故障之一。我有个血的教训——在一个大型项目中,两个变频器都设成了地址5,结果主站发指令时,两个从站同时回复,数据完全乱套。从那以后,我每次上电前都会用万用表量一下拨码开关的电压,确认地址无误。

建议:给每个从站贴标签,标注地址和功能。别信自己的记忆力,现场一忙起来什么都记不住。我现在的习惯是:设备清单+地址表+接线图,三样东西必须放在控制柜门内侧。

3.5 实战小例子——读一个CNC的坐标

假设我们要读取CNC的X轴坐标(保持寄存器地址1000),从站地址为1。

主站发送:01 03 03 E8 00 01 CRC_L CRC_H

  • 01:从站地址
  • 03:功能码(读保持寄存器)
  • 03 E8:寄存器起始地址(1000的十六进制)
  • 00 01:读取1个寄存器
  • CRC_L CRC_H:CRC校验(低字节在前)

从站回复:01 03 02 0A 8C CRC_L CRC_H

  • 01:从站地址
  • 03:功能码
  • 02:数据字节数(2字节)
  • 0A 8C:坐标值(2700,即27.00mm,假设精度为0.01mm)
  • CRC_L CRC_H:CRC校验

你看,就这么简单。但实际项目中,你可能会遇到各种问题——比如CRC算不对、地址冲突、波特率不匹配。别急,后面几章我会专门讲故障排查。

好了,这一章的内容就到这里。Modbus RTU的帧结构、功能码、CRC校验和从站配置,是后续所有通信编程的基础。下一章我们讲Modbus TCP,看看它和RTU有什么区别。