写给初学者的话:
你是不是在调试Modbus设备时,看到一串十六进制数据(比如02 02 00 0A 00 10 59 F7)就一头雾水?是不是搞不清“功能码02”、“起始地址”、“字节计数”这些词是什么意思?别担心!这篇笔记会从最基础的概念讲起,用生活化的比喻、清晰的表格和详细的步骤,带你彻底搞懂Modbus RTU协议中功能码02——读取输入线圈的完整过程。记住,功能码02就像一个“传感器状态查询器”,专门用来查看哪些传感器是触发的、哪些是没有触发的。这篇笔记内容超详细,字数很多,但每一句都是为你量身打造,慢慢看,一定能学会!
1️⃣ 核心思想:功能码02 = 查询传感器状态
在上一篇笔记中,我们已经知道:
✅ 功能码02 是用来 “读取输入线圈” 的。
那什么是“输入线圈”呢?
- 输入线圈 (Input Coil):其实就是“传感器”或“按钮”的状态。
 - 它只有两种状态:触发 (ON) = 1 或 未触发 (OFF) = 0。
 - 在Modbus协议中,它对应的是 1区(输入线圈区)。
 
所以,功能码02的作用就是:
“请问从站,从某个地址开始,连续N个传感器的状态分别是触发还是未触发?”
2️⃣ 功能码02的发送报文结构
图中给出了发送报文的结构:
✅ 发送报文 = 从站地址 + 02 + 起始线圈地址 + 线圈数量 + CRC16校验
我们来逐一分解每一个部分。
🔹 第一部分:从站地址(Slave Address)
- 这是报文的第一字节,用来指定这封“电报”是发给哪个设备的。
 - 取值范围:1 ~ 255(十进制)
 - 实际应用中,常用的地址是 
1 ~ 247。 
✅ 一句话总结:
从站地址 = 收件人地址,告诉设备“这封信是给你的”。
🔹 第二部分:功能码 02
- 这是报文的第二字节,固定为 
02。 - 它告诉从站:“我要读取输入线圈的状态”。
 
✅ 一句话总结:
功能码02 = 我要查传感器状态。
🔹 第三部分:起始线圈地址(Starting Coil Address)
- 这是报文的第三和第四字节,用来指定从哪个地址开始读取。
 - 它是一个16位无符号整数,由高字节+低字节组成。
 - 地址是从 
0开始计数的(相对地址),而不是从1开始。 
🧩 举例说明
你想从地址 00011 开始读取(也就是第11个输入线圈):
- 相对地址 = 
10(因为地址从1开始,偏移量从0开始) - 所以,起始地址 = 
00 0A(高字节=00,低字节=0A) 
💡 记忆口诀:
“绝对地址 - 1 = 相对地址”
比如:00001→0;00010→9;00011→10
🔹 第四部分:线圈数量(Coil Quantity)
- 这是报文的第五和第六字节,用来指定你要读取多少个线圈。
 - 它也是一个16位无符号整数,由高字节+低字节组成。
 - 最大可以读取 
2000个线圈(协议限制)。 
🧩 举例说明
你想读取16个线圈:
- 线圈数量 = 
16(十进制) - 十六进制 = 
00 10(高字节=00,低字节=10) 
✅ 一句话总结:
线圈数量 = 我要查几个传感器。
🔹 第五部分:CRC16校验(Cyclic Redundancy Check)
- 这是报文的最后两个字节,用来保证报文的准确性和完整性。
 - 如果在传输过程中数据被干扰或篡改,校验码就会不匹配,接收方会丢弃这个报文。
 - CRC16校验码是根据前面所有字节(从站地址到线圈数量)计算出来的。
 
✅ 一句话总结:
CRC16校验 = 数据的“验证码”,确保报文在传输过程中没有出错。
3️⃣ 功能码02的接收报文结构
图中也给出了接收报文的结构:
✅ 接收报文 = 从站地址 + 02 + 字节计数 + 具体数据 + CRC16校验
我们来逐一分解每一个部分。
🔹 第一部分:从站地址(Slave Address)
- 这是报文的第一字节,与发送报文中的从站地址相同。
 - 它告诉主站:“这是谁回复的”。
 
✅ 一句话总结:
从站地址 = 回复人地址,告诉主站“这是我回复的”。
🔹 第二部分:功能码 02
- 这是报文的第二字节,固定为 
02。 - 它告诉主站:“我正在回复你关于输入线圈的查询”。
 
✅ 一句话总结:
功能码02 = 我在回复传感器状态。
🔹 第三部分:字节计数(Byte Count)
- 这是报文的第三字节,用来指定后面“具体数据”部分有多少个字节。
 - 它是一个8位无符号整数,最大值是 
