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协议知识体系 Modbus RTU协议 帧结构 功能码 CRC校验 报文实例解析 地址域 功能码 数据域 CRC校验 读操作 写操作 CRC-16算法 多项式0x8005 读寄存器 写寄存器 异常响应 核心:一问一答,主从通信

这张图把Modbus RTU的核心内容都串起来了。你仔细看看,帧结构是基础,功能码是操作指令,CRC是安全保障,报文实例是实战应用。四者缺一不可。

好了,这一章的内容就到这儿。记住,Modbus RTU不难,难的是细节。多动手抓包分析,比看十遍书都管用。


公众号:蓝海资料掘金营,微信deep3321