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)
计算步骤:
- 初始化CRC寄存器为0xFFFF
- 取第一个字节与CRC低8位异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次(一个字节处理完)
- 处理下一个字节,直到所有字节处理完
- 最终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有什么区别。