255。 - 因为每个字节可以存储8个线圈的状态(1个bit代表1个线圈),所以最多可以返回 
255 * 8 = 2040个线圈的状态。 
🧩 举例说明
你请求读取16个线圈:
- 16个线圈需要多少字节?
- 每个字节存8个线圈 → 16 / 8 = 2 → 正好2字节
 
 - 所以,字节计数 = 
02 
✅ 一句话总结:
字节计数 = 数据部分有多少个字节。
🔹 第四部分:具体数据(Data)
- 这是报文的第四字节及以后,用来存放实际的线圈状态。
 - 每个字节的每一位(bit)代表一个线圈的状态:
1= 触发(ON)0= 未触发(OFF)
 - 数据的顺序是从低位到高位,再从左到右。
 
🧩 举例说明
假设你读取了16个线圈,它们的状态是:
| 线圈编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 状态 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 
那么,具体数据应该如何排列?
- 第1个字节:存储线圈1~8的状态 → 
01001101(二进制)→4D(十六进制) - 第2个字节:存储线圈9~16的状态 → 
10101010(二进制)→AA(十六进制) 
💡 重要提醒:
在Modbus协议中,数据的位序是从低位到高位排列的!也就是说,第0位是最低位,第7位是最高位。
所以,正确的排列应该是:
- 线圈1 → bit0
 - 线圈2 → bit1
 - ...
 - 线圈8 → bit7
 - 线圈9 → bit0(下一个字节)
 - 线圈10 → bit1(下一个字节)
 - ...
 - 线圈16 → bit7(下一个字节)
 
因此,具体数据 = 4D AA
🔹 第五部分:CRC16校验
- 这是报文的最后两个字节,用来保证报文的准确性和完整性。
 - CRC16校验码是根据前面所有字节(从站地址到具体数据)计算出来的。
 
✅ 一句话总结:
CRC16校验 = 数据的“验证码”,确保报文在传输过程中没有出错。
4️⃣ 完整案例解析:读取2号从站从10开始的16个输入线圈
图中给出了一个完整的案例:
发送报文:
02 02 00 0A 00 10 59 F7
接收报文:02 02 02 15 00 F3 28
下面我们来一步一步解析这个案例。
📌 步骤1:发送报文解析
发送报文 = 02 02 00 0A 00 10 59 F7
| 部分 | 字节位置 | 内容 | 说明 | 
|---|---|---|---|
| 从站地址 | 第1字节 | 02 | 
发送给设备2 | 
| 功能码 | 第2字节 | 02 | 
读取输入线圈 | 
| 起始地址 | 第3-4字节 | 00 0A | 
从地址10开始(即00011) | 
| 线圈数量 | 第5-6字节 | 00 10 | 
读取16个线圈 | 
| CRC16校验 | 第7-8字节 | 59 F7 | 
CRC16校验码 | 
✅ 发送报文含义:
“设备2,请告诉我从地址10开始的16个输入线圈的状态。”
📌 步骤2:接收报文解析
接收报文 = 02 02 02 15 00 F3 28
| 部分 | 字节位置 | 内容 | 说明 | 
|---|---|---|---|
| 从站地址 | 第1字节 | 02 | 
设备2回复的 | 
| 功能码 | 第2字节 | 02 | 
回复的是输入线圈状态 | 
| 字节计数 | 第3字节 | 02 | 
数据部分有2个字节 | 
| 具体数据 | 第4-5字节 | 15 00 | 
线圈状态数据 | 
| CRC16校验 | 第6-7字节 | F3 28 | 
CRC16校验码 | 
✅ 接收报文含义:
“设备2回复:从地址10开始的16个输入线圈的状态是15 00。”
📌 步骤3:解析具体数据 15 00
现在,我们要把 15 00 转换成16个线圈的状态。
🧮 第一步:将十六进制转换成二进制
15→0001010100→00000000
🧮 第二步:按位解析
| 字节 | 二进制 | 对应线圈编号 | 状态 | 
|---|---|---|---|
| 15 | 0 0 0 1 0 1 0 1 | 1 2 3 4 5 6 7 8 | 0 0 0 1 0 1 0 1 | 
| 00 | 0 0 0 0 0 0 0 0 | 9 10 ... | 0 0 0 0 0 0 0 0 | 
💡 注意:
在Modbus协议中,位序是从低位到高位排列的!也就是说,第0位是最低位,第7位是最高位。
所以,正确的解析应该是:
- 
第1个字节
15→00010101→ 从右到左:- bit0 = 1 → 线圈1 = 触发
 - bit1 = 0 → 线圈2 = 未触发
 - bit2 = 1 → 线圈3 = 触发
 - bit3 = 0 → 线圈4 = 未触发
 - bit4 = 1 → 线圈5 = 触发
 - bit5 = 0 → 线圈6 = 未触发
 - bit6 = 0 → 线圈7 = 未触发
 - bit7 = 0 → 线圈8 = 未触发
 
 - 
