Modbus 学习笔记(第二部分)Modbus RTU 通信报文分析

上一部分我们知道了 Modbus 的基本概念:主站和从站、四个存储区、功能码、地址模型。这一部分,我们开始”动真格”:把 Modbus RTU 的报文一条一条地拆开看。

这一部分可以说是整个 Modbus 学习里最重要、最核心的一部分。为什么这么说?因为你以后用 C# 写串口助手,本质就是两件事:照着规矩把报文拼出来发出去,再照着规矩把收到的报文拆开读懂。所以每一个功能码的报文长什么样,你必须烂熟于心。

好消息是,Modbus RTU 的报文结构非常规整,学会了第一个,后面的都是”换汤不换药”。我会带着你一个功能码一个功能码地拆,每拆一条都给你配一张字节对照表,确保你彻底看懂。

先记住一个贯穿全篇的结论:所有 Modbus RTU 报文,无论读写,都由”从站地址 + 功能码 + 数据 + CRC 校验”这四大块组成。 下面我们就从这里开始。


2.1 Modbus RTU 通信格式说明

2.1.1 一条 RTU 报文的整体结构

一条完整的 Modbus RTU 报文,从头到尾依次是这四个部分:

顺序 部分 长度 说明
1 从站地址 1 字节 表示这条消息是发给谁(或谁回的)
2 功能码 1 字节 表示要做什么操作
3 数据区 不定 具体内容,读和写各不相同
4 CRC 校验 2 字节 用来检查报文有没有传错

用一张图来记,就是:

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

下面逐个说清楚每个部分。

2.1.2 从站地址(1 个字节)

从站地址就是”这条消息找谁”的门牌号。每个从站都有一个唯一的地址,范围是 1 到 247。

  • 主站发报文时,在这个位置填”目标从站”的地址;
  • 从站回报文时,在这个位置填”自己”的地址,让主站知道是谁回的话。

有两个特殊地址要记住:

  • 0:广播地址。主站发广播时,所有从站都会收到并执行,但都不回话。广播只能用于”写”,不能用于”读”(因为没人回,读了也没用)。
  • 248 到 255:保留地址,正常不用。

我们这一部分的案例里,从站地址会出现 1、2、3、4,你对应着看就行。

2.1.3 功能码(1 个字节)

功能码我们在第一部分已经讲过了,就是”行为的代号”。这里再巩固一下对应关系:

功能 功能码
读输出线圈 01
读输入线圈 02
读保持寄存器 03
读输入寄存器 04
写单线圈 05
写单寄存器 06
写多线圈 0F
写多寄存器 10

这 8 个功能码,就是这一部分要逐条拆解的全部内容。

2.1.4 数据区(长度不定)

数据区是报文的”正文”,读和写的内容完全不一样。比如读的时候,要告诉对方”从哪开始读、读几个”;写的时候,要告诉对方”写到哪、写什么值”。每个功能码的数据区格式,我们在 2.2 到 2.9 里逐一展开,这里先不展开。

2.1.5 CRC 校验(2 个字节)

CRC 校验码的作用是”防错”。报文在传输过程中,可能因为干扰等原因出现错误(比如本来该是 0 的位变成了 1)。有了 CRC,接收方收到报文后,把数据部分重新算一遍 CRC,再跟报文末尾带的 CRC 对比:一致,说明没传错,可以处理;不一致,说明传错了,直接丢弃。

关于 CRC,你现在只需要记住三点:

  1. 它是 2 个字节,放在报文的最末尾;
  2. 它覆盖的范围是”从站地址 + 功能码 + 数据区”,也就是除了 CRC 自己之外的所有字节;
  3. **发送时是”低字节在前、高字节在后”**。这个顺序特别重要,很多新手写代码时把高低字节搞反了,导致报文一直校验不过。

打个比方:如果算出来的 CRC 是十六进制 BC 0D(高位 BC、低位 0D),那么放在报文末尾时,要写成 0D BC 的顺序(低位在前)。我们后面每个案例里看到的 CRC 都是”已经按低字节在前”排列好的,你照着抄就行。

