4. Modbus 功能码详解:位操作与字操作
Modbus 协议的核心在于通过一组标准化的功能码(Function Code)实现对从站设备数据的读写。本章将深入解析最常用的 8 个功能码,将其分为位操作和字操作两大类,并结合 TCP 通信场景说明其数据帧结构与典型应用。
4.1 功能码分类总览
Modbus 功能码按操作粒度可分为位操作(针对单个 bit)和字操作(针对 16-bit 寄存器)。下表列出了本章涵盖的 8 个核心功能码及其用途:
| 类别 | 功能码 | 名称 | 操作对象 | 典型用途 |
|---|---|---|---|---|
| 位操作 | 0x01 | 读线圈 | DO (数字输出) | 读取继电器、指示灯状态 |
| 0x02 | 读离散输入 | DI (数字输入) | 读取按钮、限位开关状态 | |
| 0x05 | 写单线圈 | DO (单个) | 控制单个继电器通断 | |
| 0x0F | 写多线圈 | DO (批量) | 批量设置输出端口 | |
| 字操作 | 0x03 | 读保持寄存器 | 保持寄存器 | 读取传感器数值、设备参数 |
| 0x04 | 读输入寄存器 | 输入寄存器 | 读取只读测量值(如电压、温度) | |
| 0x06 | 写单寄存器 | 保持寄存器(单个) | 修改单个设定值 | |
| 0x10 | 写多寄存器 | 保持寄存器(批量) | 批量下载参数表 |
4.2 位操作功能码详解
位操作功能码用于读写设备的数字量(布尔型)数据,每个地址对应一个独立的 bit。在 TCP 帧中,位数据被紧凑打包在字节中传输。
4.2.1 读线圈 (0x01) 与读离散输入 (0x02)
这两个功能码的请求格式完全相同,区别在于读取的对象不同:0x01 读取可读可写的线圈(DO),0x02 读取只读的离散输入(DI)。
- 请求帧:功能码 + 起始地址 (2字节) + 读取数量 (2字节)
- 响应帧:功能码 + 字节数 (1字节) + 数据 (N字节)
数据打包规则:从起始地址开始,每个 bit 按地址递增顺序排列,第一个字节的 LSB 对应起始地址。若读取数量不是 8 的倍数,最后一个字节的高位补 0。
# 请求示例:读取从地址 0x0000 开始的 10 个线圈
# 请求 PDU: 01 00 00 00 0A
# 响应 PDU: 01 02 CD 01
# 解释:字节数=2,数据=0xCD (1100 1101) 和 0x01 (0000 0001)
# 线圈状态:地址0=1, 地址1=0, 地址2=1, 地址3=1, 地址4=0, 地址5=0, 地址6=1, 地址7=1, 地址8=1, 地址9=0
4.2.2 写单线圈 (0x05) 与写多线圈 (0x0F)
0x05 用于控制单个线圈的通断,数据域固定为 2 字节:0xFF00 表示 ON,0x0000 表示 OFF。响应帧是请求帧的完全回显,用于确认。
0x0F 用于一次写入多个连续的线圈,请求帧需指定起始地址、线圈数量以及打包后的字节数据。
# 写单线圈示例:将地址 0x0005 的线圈置为 ON
# 请求 PDU: 05 00 05 FF 00
# 响应 PDU: 05 00 05 FF 00
# 写多线圈示例:从地址 0x0000 开始写入 8 个线圈,状态为 1010 1010
# 请求 PDU: 0F 00 00 00 08 01 AA
# 响应 PDU: 0F 00 00 00 08
4.3 字操作功能码详解
字操作功能码以 16-bit 寄存器为基本单位,用于读写模拟量、计数器、参数设定值等。每个寄存器包含 2 字节数据,在 TCP 帧中采用大端序(Big-Endian)传输。
4.3.1 读保持寄存器 (0x03) 与读输入寄存器 (0x04)
0x03 用于读取可读可写的保持寄存器(如设定值、累计值),0x04 用于读取只读的输入寄存器(如实时测量值)。请求帧格式一致:功能码 + 起始地址 (2字节) + 寄存器数量 (2字节)。
响应帧中,每个寄存器的高字节在前,低字节在后。例如,读取到的数值 0x1234 在帧中表现为 12 34。
# 请求示例:读取从地址 0x0100 开始的 3 个保持寄存器
# 请求 PDU: 03 01 00 00 03
# 响应 PDU: 03 06 00 0A 00 14 00 1E
# 解释:字节数=6,寄存器0=0x000A (10),寄存器1=0x0014 (20),寄存器2=0x001E (30)
4.3.2 写单寄存器 (0x06) 与写多寄存器 (0x10)
0x06 用于写入单个保持寄存器,请求帧包含地址和 2 字节数据。响应帧回显请求内容。
0x10 用于一次写入多个连续的保持寄存器,请求帧需指定起始地址、寄存器数量、字节数(= 寄存器数量 × 2)以及数据体。
# 写单寄存器示例:将地址 0x0100 的寄存器值设为 0x0BB8 (3000)
# 请求 PDU: 06 01 00 0B B8
# 响应 PDU: 06 01 00 0B B8
# 写多寄存器示例:从地址 0x0200 开始写入 2 个寄存器,值分别为 100 和 200
# 请求 PDU: 10 02 00 00 02 04 00 64 00 C8
# 响应 PDU: 10 02 00 00 02
4.4 功能码选择实战建议
- 读取数字量:优先使用 0x01/0x02 而非 0x03,因为位操作节省带宽。例如读取 16 个 DI 只需 2 字节响应数据,而用 0x03 需要 32 字节。
- 批量写入:当需要同时修改多个线圈或寄存器时,使用 0x0F/0x10 代替多次 0x05/0x06,可减少网络往返次数。
- 只读数据:对于传感器测量值等只读数据,使用 0x04(输入寄存器)更符合语义,且部分设备对 0x03 访问只读区域会返回异常。
- 异常处理:所有功能码在遇到非法地址、数量超限或写入只读区域时,从站会返回异常响应帧(功能码 + 0x80 + 异常码)。
{"fc": 0x04, "start": 0x0000, "count": 10},便于后期扩展。