第2个字节
00→00000000→ 从右到左:- bit0 = 0 → 线圈9 = 未触发
 - bit1 = 0 → 线圈10 = 未触发
 - bit2 = 0 → 线圈11 = 未触发
 - bit3 = 0 → 线圈12 = 未触发
 - bit4 = 0 → 线圈13 = 未触发
 - bit5 = 0 → 线圈14 = 未触发
 - bit6 = 0 → 线圈15 = 未触发
 - bit7 = 0 → 线圈16 = 未触发
 
 
✅ 最终结果:
| 线圈编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 状态 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 
🎉 恭喜你!你已经成功解析了功能码02的接收报文!
5️⃣ 常见问题与避坑指南
❓ 问题1:为什么我发送的报文没有响应?
- 可能原因:
- 从站地址错误 → 检查设备地址是否正确。
 - 功能码错误 → 检查功能码是否为 
02。 - 起始地址错误 → 检查地址是否从 
0开始。 - 线圈数量错误 → 检查数量是否在 
1 ~ 2000之间。 - 校验码错误 → 使用正确的CRC16算法计算校验码。
 - 通信参数错误 → 检查波特率、数据位、停止位、校验位是否一致。
 
 
❓ 问题2:为什么接收报文中的字节计数是2,而我只读了16个线圈?
- 原因:因为16个线圈需要2个字节来存储(每个字节存8个线圈)。
 - 16 / 8 = 2 → 正好2字节。
 
✅ 一句话总结:
字节计数 = ceil(线圈数量 / 8)
❓ 问题3:如何手动计算CRC16校验码?
- 不要手动计算! 使用Modbus调试软件或编程库(如Python的
pymodbus)自动生成。 - 如果你一定要手动计算,可以使用在线CRC计算器,选择“Modbus RTU CRC16”。
 
6️⃣ 实战模拟:手把手教你构造和解析报文
假设你现在有一个任务:
“我要读取设备
01的输入线圈,从地址00001开始,读取8个线圈。”
📌 步骤1:构造发送报文
- 从站地址 = 
01 - 功能码 = 
02 - 起始地址 = 
00 00(相对地址0) - 线圈数量 = 
00 08(读取8个线圈) - CRC16校验 = 
B2 0D(由软件自动计算) 
发送报文 = 01 02 00 00 00 08 B2 0D
📌 步骤2:假设接收报文
假设设备回复:
接收报文 = 01 02 01 FF 8C 4B
📌 步骤3:解析接收报文
- 从站地址 = 
01→ 设备1回复的 - 功能码 = 
02→ 回复的是输入线圈状态 - 字节计数 = 
01→ 数据部分有1个字节 - 具体数据 = 
FF→ 二进制11111111 - CRC16校验 = 
8C 4B→ 校验通过 
📌 步骤4:解析具体数据 FF
FF→11111111→ 从右到左:- bit0 = 1 → 线圈1 = 触发
 - bit1 = 1 → 线圈2 = 触发
 - bit2 = 1 → 线圈3 = 触发
 - bit3 = 1 → 线圈4 = 触发
 - bit4 = 1 → 线圈5 = 触发
 - bit5 = 1 → 线圈6 = 触发
 - bit6 = 1 → 线圈7 = 触发
 - bit7 = 1 → 线圈8 = 触发
 
✅ 最终结果:8个线圈全部为“触发”状态!
🎯 下一步学习建议
- 动手实践:使用Modbus调试软件(如Modbus Poll, QModBus),输入不同的地址、线圈数量,观察返回结果。
 - 学习其他功能码:功能码01(读取输出线圈)、03(读取输出寄存器)、04(读取输入寄存器)等,它们的结构类似,只是操作对象不同。
 - 阅读设备手册:任何Modbus设备都会在手册中列出支持的功能码、地址范围和通信参数,一定要学会看手册!
 - 学习CRC16校验:虽然不需要手动计算,但了解其原理有助于深入理解协议。
 
我心里有一簇迎着烈日而生的花,比一切美酒都要芬芳,滚烫的馨香淹没过稻草人的胸膛,草扎的精神,从此万寿无疆。——《默读》