CRC 的高低字节顺序,是将来你写代码时最容易踩的坑:计算出来的结果必须先”低字节在前”地放进报文末尾。顺序一旦搞反,从站怎么校验都通不过,通信就会一直失败。现在先记住这个规矩,等后面写串口助手时我们会专门再练。

2.1.6 两个贯穿全篇的”顺序”规矩

在正式拆报文之前,先把两个最容易搞错的”顺序”定下来,后面每个功能码都会用到:

第一个:寄存器数据是”高字节在前”。

一个寄存器是 16 位,也就是 2 个字节。传输的时候,先传高字节(高位),再传低字节(低位)。比如数值 100,十六进制是 00 64(00 是高字节,64 是低字节),报文里就按 00 64 这个顺序写。

第二个:线圈数据是”低位在前”。

读线圈时,8 个线圈被打包进 1 个字节。这个字节里,最低位(bit 0)对应第一个线圈,也就是地址最小的那个线圈。这个跟寄存器的”高字节在前”是反着的,是新手最容易踩的坑,我们在 2.2 里会详细演示。


2.2 功能码 01 读取输出线圈

2.2.1 报文格式

发送报文(主站发给从站):

1
从站地址 + 01 + 起始线圈地址 + 线圈数量 + CRC校验
  • 起始线圈地址:2 字节,是相对地址(从 0 开始数),表示从哪个线圈开始读;
  • 线圈数量:2 字节,表示要读几个线圈。

接收报文(从站回给主站):

1
从站地址 + 01 + 字节计数 + 具体数据 + CRC校验
  • 字节计数:1 字节,表示后面”具体数据”有几个字节;
  • 具体数据:把每个线圈的状态(1 或 0)按位打包成的字节。

2.2.2 案例:读取 1 号从站、从 0 开始的 10 个输出线圈

主站发送:

1
01 01 00 00 00 0A BC 0D

一个字节一个字节拆开看:

字节 数值 含义
第 1 个字节 01 从站地址,问 1 号从站
第 2 个字节 01 功能码,读取输出线圈
第 3、4 个字节 00 00 起始线圈地址(相对地址 0,即从第 1 个线圈开始)
第 5、6 个字节 00 0A 线圈数量,10 个
第 7、8 个字节 BC 0D CRC 校验码(低字节在前)

用大白话翻译这条发送报文:

“1 号从站,请把你输出线圈里,从第 1 个开始、一共 10 个线圈的状态告诉我。”

从站接收:

1
01 01 02 55 01 47 6C

继续拆:

字节 数值 含义
第 1 个字节 01 从站地址,1 号从站回话
第 2 个字节 01 功能码,读输出线圈的回应
第 3 个字节 02 字节计数,后面有 2 个字节的数据
第 4、5 个字节 55 01 10 个线圈的状态,打包成 2 个字节
第 6、7 个字节 47 6C CRC 校验码

2.2.3 重点:数据 55 01 是怎么表示 10 个线圈的

这是本节最关键的地方,我们慢一点,把 55 01 这两个字节拆到底。

先记住规则:线圈数据打包时,8 个线圈放进 1 个字节,最低位(bit 0)对应地址最小的那个线圈。 10 个线圈需要 2 个字节(前 8 个线圈 1 字节,后 2 个线圈 1 字节),所以字节计数是 02。

第一个字节 55

十六进制 55 换算成二进制是 0101 0101。把这 8 位从低到高(bit 0 到 bit 7)列出来:

bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
0 1 0 1 0 1 0 1

因为 bit 0 对应”第 1 个线圈”(地址最小的那个),所以从 bit 0 往上看,8 个线圈的状态依次是:

第 1 个线圈 = 1(开)、第 2 个 = 0(关)、第 3 个 = 1、第 4 个 = 0、第 5 个 = 1、第 6 个 = 0、第 7 个 = 1、第 8 个 = 0。

第二个字节 01

十六进制 01 换算成二进制是 0000 0001,只有 bit 0 是 1。它表示剩下的第 9、10 个线圈:

第 9 个线圈 = 1(开)、第 10 个线圈 = 0(关)。

把 10 个线圈连起来看,状态依次是:

