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是读模拟量的。你只要记住这个核心区别,剩下的就是报文格式的细节了。下一节我们讲写操作的功能码,到时候你会发现,读和写其实是对称的。