4、功能码详解(一):01H读线圈状态、02H读离散输入状态、03H读保持寄存器、04H读输入寄存器
各位同学,咱们今天来啃Modbus协议里最基础、也最常用的四个功能码。说实话,这四个功能码你要是搞明白了,Modbus通信的基本功就算打扎实了。我在现场调试的时候,90%的读写操作都离不开它们。
先给大家一个整体印象:01H和02H是读“位”的,03H和04H是读“字”的。位就是开关量,字就是模拟量。这个区分很重要,你想想看,现场传感器要么输出一个开关信号(比如液位开关),要么输出一个连续数值(比如压力变送器),正好对应这两类功能码。
4.1 01H - 读线圈状态
01H功能码,说白了就是读PLC或者控制器的DO(数字量输出)通道。我习惯叫它“读输出线圈”。
请求报文格式:
从站地址 | 功能码01H | 起始地址高8位 | 起始地址低8位 | 线圈数量高8位 | 线圈数量低8位 | CRC低8位 | CRC高8位
响应报文格式:
从站地址 | 功能码01H | 字节数 | 数据1 | 数据2 | ... | CRC低8位 | CRC高8位
关键点:线圈地址从0开始编号。比如你要读第1个线圈,起始地址填0x0000。数量最多2000个,但我建议一次别超过128个,否则通信效率反而下降。
举个例子。我要读从站地址为01的设备,从线圈地址10开始,连续读8个线圈的状态。请求报文是这样的:
01 01 00 0A 00 08 5D CC
解释一下:01是从站地址,01是功能码,00 0A是起始地址(十进制10),00 08是数量(8个线圈),5D CC是CRC校验。
假设响应报文是:
01 01 01 55 88 3C
这里01是地址,01是功能码,01表示数据占1个字节,55(二进制01010101)就是线圈状态。怎么解析?
- 线圈10(地址0x000A):对应bit0,值为1
- 线圈11:对应bit1,值为0
- 线圈12:bit2,值为1
- ...以此类推
我的经验:解析线圈数据时,记得bit0对应起始地址。我曾经见过一个同事把bit顺序搞反了,结果现场所有阀门动作都反了,排查了一下午才发现是字节序的问题。
4.2 02H - 读离散输入状态
02H和01H非常像,唯一的区别是:02H读的是DI(数字量输入),是只读的。你只能读取状态,不能修改它。
报文格式和01H完全一样,只是功能码换成02H。
请求:
从站地址 | 02H | 起始地址 | 数量 | CRC
响应:
从站地址 | 02H | 字节数 | 数据 | CRC
举个例子,读从站02的离散输入,从地址0开始读16个点:
02 02 00 00 00 10 39 84
响应可能是:
02 02 02 03 0A 78 9A
这里02是字节数,03和0A是两个数据字节。03(二进制00000011)表示输入0和1为ON,其他为OFF。0A(二进制00001010)表示输入8和10为ON。
注意:离散输入和线圈的地址空间是独立的。也就是说,线圈地址0和离散输入地址0是两个不同的物理点。我在一个项目里就遇到过这种混淆,上位机读到的数据怎么都对不上,后来发现是地址空间搞混了。
4.3 03H - 读保持寄存器
03H是Modbus里最常用的功能码,没有之一。保持寄存器是可读可写的,一般用来存放配置参数、设定值、累计值等。
请求报文:
从站地址 | 03H | 起始地址高8位 | 起始地址低8位 | 寄存器数量高8位 | 寄存器数量低8位 | CRC低8位 | CRC高8位
响应报文:
从站地址 | 03H | 字节数 | 数据高8位 | 数据低8位 | ... | CRC
注意,每个寄存器占2个字节,所以响应里的字节数 = 寄存器数量 × 2。
举个例子,读从站01的保持寄存器,从地址100开始读3个寄存器:
01 03 00 64 00 03 04 08
假设响应是:
01 03 06 00 0A 00 14 00 1E 48 7A
解析:06表示6个字节,00 0A(十进制10)是寄存器100的值,00 14(20)是寄存器101的值,00 1E(30)是寄存器102的值。
重要:Modbus协议规定,寄存器数据是大端模式(Big-Endian),即高字节在前,低字节在后。比如0x1234,发送时先发0x12,再发0x34。这个细节搞错了,数据全乱套。
4.4 04H - 读输入寄存器
04H和03H的区别,就像02H和01H的区别一样——输入寄存器是只读的。一般用来读取传感器采集的实时数据,比如温度、压力、流量等。
报文格式和03H完全一样,只是功能码换成04H。
请求:
从站地址 | 04H | 起始地址 | 数量 | CRC
响应:
从站地址 | 04H | 字节数 | 数据 | CRC
举个例子,读从站03的输入寄存器,从地址0开始读2个寄存器:
03 04 00 00 00 02 71 F6
响应:
03 04 04 01 2C 02 58 1A 3B
这里04是字节数,01 2C(十进制300)是输入寄存器0的值,02 58(十进制600)是输入寄存器1的值。
我的建议:在实际项目中,我习惯把传感器校准参数放在保持寄存器里,把实时测量值放在输入寄存器里。这样逻辑清晰,维护起来也方便。曾经有个项目,工程师把所有数据都塞到保持寄存器里,结果现场调试时参数被误写,整个系统都乱了。
4.5 四个功能码对比总结
| 功能码 | 名称 | 读写属性 | 数据单位 | 典型用途 | 最大读取数量 |
|---|---|---|---|---|---|
| 01H | 读线圈状态 | 只读 | 位(bit) | 读取DO输出状态 | 2000个 |
| 02H | 读离散输入 | 只读 | 位(bit) | 读取DI输入状态 | 2000个 |
| 03H | 读保持寄存器 | 可读可写 | 字(16bit) | 参数、设定值 | 125个 |
| 04H | 读输入寄存器 | 只读 | 字(16bit) | 实时测量值 | 125个 |
避坑指南:我曾经在一个灌溉控制项目里,把03H和04H搞混了。上位机用03H去读输入寄存器,结果一直返回异常码02(非法数据地址)。排查了半天才发现,原来设备手册里写的是输入寄存器,应该用04H。所以,拿到设备手册第一件事,就是确认功能码和地址映射表。
4.6 实战小技巧
嗯,这里再分享几个我实际调试中的心得:
- 批量读取优于单点读取:一次读多个寄存器比多次读单个寄存器效率高得多。我一般一次读10-20个寄存器,既不会占用太多总线时间,也不会让报文太长。
- 注意超时设置:Modbus是主从模式,主机发送请求后要等待从机响应。超时时间我一般设500ms,太短容易误判,太长影响实时性。
- CRC校验不能省:虽然很多上位机库自动处理了CRC,但如果你自己写代码,一定要校验CRC。工业现场干扰多,一个错误的报文可能导致设备误动作。
好了,这四个功能码就讲到这里。说白了,01H和02H是读开关量的,03H和04H是读模拟量的。你只要记住这个核心区别,剩下的就是报文格式的细节了。下一节我们讲写操作的功能码,到时候你会发现,读和写其实是对称的。