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 实际编程中的注意事项

我个人在写上位机程序时,会遵循几个原则:

  1. 先读再写:调试时先用功能码03或04读一下,确认通信正常,再考虑写操作。
  2. 超时处理:Modbus是主从模式,主站发出请求后,从站必须在规定时间内回复。我一般设500ms超时,超过就重试3次。
  3. 数据校验:CRC校验不能省。虽然有些设备不校验也能通信,但一旦出现干扰,数据错位了都不知道。
  4. 地址映射:建议做一个地址映射表,把Modbus地址和物理含义对应起来。比如「保持寄存器地址100 = 当前温度」。这样代码可读性高,也方便维护。

一个小技巧:如果你用Python写上位机,推荐用pymodbus库。它封装了功能码的细节,你只需要调用read_coils()read_discrete_inputs()read_holding_registers()read_input_registers()这四个方法就行。底层报文组装和解析,库帮你做了。

好了,四个读功能码就讲到这里。下一节我们聊写功能码——05写单个线圈、06写单个寄存器、15写多个线圈、16写多个寄存器。到时候你会发现,写操作比读操作要小心得多,因为写错了可能真的会出事故。