3. Modbus RTU协议详解:帧结构、功能码、CRC校验、报文实例解析
各位同学,咱们今天来啃一块硬骨头——Modbus RTU协议。说实话,这玩意儿在工业现场摸爬滚打这么多年,我敢说90%的串口通信设备都在用它。你想想看,一个1980年代诞生的协议,到现在还是工业界的"通用语言",这本身就说明了很多问题。
我个人习惯把Modbus RTU比作工业通信的"普通话"。不管你是PLC、变频器、还是智能仪表,只要支持Modbus,大家就能对话。嗯,这里要注意,RTU模式是二进制传输,和ASCII模式不一样,咱们今天只讲RTU。
3.1 帧结构:数据怎么打包?
先看帧结构。Modbus RTU的报文格式其实很简单,就四个部分:
| 地址域 | 功能码 | 数据域 | CRC校验 |
|---|---|---|---|
| 1字节 | 1字节 | N字节 | 2字节 |
说白了,就是一问一答的模式。主机发请求,从机回响应。我在项目中遇到过不少新手,上来就写代码,结果报文格式搞错了,设备死活没反应。其实你只要记住这个结构,后面就好办了。
地址域:从机地址,范围1-247。0是广播地址,248-255保留。我建议你从1开始分配,别用0,容易出乱子。
功能码:告诉从机要干什么。比如读线圈、读寄存器、写单个寄存器等等。后面会详细讲。
数据域:具体的数据内容。比如你要读哪个寄存器,读几个,或者要写什么值。
CRC校验:这个最关键。没有它,数据传错了你都不知道。我曾经吃过这个亏,后面会讲。
关键点:Modbus RTU的帧之间必须有3.5个字符时间的静默间隔。这个时间很重要,很多通信不稳定都是因为这个间隔没处理好。
3.2 功能码:能干什么?
功能码是Modbus的核心。常用的就那么几个,我列个表给你看:
| 功能码 | 名称 | 作用 | 我常用的场景 |
|---|---|---|---|
| 01 | 读线圈 | 读取DO状态 | 控制继电器 |
| 02 | 读离散输入 | 读取DI状态 | 读取按钮、传感器 |
| 03 | 读保持寄存器 | 读取模拟量数据 | 读取温度、压力值 |
| 04 | 读输入寄存器 | 读取只读模拟量 | 读取设备版本号 |
| 05 | 写单个线圈 | 控制单个DO | 启动/停止设备 |
| 06 | 写单个寄存器 | 写入单个模拟量 | 设定目标温度 |
| 15 | 写多个线圈 | 批量控制DO | 批量复位 |
| 16 | 写多个寄存器 | 批量写入模拟量 | 批量设定参数 |
你想想看,实际项目中80%的情况,用03和06这两个功能码就够了。读数据用03,写数据用06。简单粗暴,但有效。
3.3 CRC校验:数据不出错的保障
CRC校验,全称是循环冗余校验。说白了,就是给报文加个"指纹",接收方收到后验证一下指纹对不对。
Modbus RTU用的是CRC-16,多项式是0x8005。计算过程是这样的:
// CRC-16计算示例(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 = crc >> 1;
}
}
}
return crc;
}
我的经验:CRC计算时,注意字节顺序。Modbus RTU发送时,先发低字节,再发高字节。这个顺序搞反了,校验永远过不了。我曾经调试一个设备,花了整整一个下午才发现是这个顺序问题。
3.4 报文实例解析:手把手教你读懂报文
光说不练假把式。咱们来看几个真实报文。
实例1:读取保持寄存器(功能码03)
假设我们要读取从机地址1的设备,从寄存器地址0开始,读2个寄存器。
请求报文:
01 03 00 00 00 02 C4 0B
拆解一下:
- 01:从机地址
- 03:功能码(读保持寄存器)
- 00 00:起始寄存器地址(高字节在前)
- 00 02:读取数量(2个寄存器)
- C4 0B:CRC校验(低字节在前)
响应报文:
01 03 04 00 0A 00 14 7A 3E
拆解一下:
- 01:从机地址
- 03:功能码
- 04:数据字节数(2个寄存器×2字节=4字节)
- 00 0A:第一个寄存器的值(10)
- 00 14:第二个寄存器的值(20)
- 7A 3E:CRC校验
注意:响应报文中的字节数,是数据域的字节数,不是整个报文的长度。这个很多人会搞混。
实例2:写单个寄存器(功能码06)
我们要向从机地址1的设备,寄存器地址0写入值100(0x0064)。
请求报文:
01 06 00 00 00 64 48 0A
拆解:
- 01:从机地址
- 06:功能码(写单个寄存器)
- 00 00:寄存器地址
- 00 64:要写入的值(100)
- 48 0A:CRC校验
响应报文:
01 06 00 00 00 64 48 0A
注意看,写成功的响应报文和请求报文一模一样。这是Modbus的设计特点——回显确认。
避坑指南:我曾经遇到过一个问题,写寄存器时响应报文和请求报文完全一样,但设备实际没写入成功。后来发现是设备处于"写保护"模式。所以,写操作后最好再读一次确认。
3.5 异常响应:出错了怎么办?
设备不是永远都正常工作的。当从机收到错误请求时,会返回异常响应。
异常响应格式:
地址域 | 功能码+0x80 | 异常码 | CRC
常见的异常码:
| 异常码 | 含义 | 我遇到过的场景 |
|---|---|---|
| 01 | 非法功能码 | 设备不支持该功能 |
| 02 | 非法数据地址 | 寄存器地址超出范围 |
| 03 | 非法数据值 | 写入的值超出范围 |
| 04 | 从机设备故障 | 设备硬件异常 |
举个例子,如果请求读一个不存在的寄存器:
请求: 01 03 00 00 00 05 85 C9(读5个寄存器,但设备只有3个)
异常响应: 01 83 02 C0 F1
拆解:
- 01:从机地址
- 83:功能码03 + 0x80 = 0x83,表示异常
- 02:异常码02(非法数据地址)
- C0 F1:CRC校验
我的建议:写代码时一定要处理异常响应。很多工程师只处理正常响应,结果设备出问题了都不知道。我一般会在调试时打印所有原始报文,这样一眼就能看出问题。
3.6 知识体系总览
为了让你对本章内容有个整体认识,我画了一张图:
这张图把Modbus RTU的核心内容都串起来了。你仔细看看,帧结构是基础,功能码是操作指令,CRC是安全保障,报文实例是实战应用。四者缺一不可。
好了,这一章的内容就到这儿。记住,Modbus RTU不难,难的是细节。多动手抓包分析,比看十遍书都管用。
公众号:蓝海资料掘金营,微信deep3321