1
开 关 开 关 开 关 开 关 开 关

用数字表示就是:

1
1 0 1 0 1 0 1 0 1 0

到这里,你就完整地读懂了这条报文:1 号从站的这 10 个输出线圈,第 1、3、5、7、9 个是开的,第 2、4、6、8、10 个是关的。

再强调一遍那个坑:线圈是”低位在前”,bit 0 对应第一个线圈。而寄存器是”高字节在前”。这两个顺序是反的,读线圈时千万别按”高位在前”去理解,否则状态会整个翻过来、全错。

这里务必再敲一次黑板:线圈是”低位在前”(bit 0 对应第一个线圈),寄存器是”高字节在前”。这两个顺序是反的,是新手最常犯的错。读线圈数据时,如果你按”高位在前”去理解,10 个线圈的状态会整个翻过来,全错。


2.3 功能码 02 读取输入线圈

2.3.1 报文格式

功能码 02 和功能码 01 长得几乎一模一样,唯一的区别就是:01 读的是”输出线圈”(0 区),02 读的是”输入线圈”(1 区)。报文结构完全一样:

发送报文:

1
从站地址 + 02 + 起始线圈地址 + 线圈数量 + CRC16

接收报文:

1
从站地址 + 02 + 字节计数 + 具体数据 + CRC16

所以这一节你不需要记新格式,只要把 2.2 里学的”线圈打包规则”再练一遍就行。

功能码 02 和 01 的唯一区别,就是读的存储区不同:01 读输出线圈(0 区),02 读输入线圈(1 区)。报文结构、线圈打包规则完全一样。以后再看到 02 的报文,直接套 01 的拆法就行。

2.3.2 案例:读取 2 号从站、从 10 开始的 16 个输入线圈

主站发送:

1
02 02 00 0A 00 10 59 F7

拆解:

字节 数值 含义
第 1 个字节 02 从站地址,问 2 号从站
第 2 个字节 02 功能码,读取输入线圈
第 3、4 个字节 00 0A 起始线圈地址(相对地址 10,即从第 11 个线圈开始)
第 5、6 个字节 00 10 线圈数量,16 个
第 7、8 个字节 59 F7 CRC 校验码

注意这里起始地址是 00 0A,也就是十进制的 10。因为相对地址从 0 开始数,所以相对地址 10 实际上对应”第 11 个线圈”。这个概念第一部分讲过,这里再巩固一次。

从站接收:

1
02 02 02 15 00 F3 28

拆解:

字节 数值 含义
第 1 个字节 02 从站地址
第 2 个字节 02 功能码,读输入线圈的回应
第 3 个字节 02 字节计数,后面 2 个字节数据
第 4、5 个字节 15 00 16 个线圈的状态,打包成 2 个字节
第 6、7 个字节 F3 28 CRC 校验码

2.3.3 拆解数据 15 00

16 个线圈需要 2 个字节(16 除以 8 正好 2 个),所以字节计数是 02。

第一个字节 15

十六进制 15 换算成二进制是 0001 0101,从低到高是 bit 0 到 bit 7:

bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
0 0 0 1 0 1 0 1

bit 0 对应第一个线圈(这里是相对地址 10,即”第 11 个线圈”),往上依次排,前 8 个线圈的状态是:

第 11 个 = 1、第 12 个 = 0、第 13 个 = 1、第 14 个 = 0、第 15 个 = 1、第 16 个 = 0、第 17 个 = 0、第 18 个 = 0。

第二个字节 00

十六进制 00 就是 0000 0000,8 位全是 0,所以剩下的 8 个线圈(第 19 到第 26 个)全部是关(0)。

把 16 个线圈连起来看:

1
1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0

到这里,功能码 02 你就掌握了。它和 01 的唯一区别就是读的存储区不同,报文套路完全相同。


2.4 功能码 03 读取保持型寄存器

2.4.1 报文格式

功能码 03 是工控里使用频率最高的功能码,读模拟量、读参数基本全靠它,务必彻底吃透。

发送报文:

1
从站地址 + 03 + 起始寄存器地址 + 寄存器数量 + CRC16

接收报文:

