4、常用功能码详解:01/02/03/04/05/06/15/16

功能码是Modbus协议的灵魂。说白了,主站和从站之间怎么对话,全靠功能码来定调。我刚开始接触Modbus时,面对这8个功能码也懵过一阵。后来摸透了规律,发现其实就三类:读线圈、读寄存器、写线圈、写寄存器。今天咱们把这8个常用功能码彻底讲透。

4.1 位操作类:01、02、05、15

这4个功能码操作的是“位”,也就是0或1。在PLC里对应的是线圈(Coil)状态。

01功能码:读线圈

读线圈,说白了就是读一个或多个开关量。比如你接了个按钮,想知道它按没按,就用01功能码。

  • 功能:读取从站1~2000个线圈的ON/OFF状态
  • 请求帧:从站地址 + 0x01 + 起始地址(2字节) + 数量(2字节) + CRC
  • 响应帧:从站地址 + 0x01 + 字节数 + 数据(N字节) + CRC

实际案例:读取从站地址0x01,起始线圈0x0000,连续读10个线圈

请求:01 01 00 00 00 0A 3D C6
响应:01 01 02 CD 0B B0 7E
解释:CD = 1100 1101,0B = 0000 1011
线圈0~7:1,0,1,1,0,0,1,1
线圈8~9:1,1,0,0,0,0,0,0

我的经验:读线圈时,数量最好按字节对齐。比如读10个线圈,实际返回2个字节。多出来的6个位会被填充0。我曾经遇到一个项目,上位机没处理多余的位,结果误判了状态。嗯,这里要注意。

02功能码:读离散输入

02和01几乎一样,唯一的区别是:02读的是只读的输入点,比如传感器信号。01读的是可读可写的线圈。

  • 功能:读取从站1~2000个离散输入状态
  • 请求帧:从站地址 + 0x02 + 起始地址(2字节) + 数量(2字节) + CRC
  • 响应帧:从站地址 + 0x02 + 字节数 + 数据(N字节) + CRC

避坑指南:我曾经把02和01搞混过,结果读回来的数据全是0。后来才意识到,离散输入是只读的,线圈是可读写的。你想想看,如果传感器信号能被写,那现场得多乱?

05功能码:写单个线圈

05功能码用来控制一个线圈。比如启动一个电机、点亮一盏灯。

  • 功能:强制一个线圈为ON或OFF
  • 数据规则:0xFF00表示ON,0x0000表示OFF
  • 请求帧:从站地址 + 0x05 + 线圈地址(2字节) + 数据(2字节) + CRC
  • 响应帧:与请求帧完全相同(回显确认)

实际案例:让从站0x01的线圈0x000A(第11个线圈)吸合

请求:01 05 00 0A FF 00 6C 36
响应:01 05 00 0A FF 00 6C 36

我的习惯:写线圈时,我建议先读一下当前状态,确认无误再写。有一次我在调试时,直接写了个ON,结果把正在运行的设备给停了。从那以后,我养成了“先读后写”的习惯。

15功能码:写多个线圈

15功能码是05的升级版,一次可以写多个线圈。比如批量设置一组继电器的状态。

  • 功能:强制多个线圈为ON/OFF
  • 请求帧:从站地址 + 0x0F + 起始地址(2字节) + 数量(2字节) + 字节数 + 数据(N字节) + CRC
  • 响应帧:从站地址 + 0x0F + 起始地址(2字节) + 数量(2字节) + CRC

实际案例:写从站0x01,从线圈0x0000开始,连续写10个线圈,状态为:1,0,1,1,0,0,1,1,1,1

请求:01 0F 00 00 00 0A 02 CD 0F 3F 6A
响应:01 0F 00 00 00 0A 10 3D

4.2 寄存器操作类:03、04、06、16

寄存器操作的是16位数据。在PLC里对应的是保持寄存器(可读可写)和输入寄存器(只读)。

03功能码:读保持寄存器

03功能码是最常用的。读取模拟量、设定值、状态字等,都用它。

  • 功能:读取从站1~125个保持寄存器的值
  • 请求帧:从站地址 + 0x03 + 起始地址(2字节) + 数量(2字节) + CRC
  • 响应帧:从站地址 + 0x03 + 字节数 + 数据(N字节) + CRC

