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 功能码选择逻辑
在实际项目中,怎么选功能码?我总结了一个简单的判断逻辑:
- 先看对象:是位还是16位数据?位用01/02/05/15,16位用03/04/06/16
- 再看属性:只读还是可读写?只读用02/04,可读写用01/03
- 最后看数量:单个用05/06,多个用01/02/03/04/15/16
核心逻辑图
这8个功能码,覆盖了Modbus RTU 90%以上的应用场景。我个人建议,把这8个功能码的报文格式背下来,调试时能省不少时间。嗯,今天就讲到这里,下节课咱们聊聊异常码处理。