1
从站地址 + 03 + 字节计数 + 具体数据 + CRC16

这里有一个和线圈不同的关键点:寄存器是 16 位的,一个寄存器占 2 个字节。 所以接收报文里的”字节计数”,永远等于”寄存器数量 乘以 2”。

2.4.2 案例:读取 3 号从站、从 5 开始的 2 个寄存器

主站发送:

1
03 03 00 05 00 02 D5 E8

拆解:

字节 数值 含义
第 1 个字节 03 从站地址,问 3 号从站
第 2 个字节 03 功能码,读取保持寄存器
第 3、4 个字节 00 05 起始寄存器地址(相对地址 5,即第 6 个寄存器)
第 5、6 个字节 00 02 寄存器数量,2 个
第 7、8 个字节 D5 E8 CRC 校验码

翻译成大白话:

“3 号从站,请把你保持寄存器里,从第 6 个开始、一共 2 个寄存器的值告诉我。”

从站接收:

1
03 03 04 00 64 00 C8 99 BA

拆解:

字节 数值 含义
第 1 个字节 03 从站地址
第 2 个字节 03 功能码,读保持寄存器的回应
第 3 个字节 04 字节计数,后面 4 个字节数据
第 4、5 个字节 00 64 第 1 个寄存器的值,十进制 100
第 6、7 个字节 00 C8 第 2 个寄存器的值,十进制 200
第 8、9 个字节 99 BA CRC 校验码

2.4.3 读懂数据部分

这条回应里有 2 个寄存器,所以数据是 4 个字节,字节计数是 04(2 乘以 2)。

  • 第一个寄存器:00 64,十六进制 64 就是十进制 100;
  • 第二个寄存器:00 C8,十六进制 C8 就是十进制 200。

注意这里每个寄存器都是”高字节在前”:00 是高字节,64 是低字节,合起来是十六进制 0064,等于十进制 100。

到这里,功能码 03 你就彻底掌握了。记住这句核心:接收报文里,字节计数 = 寄存器数量 × 2,每个寄存器 2 字节、高字节在前。

功能码 03 是工控里用得最多的功能码,这条核心规律务必背下来:接收报文里,字节计数 = 寄存器数量 × 2;每个寄存器占 2 个字节,且高字节在前。以后你写程序解析数据,就是靠这条规律把收到的字节两两一组还原成数字的。


2.5 功能码 04 读取输入寄存器

2.5.1 报文格式

功能码 04 和 03 的关系,跟 02 和 01 的关系一样:格式完全相同,只是读的存储区不同。03 读保持寄存器(4 区),04 读输入寄存器(3 区)。

发送报文:

1
从站地址 + 04 + 起始寄存器地址 + 寄存器数量 + CRC16

接收报文:

1
从站地址 + 04 + 字节计数 + 具体数据 + CRC16

2.5.2 案例:读取 4 号从站、从 10 开始的 5 个输入寄存器

主站发送:

1
04 04 00 0A 00 05 10 5E

拆解:

字节 数值 含义
第 1 个字节 04 从站地址,问 4 号从站
第 2 个字节 04 功能码,读取输入寄存器
第 3、4 个字节 00 0A 起始寄存器地址(相对地址 10,即第 11 个寄存器)
第 5、6 个字节 00 05 寄存器数量,5 个
第 7、8 个字节 10 5E CRC 校验码

从站接收:

1
04 04 0A 00 01 00 02 00 03 00 04 00 05 36 EA

拆解:

字节 数值 含义
第 1 个字节 04 从站地址
第 2 个字节 04 功能码,读输入寄存器的回应
第 3 个字节 0A 字节计数,后面 10 个字节数据
第 4 到 13 个字节 00 01 00 02 00 03 00 04 00 05 5 个寄存器的值
第 14、15 个字节 36 EA CRC 校验码

2.5.3 读懂数据部分

5 个寄存器,所以数据是 10 个字节,字节计数是 0A(十六进制的 10,也就是 5 乘以 2)。

把这 10 个字节两两一组拆开:

字节 十进制值 对应寄存器
第 1 组 00 01 1 第 11 个寄存器(相对地址 10)
第 2 组 00 02 2 第 12 个寄存器
第 3 组 00 03 3 第 13 个寄存器
第 4 组 00 04 4 第 14 个寄存器
第 5 组 00 05 5 第 15 个寄存器

所以这 5 个输入寄存器的值分别是 1、2、3、4、5。这条案例选得很友好,数据正好是 1 到 5,方便你一眼对照。

到这里,四个”读”功能码(01、02、03、04)就全部学完了。你会发现它们的套路完全一致:发送时给”起始地址 + 数量”,接收时给”字节计数 + 数据”。下面我们进入”写”功能码。


2.6 功能码 05 写入单输出线圈

2.6.1 报文格式

功能码 05 用来控制某一个线圈的开关。它的一个特点很特别:从站回应时,会把主站发来的报文原封不动地再发回来,这种回应方式叫”原报文返回”。所以发送报文和接收报文长得一模一样。

发送报文:

1
从站地址 + 05 + 线圈地址 + 写入值 + CRC16

接收报文(原样返回):

1
从站地址 + 05 + 线圈地址 + 写入值 + CRC16

2.6.2 重点:写入值只有两种

“写入值”这个字段是 2 个字节,但它不表示”线圈的状态是 0 或 1”,而是用两个固定的组合来表示开和关:

写入值 含义
FF 00 置位(打开线圈,ON,状态为 1)
00 00 复位(关闭线圈,OFF,状态为 0)

记住这个特殊规则:开是 FF 00,关是 00 00。这是很多新手会疑惑的地方:为什么”开”不是 00 01?因为 Modbus 规范就是这么定的,用 FF 00 表示开,00 00 表示关,别的值无效。

写单线圈的写入值,只有两种合法写法:开 = FF 00,关 = 00 00。别自作主张写成 00 01 或别的值,否则从站会拒绝执行。这个跟”读线圈时用 1 表示开”是两码事,一个是数据打包,一个是命令值,别搞混。

2.6.3 案例:把 1 号从站、第 1 个线圈置位(打开)

发送报文(同时也是接收报文):

1
01 05 00 01 FF 00 DD FA

拆解:

字节 数值 含义
第 1 个字节 01 从站地址,1 号从站
第 2 个字节 05 功能码,写单个线圈
第 3、4 个字节 00 01 线圈地址(相对地址 1,即第 2 个线圈)
第 5、6 个字节 FF 00 写入值,FF 00 表示置位(打开)
第 7、8 个字节 DD FA CRC 校验码

翻译成大白话:

“1 号从站,请把你的第 2 个输出线圈打开(置位)。”

从站执行完这个操作后,会原样把这条报文发回来:

1
01 05 00 01 FF 00 DD FA

主站收到这条一模一样的回应,就知道”操作成功了”。如果从站没回应,或者回了别的(比如异常码),就说明写入失败了。

顺便一提,如果你想让这个线圈关闭,把写入值从 FF 00 改成 00 00 就行,其余部分不变。


2.7 功能码 06 写入单寄存器

2.7.1 报文格式

功能码 06 用来设置某一个保持寄存器的值,它和功能码 05 一样,也是”原报文返回”。

发送报文:

1
从站地址 + 06 + 寄存器地址 + 写入值(2字节) + CRC16

接收报文(原样返回):

1
从站地址 + 06 + 寄存器地址 + 写入值(2字节) + CRC16

2.7.2 案例:把 3 号从站、地址为 5 的寄存器改成 200

发送报文(同时也是接收报文):

1
03 06 00 05 00 C8 99 BF

拆解:

字节 数值 含义
第 1 个字节 03 从站地址,3 号从站
第 2 个字节 06 功能码,写单个寄存器
第 3、4 个字节 00 05 寄存器地址(相对地址 5,即第 6 个寄存器)
第 5、6 个字节 00 C8 写入值,十六进制 C8 就是十进制 200
第 7、8 个字节 99 BF CRC 校验码

翻译成大白话:

“3 号从站,请把你的第 6 个保持寄存器改成 200。”

