4、Modbus功能码详解(上):01读线圈、02读离散量输入、03读保持寄存器、04读输入寄存器
功能码,说白了就是Modbus通信的指令集。
你想想看,主站和从站之间要交换数据,总得有个约定吧?主站说「我要读数据」,从站就得知道读什么、从哪里读、读多少。这个约定,就是功能码。
我个人习惯把功能码分成两类:读操作和写操作。今天我们先聊四个最常用的读功能码——01、02、03、04。这四个码,覆盖了Modbus中所有数据类型(线圈、离散量、寄存器)的读取操作。
4.1 功能码01:读线圈(Read Coils)
线圈是什么?说白了就是开关量输出。在PLC里,线圈对应的是Y点或者Q点。比如控制一个继电器的通断、一个指示灯的亮灭。
功能码01的作用,就是读取这些线圈的当前状态——是ON(1)还是OFF(0)。
请求报文格式(主站→从站):
- 从站地址:1字节
- 功能码:0x01
- 起始地址:2字节(高位在前)
- 线圈数量:2字节(高位在前)
- CRC校验:2字节
举个例子。我想读取从站地址为01的设备的第0~7号线圈(共8个)。
请求:01 01 00 00 00 08 3D CC
这里00 00是起始地址,00 08是数量。3D CC是CRC。
从站收到后,会回复什么?
响应:01 01 01 05 41 9B
解析一下:01是从站地址,01是功能码,01是数据字节数(8个线圈占1字节),05是线圈状态(二进制0000 0101,表示线圈0和线圈2为ON)。
我的经验:线圈数量必须是8的倍数吗?不是。你可以读任意数量,比如读3个线圈。但返回的字节数会按8位对齐,多出来的高位补0。我曾经在项目里读5个线圈,结果从站返回了1字节,高3位全是0,差点以为是故障。
4.2 功能码02:读离散量输入(Read Discrete Inputs)
离散量输入,就是开关量输入。对应PLC里的X点或者I点。比如按钮、限位开关、光电传感器的信号。
功能码02和01非常像,唯一的区别是:01读的是输出线圈,02读的是输入触点。线圈可以写,离散量只能读。
请求报文格式:
- 从站地址:1字节
- 功能码:0x02
- 起始地址:2字节
- 输入数量:2字节
- CRC:2字节
比如读从站01的第10~17号离散量输入(共8个)。
请求:01 02 00 0A 00 08 5D CC
响应示例:
响应:01 02 01 00 41 9B
这里00表示所有8个输入都是OFF状态。
注意:离散量输入的地址通常从0开始,但有些设备手册会从1开始编号。比如手册说「第1路输入」,对应Modbus地址是0。我曾经踩过这个坑,读出来的数据全对不上,后来发现是地址偏移了1位。
4.3 功能码03:读保持寄存器(Read Holding Registers)
保持寄存器,这是Modbus里最常用的数据类型。它存储的是16位数据,可以读写。比如设置温度值、读取当前转速、写入PID参数,都用保持寄存器。
功能码03,就是读取这些寄存器的当前值。
请求报文格式:
- 从站地址:1字节
- 功能码:0x03
- 起始地址:2字节
- 寄存器数量:2字节
- CRC:2字节
举个例子。读从站01的第0~1号保持寄存器(共2个)。
请求:01 03 00 00 00 02 C4 0B
响应:
响应:01 03 04 00 0A 00 14 7A 5B
解析:04表示数据长度(2个寄存器×2字节=4字节)。00 0A是第一个寄存器的值(十进制10),00 14是第二个寄存器的值(十进制20)。
我的习惯:读保持寄存器时,我一般一次读多个连续寄存器。比如读10个,比读10次每次1个要快得多。但要注意,有些从站限制了一次读取的最大数量,通常是125个寄存器(250字节)。
4.4 功能码04:读输入寄存器(Read Input Registers)
输入寄存器和保持寄存器很像,都是16位数据。区别在于:输入寄存器是只读的。它通常用来存储传感器采集的实时数据,比如温度、压力、流量。
功能码04的报文格式和03完全一样,只是功能码不同。
请求报文格式:
- 从站地址:1字节
- 功能码:0x04
- 起始地址:2字节
- 寄存器数量:2字节
- CRC:2字节
比如读从站01的第5~6号输入寄存器。
请求:01 04 00 05 00 02 21 CB
响应:
响应:01 04 04 01 2C 00 F0 7A 5B
这里01 2C是第一个寄存器的值(十进制300),00 F0是第二个寄存器的值(十进制240)。
避坑指南:我曾经遇到一个温控器,它的温度值存在输入寄存器里,但数据格式是带符号的。比如-10℃用0xFFF6表示(补码)。如果你用无符号数去读,会得到65526,完全不对。所以读之前,一定要搞清楚数据格式——是无符号、有符号、还是浮点数拆成两个寄存器。
4.5 四个功能码的对比总结
| 功能码 | 名称 | 数据类型 | 读写属性 | 数据单位 | 典型用途 |
|---|---|---|---|---|---|
| 01 | 读线圈 | 位 | 可读可写 | 1位 | 继电器、指示灯 |
| 02 | 读离散量输入 | 位 | 只读 | 1位 | 按钮、限位开关 |
| 03 | 读保持寄存器 | 16位 | 可读可写 | 2字节 | 参数设置、状态读取 |
| 04 | 读输入寄存器 | 16位 | 只读 | 2字节 | 传感器实时数据 |
嗯,这里要注意一点:地址空间是独立的。线圈地址0和离散量输入地址0,是两个不同的地址。保持寄存器地址0和输入寄存器地址0,也是两个不同的地址。所以你在编程时,一定要区分清楚用的是哪个功能码。
4.6 实际编程中的注意事项
我个人在写上位机程序时,会遵循几个原则:
- 先读再写:调试时先用功能码03或04读一下,确认通信正常,再考虑写操作。
- 超时处理:Modbus是主从模式,主站发出请求后,从站必须在规定时间内回复。我一般设500ms超时,超过就重试3次。
- 数据校验:CRC校验不能省。虽然有些设备不校验也能通信,但一旦出现干扰,数据错位了都不知道。
- 地址映射:建议做一个地址映射表,把Modbus地址和物理含义对应起来。比如「保持寄存器地址100 = 当前温度」。这样代码可读性高,也方便维护。
一个小技巧:如果你用Python写上位机,推荐用pymodbus库。它封装了功能码的细节,你只需要调用read_coils()、read_discrete_inputs()、read_holding_registers()、read_input_registers()这四个方法就行。底层报文组装和解析,库帮你做了。
好了,四个读功能码就讲到这里。下一节我们聊写功能码——05写单个线圈、06写单个寄存器、15写多个线圈、16写多个寄存器。到时候你会发现,写操作比读操作要小心得多,因为写错了可能真的会出事故。