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 写多寄存器 保持寄存器(批量) 批量下载参数表
提示:在 Modbus TCP 中,功能码是 PDU(协议数据单元)的第一个字节。MBAP 头中的单元标识符(Unit ID)用于区分同一 TCP 连接下的不同从站设备。

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
注意:写多线圈 (0x0F) 的响应帧只返回起始地址和写入数量,不返回具体数据。客户端需自行比对请求与响应中的地址和数量字段来确认成功。
位操作功能码数据流(读/写线圈与离散输入) Modbus 客户端 请求 (0x01/02/05/0F) 从站设备 (服务器) 响应 (数据/回显) 解析与更新 位数据打包规则 地址 0 地址 1 地址 2 地址 3 地址 4 地址 5 地址 6 地址 7 字节 0: bit7~bit0 LSB = 地址0 字节 1: bit7~bit0 LSB = 地址8 最后一个字节高位补 0

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
关键区别:0x06 和 0x10 的响应帧只返回起始地址和写入数量,不包含数据体。0x10 的请求帧中必须包含字节数字段(寄存器数×2),而 0x06 不需要。
字操作功能码帧结构对比(读/写寄存器) 读请求 (0x03/0x04): 功能码 起始地址 (2B) 寄存器数量 (2B) 读响应: 功能码 字节数 寄存器数据 (N × 2B, 大端序) 写单请求 (0x06): 功能码 寄存器地址 (2B) 寄存器值 (2B) 写多请求 (0x10): 功能码 起始地址 寄存器数量 字节数 寄存器数据 (N × 2B)

4.4 功能码选择实战建议

  • 读取数字量:优先使用 0x01/0x02 而非 0x03,因为位操作节省带宽。例如读取 16 个 DI 只需 2 字节响应数据,而用 0x03 需要 32 字节。
  • 批量写入:当需要同时修改多个线圈或寄存器时,使用 0x0F/0x10 代替多次 0x05/0x06,可减少网络往返次数。
  • 只读数据:对于传感器测量值等只读数据,使用 0x04(输入寄存器)更符合语义,且部分设备对 0x03 访问只读区域会返回异常。
  • 异常处理:所有功能码在遇到非法地址、数量超限或写入只读区域时,从站会返回异常响应帧(功能码 + 0x80 + 异常码)。
实战经验:在编写采集程序时,建议将功能码、起始地址和读取数量封装为配置项。例如,一个温度采集任务可配置为 {"fc": 0x04, "start": 0x0000, "count": 10},便于后期扩展。