注意写入值 00 C8 是”高字节在前”:00 是高字节,C8 是低字节,合起来十六进制 00C8,等于十进制 200。

从站执行后,原样返回:

1
03 06 00 05 00 C8 99 BF

主站收到一模一样的回应,就知道写入成功。

到这里,”写单个”的两个功能码(05 写单线圈、06 写单寄存器)都学完了。它们都是”原报文返回”。下面进入最后两个”写多个”的功能码,这两个稍微复杂一点点。


2.8 功能码 0F 写入多输出线圈

2.8.1 报文格式

功能码 05 一次只能写一个线圈,如果要一次性写多个线圈,就用功能码 0F。它的发送报文比 05 多了一个”字节计数”字段。

发送报文:

1
从站地址 + 0F + 起始线圈地址 + 线圈数量 + 字节计数 + 具体写入值 + CRC16

接收报文:

1
从站地址 + 0F + 起始线圈地址 + 线圈数量 + CRC16

注意对比:接收报文比发送报文短,从站只回”地址 + 数量”,不回具体数据。因为从站的意思是:”我收到你的命令了,已经写完这 N 个线圈。”具体写的数据主站自己知道,没必要回。

2.8.2 案例:把 1 号从站、从 0 开始的 8 个线圈全部置为 True(打开)

主站发送:

1
01 0F 00 00 00 08 01 FF BE D5

拆解:

字节 数值 含义
第 1 个字节 01 从站地址,1 号从站
第 2 个字节 0F 功能码,写多个线圈
第 3、4 个字节 00 00 起始线圈地址(相对地址 0,从第 1 个线圈开始)
第 5、6 个字节 00 08 线圈数量,8 个
第 7 个字节 01 字节计数,后面 1 个字节数据
第 8 个字节 FF 具体写入值,8 个线圈的状态打包
第 9、10 个字节 BE D5 CRC 校验码

2.8.3 重点:写多线圈时的数据打包

这里的关键是理解 FF 这个字节。

写多线圈时,线圈状态同样按”8 个线圈打包成 1 个字节、低位在前”的规则来。8 个线圈正好占 1 个字节,所以字节计数是 01。

我们要把 8 个线圈全部置为 1(开),也就是 8 个 bit 全是 1,二进制就是 1111 1111,换算成十六进制正好是 FF

反过来想:如果你想把这 8 个线圈全部置为 0(关),那 8 个 bit 全是 0,这个字节就是 00

从站接收:

1
01 0F 00 00 00 08 54 0D

拆解:

字节 数值 含义
第 1 个字节 01 从站地址
第 2 个字节 0F 功能码,写多线圈的回应
第 3、4 个字节 00 00 起始线圈地址(原样返回)
第 5、6 个字节 00 08 线圈数量(原样返回)
第 7、8 个字节 54 0D CRC 校验码

看到没有,接收报文只回了”地址 + 数量”,表示”这 8 个线圈我已经写好了”。


2.9 功能码 10 写入多个寄存器

2.9.1 报文格式

功能码 06 一次只能写一个寄存器,要一次性写多个寄存器,就用功能码 10。它是这一部分报文最长的一个,但只要记住结构,一点不难。

发送报文:

1
从站地址 + 10 + 起始寄存器地址 + 寄存器数量 + 字节计数 + 具体写入值 + CRC16

接收报文:

1
从站地址 + 10 + 起始寄存器地址 + 寄存器数量 + CRC16

和功能码 0F 一样,从站回应时只回”地址 + 数量”,不回数据。

2.9.2 案例:把 3 号从站、从 0 开始的 3 个寄存器全部改成 100

主站发送:

1
03 10 00 00 00 03 06 00 64 00 64 00 64 D0 3E

拆解:

字节 数值 含义
第 1 个字节 03 从站地址,3 号从站
第 2 个字节 10 功能码,写多个寄存器
第 3、4 个字节 00 00 起始寄存器地址(相对地址 0,从第 1 个寄存器开始)
第 5、6 个字节 00 03 寄存器数量,3 个
第 7 个字节 06 字节计数,后面 6 个字节数据
第 8 到 13 个字节 00 64 00 64 00 64 3 个寄存器的值,每个都是 100
第 14、15 个字节 D0 3E CRC 校验码

