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写多个寄存器。到时候你会发现,读和写其实是对称的,一通百通。