实际案例:读取从站0x01,起始寄存器0x0064(十进制100),连续读3个寄存器

请求:01 03 00 64 00 03 04 0E
响应:01 03 06 00 0A 00 14 00 1E 3A 5B
解释:寄存器100 = 0x000A = 10
     寄存器101 = 0x0014 = 20
     寄存器102 = 0x001E = 30

我的经验:读寄存器时,数据是大端模式(高字节在前)。很多新手会搞反字节序。我建议在调试时,先用一个已知值验证一下字节顺序。比如写个0x1234进去,读回来看看是不是0x12 0x34。

04功能码:读输入寄存器

04和03的区别,就像02和01的区别。04读的是只读的输入寄存器,比如温度传感器、压力变送器的值。

  • 功能:读取从站1~125个输入寄存器的值
  • 请求帧:从站地址 + 0x04 + 起始地址(2字节) + 数量(2字节) + CRC
  • 响应帧:从站地址 + 0x04 + 字节数 + 数据(N字节) + CRC

避坑指南:我曾经遇到一个温控器,它的温度值在输入寄存器里,但我用03去读,结果全是0。折腾了半天才发现,输入寄存器只能用04读。你想想看,如果只读寄存器能用03写,那传感器数据不就被篡改了吗?

06功能码:写单个寄存器

06功能码用来设置一个寄存器的值。比如设定温度、调整PID参数。

  • 功能:写一个16位数据到保持寄存器
  • 请求帧:从站地址 + 0x06 + 寄存器地址(2字节) + 数据(2字节) + CRC
  • 响应帧:与请求帧完全相同(回显确认)

实际案例:将从站0x01的寄存器0x0001的值设为0x1234

请求:01 06 00 01 12 34 18 0E
响应:01 06 00 01 12 34 18 0E

我的习惯:写单个寄存器时,我建议先读一下当前值,确认范围。比如温度设定值范围是0~100,你写个200进去,设备可能就保护了。我曾经在调试变频器时,写了个超范围的频率值,结果变频器直接报错停机。

16功能码:写多个寄存器

16功能码是06的升级版,一次可以写多个寄存器。比如批量设置一组参数。

  • 功能:写多个16位数据到保持寄存器
  • 请求帧:从站地址 + 0x10 + 起始地址(2字节) + 数量(2字节) + 字节数 + 数据(N字节) + CRC
  • 响应帧:从站地址 + 0x10 + 起始地址(2字节) + 数量(2字节) + CRC

实际案例:将从站0x01,起始寄存器0x0000,连续写3个寄存器,值分别为0x000A、0x0014、0x001E

请求:01 10 00 00 00 03 06 00 0A 00 14 00 1E 3A 5B
响应:01 10 00 00 00 03 10 3D

4.3 功能码对比总结

功能码 名称 操作对象 一次最多 读写属性
01 读线圈 线圈(位) 2000个 可读可写
02 读离散输入 离散输入(位) 2000个 只读
03 读保持寄存器 保持寄存器(16位) 125个 可读可写
04 读输入寄存器 输入寄存器(16位) 125个 只读
05 写单个线圈 线圈(位) 1个
06 写单个寄存器 保持寄存器(16位) 1个
15 写多个线圈 线圈(位) 1968个
16 写多个寄存器 保持寄存器(16位) 123个

4.4 功能码选择逻辑

在实际项目中,怎么选功能码?我总结了一个简单的判断逻辑:

  1. 先看对象:是位还是16位数据?位用01/02/05/15,16位用03/04/06/16
  2. 再看属性:只读还是可读写?只读用02/04,可读写用01/03
  3. 最后看数量:单个用05/06,多个用01/02/03/04/15/16

核心逻辑图

开始 位还是16位? 16位 读:01/02 写:05/15 读:03/04 写:06/16 结束

这8个功能码,覆盖了Modbus RTU 90%以上的应用场景。我个人建议,把这8个功能码的报文格式背下来,调试时能省不少时间。嗯,今天就讲到这里,下节课咱们聊聊异常码处理。


专注资料整理