2.9.3 读懂数据部分

3 个寄存器,每个 2 字节,所以数据总共 6 个字节,字节计数是 06(3 乘以 2)。

数据部分 00 64 00 64 00 64,两两一组:

字节 十进制值
第 1 组 00 64 100
第 2 组 00 64 100
第 3 组 00 64 100

所以这条命令就是把 3 个寄存器都写成了 100。

从站接收:

1
03 10 00 00 00 03 81 EA

拆解:

字节 数值 含义
第 1 个字节 03 从站地址
第 2 个字节 10 功能码,写多寄存器的回应
第 3、4 个字节 00 00 起始寄存器地址(原样返回)
第 5、6 个字节 00 03 寄存器数量(原样返回)
第 7、8 个字节 81 EA CRC 校验码

从站只回”地址 + 数量”,表示”这 3 个寄存器我已经写好了”。


第二部分小结

这一部分我们把 8 个功能码的报文全部拆了一遍。现在停下来,把最重要的规律总结一下,帮你形成整体记忆:

第一,所有报文都是”从站地址 + 功能码 + 数据 + CRC”四大块。

这是 Modbus RTU 的铁律,任何一条报文都逃不出这个结构。

第二,”读”和”写”的报文套路不同。

  • 读(01、02、03、04):发送时给”起始地址 + 数量”,接收时回”字节计数 + 数据”;
  • 写单个(05、06):发送和接收内容相同,从站”原报文返回”;
  • 写多个(0F、10):发送时给”起始地址 + 数量 + 字节计数 + 数据”,接收时只回”地址 + 数量”。

第三,两个”顺序”千万别搞反。

  • 寄存器是”高字节在前”(一个寄存器 2 字节,先传高字节);
  • 线圈是”低位在前”(8 个线圈打包 1 字节,bit 0 对应第一个线圈)。

第四,几个关键的数字关系。

  • 读寄存器时:字节计数 = 寄存器数量 × 2;
  • 写单个线圈时:开 = FF 00,关 = 00 00;
  • CRC 校验码在报文末尾,低字节在前。

第五,从站地址 1 到 247,0 是广播地址。

这一部分你如果能做到”随便给你一条报文,你能说出它是读还是写、读什么、数据是什么”,那 Modbus RTU 报文分析就真正过关了。

用下面这份清单,做一次最终自测:

我能说出 RTU 报文由”从站地址 + 功能码 + 数据 + CRC”四大块组成

我能拆解功能码 01 的线圈数据(低位在前)

我知道读寄存器时,字节计数 = 寄存器数量 × 2

我知道写单线圈时,开 = FF 00、关 = 00 00

我能分清写单(原报文返回)和写多(只回地址和数量)的区别

随便给我一条报文,我能说出它是读还是写、数据是多少

第二部分

课后小练习:这条报文你能自己拆出来吗

题目:主站要读取 1 号从站、从 0 开始的 3 个保持寄存器。

请你想一想:

  1. 主站应该发什么报文?(提示:功能码 03,起始地址 00 00,数量 00 03)
  2. 如果从站回复 01 03 06 00 0A 00 14 00 1E(暂不计 CRC),这 3 个寄存器的值分别是多少?
查看答案
  1. 主站发送:从站地址 01 + 功能码 03 + 起始地址 00 00 + 数量 00 03,拼起来就是 01 03 00 00 00 03,最后再补上 2 字节 CRC。
  2. 从站回复里,字节计数是 06(共 6 个字节数据),两两一组拆开:
    • 00 0A 换算成十进制是 10
    • 00 14 换算成十进制是 20
    • 00 1E 换算成十进制是 30
      所以这 3 个寄存器的值分别是 10、20、30。

下一部分,我们进入动手环节:把 Modbus Poll 和 Modbus Slave 装起来,亲手发一遍上面这些报文,亲眼看到它们在电脑里跑起来。到那时候,你再看这些十六进制数字,就不再是抽象的概念,而是”活”的数据了。

有任何一条报文没看懂的,随时回来问我,我陪你一条一条过。