02-Modbus RTU通信报文分析
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,你现在只需要记住三点:
- 它是 2 个字节,放在报文的最末尾;
- 它覆盖的范围是”从站地址 + 功能码 + 数据区”,也就是除了 CRC 自己之外的所有字节;
- **发送时是”低字节在前、高字节在后”**。这个顺序特别重要,很多新手写代码时把高低字节搞反了,导致报文一直校验不过。
打个比方:如果算出来的 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 个保持寄存器。
请你想一想:
- 主站应该发什么报文?(提示:功能码 03,起始地址 00 00,数量 00 03)
- 如果从站回复
01 03 06 00 0A 00 14 00 1E(暂不计 CRC),这 3 个寄存器的值分别是多少?
查看答案
- 主站发送:从站地址 01 + 功能码 03 + 起始地址 00 00 + 数量 00 03,拼起来就是
01 03 00 00 00 03,最后再补上 2 字节 CRC。 - 从站回复里,字节计数是 06(共 6 个字节数据),两两一组拆开:
- 00 0A 换算成十进制是 10
- 00 14 换算成十进制是 20
- 00 1E 换算成十进制是 30
所以这 3 个寄存器的值分别是 10、20、30。
下一部分,我们进入动手环节:把 Modbus Poll 和 Modbus Slave 装起来,亲手发一遍上面这些报文,亲眼看到它们在电脑里跑起来。到那时候,你再看这些十六进制数字,就不再是抽象的概念,而是”活”的数据了。
有任何一条报文没看懂的,随时回来问我,我陪你一条一条过。

