4、功能码详解(上):01读线圈、02读离散输入、03读保持寄存器、04读输入寄存器

各位同学,咱们今天正式进入功能码的世界。

说实话,Modbus协议里最核心的就是这16个功能码。但真正在工控现场天天打交道的,其实就前面这4个。我个人习惯把这四个称为「四大金刚」——读线圈、读离散输入、读保持寄存器、读输入寄存器。

你想想看,一个PLC或者DCS系统,80%的通讯任务都是在干这几件事。所以把这四个搞透,你基本就能应付大多数现场问题了。

4.1 01功能码:读线圈(Read Coils)

01功能码,用来读取从设备的线圈状态。线圈是什么?说白了就是开关量输出,比如继电器、接触器、指示灯这些。

报文格式

方向字节数内容
主站请求1功能码 0x01
2起始地址(高字节在前)
2线圈数量(1~2000)
从站响应1功能码 0x01
1数据字节数
N线圈状态数据

这里有个细节要注意:线圈状态是按位打包的。一个字节可以存8个线圈的状态。比如你读10个线圈,返回的数据就是2个字节,最后一个字节的高6位补0。

实战经验:我在项目中遇到过,有人读线圈数量写了个2001,结果从站直接返回异常码。Modbus规定单次最多读2000个线圈,别超了。

小技巧:如果你只需要读一个线圈,比如地址0的启动按钮,直接发01 00 00 00 01就行。返回的1个字节里,最低位就是你要的状态。

4.2 02功能码:读离散输入(Read Discrete Inputs)

02功能码和01很像,区别在于它读的是离散输入,也就是开关量输入信号。比如按钮、限位开关、光电传感器这些。

报文格式和01完全一样,只是功能码换成了0x02。

为什么要把01和02分开? 嗯,这里要讲清楚。线圈(01)是可读可写的,离散输入(02)是只读的。你想想看,按钮按下去的状态,总不能通过通讯去改吧?所以Modbus在设计时就做了区分。

避坑指南:我曾经遇到一个现场,工程师用01功能码去读离散输入,结果从站一直返回异常码。查了半天才发现功能码用错了。记住:读输入信号用02,读输出信号用01。

4.3 03功能码:读保持寄存器(Read Holding Registers)

03功能码,这是Modbus里最常用的功能码,没有之一。保持寄存器用来存放模拟量数据,比如温度值、压力值、频率设定值这些。而且它是可读可写的。

报文格式

方向字节数内容
主站请求1功能码 0x03
2起始地址
2寄存器数量(1~125)
从站响应1功能码 0x03
1数据字节数
N*2寄存器数据(每个寄存器2字节)

注意看,03功能码单次最多读125个寄存器。为什么是125?因为响应报文最大长度是255字节,减去功能码和长度字节,剩下253字节,除以2就是126个寄存器。但协议里规定最大125,留了点余量。

个人经验:我习惯一次读10~20个寄存器。读太多的话,万一中间某个寄存器出问题,整个报文就废了。分批读虽然慢点,但稳定。

4.4 04功能码:读输入寄存器(Read Input Registers)

04功能码和03的区别,跟01与02的区别一样——输入寄存器是只读的。它通常用来读取传感器采集的实时数据,比如模拟量输入模块的值。

报文格式和03完全一样,功能码换成0x04。

什么时候用03,什么时候用04? 这个问题我经常被问到。其实很简单:

  • 如果你要读的数据是设备主动采集的(比如温度传感器),用04
  • 如果你要读的数据是你可以修改的(比如PID设定值),用03

实用技巧:很多国产仪表把参数都放在保持寄存器里,不管是不是只读。但正规的Modbus设备一定会区分。我建议你拿到设备手册后,先看寄存器地址范围,03和04各试一次,看哪个能正常返回数据。

4.5 Python实战:用modbus_tk库读取数据

光说不练假把式。咱们用Python写个例子,把这四个功能码都跑一遍。

import modbus_tk.modbus_tcp as mt
import modbus_tk.defines as md

# 连接从站
master = mt.TcpMaster("192.168.1.100", 502)
master.set_timeout(5.0)

try:
    # 01功能码:读线圈,从地址0开始读8个
    coils = master.execute(1, md.READ_COILS, 0, 8)
    print(f"线圈状态: {coils}")

    # 02功能码:读离散输入,从地址0开始读8个
    discrete_inputs = master.execute(1, md.READ_DISCRETE_INPUTS, 0, 8)
    print(f"离散输入: {discrete_inputs}")

    # 03功能码:读保持寄存器,从地址0开始读5个
    holding_regs = master.execute(1, md.READ_HOLDING_REGISTERS, 0, 5)
    print(f"保持寄存器: {holding_regs}")

    # 04功能码:读输入寄存器,从地址0开始读5个
    input_regs = master.execute(1, md.READ_INPUT_REGISTERS, 0, 5)
    print(f"输入寄存器: {input_regs}")

except Exception as e:
    print(f"通讯失败: {e}")

这段代码很直观。注意看,execute函数的参数分别是:从站地址、功能码、起始地址、数量。返回的数据类型是元组,按顺序排列。

重要提醒:地址是从0开始的。但很多设备手册里写的地址是PLC地址,比如40001、30001这些。40001对应保持寄存器地址0,30001对应输入寄存器地址0。换算的时候要减1,这个坑我踩过不止一次。

4.6 常见异常与处理

现场调试时,这四个功能码最容易出什么问题?我总结了几点:

  • 异常码01(非法功能码):从站不支持这个功能码。比如有些简单的IO模块只支持01和03,不支持02和04。
  • 异常码02(非法数据地址):你读的地址超出了从站的范围。比如设备只有10个寄存器,你非要去读地址20。
  • 异常码03(非法数据值):数量参数不对。比如读线圈时写了0个,或者超过了2000个。

遇到异常码怎么办?我的做法是:先检查功能码对不对,再确认地址范围,最后看数量是否合理。90%的问题都能解决。

最后说一句:这四个功能码是Modbus通讯的基石。你花一天时间把它们吃透,后面学写功能码、读写多寄存器就会轻松很多。下一节咱们讲05写单个线圈、06写单个寄存器,还有15写多个线圈、16写多个寄存器。到时候你会发现,读和写其实是对称的,一通百通。