Modbus 协议深度解析:变种、功能码与位级细节
工控领域的事实标准协议,自 1979 年由 Modicon(现施耐德电气)发布以来,至今仍是 PLC、传感器、执行器、RTU、IED 之间最广泛使用的通信协议。本文从 变种差异、帧结构、功能码清单、位级细节 四个维度完整拆解 Modbus。
目录
- 协议概述与变种一览
- [Modbus RTU](#Modbus RTU)
- [Modbus ASCII](#Modbus ASCII)
- [Modbus TCP](#Modbus TCP)
- [Modbus RTU-over-TCP(Modbus TCP 变体)](#Modbus RTU-over-TCP(Modbus TCP 变体))
- [Modbus Plus(MB+)](#Modbus Plus(MB+))
- 数据模型与地址空间
- 功能码详解
- 异常码详解
- 功能码位级详细拆解
- [附录:PDU 与 ADU 结构速查](#附录:PDU 与 ADU 结构速查)
1. 协议概述与变种一览
Modbus 是一个 主/从(Master/Slave) 或 客户端/服务器(Client/Server) 协议,物理层可承载于串口、以太网或光纤等介质。协议栈只定义 应用层(PDU) 和 数据链路层封装(ADU),物理层由下层标准负责。
| 变种 | 传输介质 | 帧界定方式 | 最大寻址 | 速率 | 典型场景 |
|---|---|---|---|---|---|
| Modbus RTU | RS-232 / RS-485 | 3.5 字符静默间隔 | 247 从站 | 9600 ~ 115200 bps | 工业现场总线,最广泛 |
| Modbus ASCII | RS-232 / RS-485 | : 起始 + CR/LF 结束 |
247 从站 | 同 RTU 但传输效率 ~50% | 时间宽松、字符调试友好 |
| Modbus TCP | 以太网 (TCP/IP) | 内置长度字段(无校验) | 无限制(IP 层) | 100 Mbps ~ 1 Gbps | SCADA、上位机、跨系统集成 |
| Modbus RTU-over-TCP | 以太网 | 与 RTU 相同 + TCP 流 | 无限制 | 同上 | 串口协议平滑迁移至 TCP |
| Modbus Plus (MB+) | 双绞线(专用) | 令牌传递 | 64 节点 | 1 Mbps | 施耐德 PLC 高速对等网络 |
注:2019 年后施耐德推动术语从 "Master/Slave" 转为 "Client/Server",TCP 变种也建议用 "Client/Server"。本文在 RTU/ASCII 上下文保留 "主/从" 以匹配行业习惯。
2. Modbus RTU
2.1 帧结构(ADU)
一条 RTU 报文由以下四部分组成,顺序为:地址 + PDU + CRC。
| 字段 | 地址 | PDU | CRC 校验 |
|---|---|---|---|
| 大小 | 1 字节 | N 字节(功能码 + 数据) | 2 字节 |
| 说明 | 0x00 = 广播, 1--247 = 从站地址 | 应用层协议数据单元 | CRC-16 (Modbus), 低字节在前 |
帧之间必须有 ≥ 3.5 个字符时间 的静默间隔(Silent Interval),接收方以此判断一帧结束。
波特率与 1.5T / 3.5T 的关系:
1.5T = 1.5 × 字符时间
3.5T = 3.5 × 字符时间
字符时间 = 1 / 波特率 × (数据位 + 起始位 + 停止位 + 校验位)
例:9600 bps, 8N1 → 9.6k / (1+8+1) ≈ 1.04 ms/char
3.5T ≈ 3.64 ms (大多数实现取 3.5 ms 整数倍)
- 地址域:0x00 为广播地址,1--247 为从站地址
- CRC :CRC-16 (Modbus 多项式
x¹⁶ + x¹⁵ + x² + 1,初始值0xFFFF),低字节在前 - 最大报文长度:256 字节
2.2 位级编址特性
RTU 的 PDU 内 多字节整数以 Big-Endian(大端)编码,即高字节在前。
示例:0x1234 → 编码为 0x12 0x34
3. Modbus ASCII
3.1 帧结构
| 字段 | 起始 | 地址 | 功能码 | 数据 (HEX 字符) | LRC | 结束 |
|---|---|---|---|---|---|---|
| 内容 | : (0x3A) |
2 字符 | 2 字符 | N × 2 字符 | 2 字符 | CRLF (0x0D 0x0A) |
每个字节被拆分为两个 ASCII 十六进制字符发送,报文体积是 RTU 的约 2 倍。
3.2 LRC(纵向冗余校验)
LRC = (所有字节的累加和) 的补码,只覆盖地址 + 功能码 + 数据(不包括起始 : 和结束 CRLF)。计算方式:
LRC = (~(byte_sum) + 1) & 0xFF
等价于:LRC = (-byte_sum) & 0xFF
等价于:LRC = (0x100 - byte_sum) & 0xFF
- 发送方:将 LRC 结果以 2 字符 ASCII 追加
- 接收方:从
:后到 LRC 前的所有字节做和(含地址、功能码、数据),并与 LRC 相加,结果应为0x00(模 256)
3.3 与 RTU 的核心差异
| 特性 | RTU | ASCII |
|---|---|---|
| 帧界定 | 静默 3.5T | : 和 CRLF |
| 校验 | CRC-16 (2 字节) | LRC (1 字节) |
| 效率 | ~1 字节/字节 | ~2 字符/字节 |
| 调试 | 二进制,不可读 | 纯 ASCII,可读 |
| 容错 | 静默间隔被噪声触发时可误判 | 字符帧头帧尾明确,更健壮 |
4. Modbus TCP
4.1 帧结构 (ADU)
| MBAP 头 (7 字节) | 功能码 | 数据 |
|---|---|---|
| 事务ID(2) + 协议ID(2) + 长度(2) + 单元ID(1) | 1 字节 | N 字节(同 RTU PDU) |
MBAP(Modbus Application Protocol)Header 各字段详情:
| 字段 | 大小 | 值范围 | 说明 |
|---|---|---|---|
| 事务标识符 (Transaction ID) | 2 字节 | 0x0000--0xFFFF | 客户端生成,用于请求/响应配对 |
| 协议标识符 (Protocol ID) | 2 字节 | 0x0000 | Modbus 协议(其他值保留) |
| 长度 (Length) | 2 字节 | 后续字节数 | 当前报文剩余字节总数(单元标识符 + PDU) |
| 单元标识符 (Unit ID) | 1 字节 | 0x00--0xFF | 原 RTU "从站地址" 的继承,TCP 网关转发时填入实际从站地址 |
关键点:
- 无 CRC:TCP 校验由底层 TCP 协议完成
- 无地址
0x00广播 :TCP 没有真正的广播语义,单元标识符0xFF约定为广播 - MBAP 中的
Length字段使 TCP 流可以正确切分报文,无需静默间隔 - 最大报文长度:260 字节
4.2 PDU 一致性
Modbus TCP 的 PDU(功能码 + 数据) 与 RTU/ASCII 完全一致,这是协议统一性的核心保证:
Modbus TCP PDU = [功能码 | 数据]
Modbus RTU PDU = [功能码 | 数据] // 相同
5. Modbus RTU-over-TCP(Modbus TCP 变体)
也称为 Modbus TCP/IP with RTU framing 或 Raw Modbus over TCP。
将完整的 RTU 帧(含 CRC)直接封装进 TCP 载荷发送,不使用 MBAP 头 。接收方按 RTU 规则解析(静默间隔或 length 预先协商)。常用于串口转以太网模块的透明传输模式。
TCP Payload = [地址 | 功能码 | 数据 | CRC-16]
|<--- 完整的 RTU ADU --->|
注意:RFC 标准建议使用正规 Modbus TCP + MBAP。RTU-over-TCP 是事实存在的兼容方式,但非标准做法。
6. Modbus Plus (MB+)
施耐德电气专有协议,不对外开放帧格式。关键特性:
- 物理层:RS-485 同轴双绞线,1 Mbps
- 介质访问:令牌传递(Token Passing),节点数量 2--64
- 通信模式 :对等(Peer-to-Peer),无需主站
- 可承载 Modbus 消息:MB+ 数据帧内部包含标准 Modbus 应用数据
因 MB+ 帧格式为闭源,本文不做位级展开。需使用专用芯片或施耐德通信模块(如 Modicon MBP-M)接入。
7. 数据模型与地址空间
Modbus 定义了四种基本数据对象:
| 对象类型 | 大小 | 访问类型 | 编号规则 | 传统 PLC 命名 |
|---|---|---|---|---|
| Coil (线圈) | 1 bit | 可读可写 | 00001--09999 | %Q, %M |
| Discrete Input (离散输入) | 1 bit | 只读 | 10001--19999 | %I |
| Input Register (输入寄存器) | 16 bits | 只读 | 30001--39999 | %IW, %AI |
| Holding Register (保持寄存器) | 16 bits | 可读可写 | 40001--49999 | %MW, %QW |
7.1 地址空间说明
重要历史遗留问题 :传统 5 位 PLC 地址(如 40001)对应 协议内实际地址偏移 1。
PLC 地址 40001 → 协议帧中地址字段 = 0x0000
PLC 地址 40002 → 协议帧中地址字段 = 0x0001
...
PLC 地址 40100 → 协议帧中地址字段 = 0x0063
现代实现推荐直接使用 0-based 十六进制地址,避免 5 位 PLC 编号混淆。
7.2 数据打包规则
多字节寄存器(32-bit 整数、浮点数等):
数据模型无强制字节序标准,常见的三种实现:
| 方案 | 32-bit 值 0x12345678 的字节序列 | 说明 |
|---|---|---|
| AB-CD (Big-Endian) | 0x1234 0x5678 | 大部分 PLC 默认 |
| CD-AB (Big-Endian Swap) | 0x5678 0x1234 | 一些 DCS 系统 |
| BA-DC (Little-Endian) | 0x3412 0x7856 | Windows 兼容设备 |
位打包 :线圈/离散输入请求中,响应数据每个字节的 LSB 对应最低起始地址:
例:请求地址 0 读取 8 个线圈,响应字节的位映射:
bit 0 = 地址 0 的线圈
bit 1 = 地址 1 的线圈
...
bit 7 = 地址 7 的线圈
8. 功能码详解
8.1 功能码分类
Modbus 按功能码范围分为三类:
| 范围 | 分类 | 说明 |
|---|---|---|
| 1--64 (0x01--0x40) | 公共功能码 (Public) | 由 Modbus 组织定义并公开文档 |
| 65--72 (0x41--0x48) | 用户定义功能码 | 由用户自行实现,无需审批 |
| 73--119 (0x49--0x77) | 保留 | 用于扩展 |
| 120--127 (0x78--0x7F) | 保留 | 用于内部调试 |
| 128--255 (0x80--0xFF) | 异常响应 | 功能码 + 0x80 标识异常 |
8.2 公共功能码完整清单
位操作(Bit Access)
| 功能码 | 名称 | 操作对象 | 最大操作量 | 广播支持 | 说明 |
|---|---|---|---|---|---|
| 01 (0x01) | Read Coils | Coil (bit) | 2000 线圈 | 否 | 读取 1--N 个线圈状态 |
| 02 (0x02) | Read Discrete Inputs | Discrete Input (bit) | 2000 输入 | 否 | 读取 1--N 个离散输入状态 |
| 05 (0x05) | Write Single Coil | Coil (bit) | 1 线圈 | 是 | 写入一个线圈为 ON 或 OFF |
| 15 (0x0F) | Write Multiple Coils | Coil (bit) | 1968 线圈(最大 246 字节) | 是 | 批量写入多个线圈 |
16 位寄存器操作(16-bit Access)
| 功能码 | 名称 | 操作对象 | 最大操作量 | 广播支持 | 说明 |
|---|---|---|---|---|---|
| 03 (0x03) | Read Holding Registers | Holding Register (16-bit) | 125 寄存器 | 否 | 读取 1--N 个保持寄存器 |
| 04 (0x04) | Read Input Registers | Input Register (16-bit) | 125 寄存器 | 否 | 读取 1--N 个输入寄存器 |
| 06 (0x06) | Write Single Register | Holding Register (16-bit) | 1 寄存器 | 是 | 写入一个保持寄存器 |
| 16 (0x10) | Write Multiple Registers | Holding Register (16-bit) | 123 寄存器(最大 246 字节) | 是 | 批量写入多个保持寄存器 |
| 22 (0x16) | Mask Write Register | Holding Register (16-bit) | 1 寄存器 | 否 | 通过 AND/OR 掩码原子修改单个寄存器 |
| 23 (0x17) | Read/Write Multiple Registers | Holding Register (16-bit) | 读 125 / 写 121 | 否 | 一次事务中先读后写同一地址区域 |
文件记录操作(File Record Access)
| 功能码 | 名称 | 操作对象 | 说明 |
|---|---|---|---|
| 20 (0x14) | Read File Record | 文件子记录 (Sub-record) | 读取最多 1--3 个文件记录,每个 1--1000 寄存器 |
| 21 (0x15) | Write File Record | 文件子记录 (Sub-record) | 写入最多 1--3 个文件记录,每个 ≤ 646 字节 |
诊断与系统(Diagnostic & System)
| 功能码 | 名称 | 说明 |
|---|---|---|
| 07 (0x07) | Read Exception Status | 读取 8 个异常状态位(仅用于串行线路) |
| 08 (0x08) | Diagnostic (子功能码 0x00--0x18) | 通信诊断、计数器复位、监听等 |
| 11 (0x0B) | Get Com Event Counter | 获取通信事件计数器值 |
| 12 (0x0C) | Get Com Event Log | 获取通信事件日志(64 字节) |
| 17 (0x11) | Report Server ID | 返回设备的服务器 ID、运行状态、扩展数据 |
| 24 (0x18) | Read FIFO Queue | 读取 FIFO 队列内容 |
| 43 (0x2B) | Encapsulated Interface Transport | 封装接口传输,用于 CANopen 等上层协议 |
9. 异常码详解
9.1 异常响应结构
当从站无法处理请求时,返回 功能码 + 0x80 + 异常码。
请求: 0x03 0x00 0x64 0x00 0x01 (读保持寄存器)
异常响应:0x83 0x02 (功能码 0x83 = 0x03 | 0x80, 异常码 0x02)
9.2 异常码表
| 码值 | 名称 | 含义 |
|---|---|---|
| 01 (0x01) | ILLEGAL FUNCTION | 功能码不被该从站支持 |
| 02 (0x02) | ILLEGAL DATA ADDRESS | 请求的地址超出范围 |
| 03 (0x03) | ILLEGAL DATA VALUE | 请求的数据值无效(如读取超量) |
| 04 (0x04) | SLAVE DEVICE FAILURE | 从站执行时发生不可恢复错误 |
| 05 (0x05) | ACKNOWLEDGE | 从站已接受请求,但需长时间处理(仅串行) |
| 06 (0x06) | SLAVE DEVICE BUSY | 从站忙,请稍后重试 |
| 08 (0x08) | MEMORY PARITY ERROR | 存储区奇偶校验错误 |
| 10 (0x0A) | GATEWAY PATH UNAVAILABLE | 网关路径不可用 |
| 11 (0x0B) | GATEWAY TARGET DEVICE FAILED TO RESPOND | 目标设备无响应 |
10. 功能码位级详细拆解
本章按功能码逐一拆解 请求 PDU 和 响应 PDU 的每个字节、每个字段的每一位含义。
10.1 功能码 01 (0x01) --- Read Coils
请求 PDU:3 个字段,共 5 字节
| 字段 | 字节偏移 | 大小 | 范围 | 说明 |
|---|---|---|---|---|
| 功能码 | 0 | 1 字节 | 0x01 | |
| 起始地址 | 1--2 | 2 字节 | 0x0000--0xFFFF | 大端,PLC 地址偏移 -1 后的协议地址 |
| 线圈数量 (N) | 3--4 | 2 字节 | 0x0001--0x07D0 (1--2000) | 最大 2000 个线圈 |
响应 PDU:功能码 + 字节计数 + 线圈数据
| 字段 | 字节偏移 | 大小 | 说明 |
|---|---|---|---|
| 功能码 | 0 | 1 字节 | 0x01 |
| 字节计数 (M) | 1 | 1 字节 | N ÷ 8 向上取整(例: N=10 → M=2) |
| 线圈状态 | 2 ~ 2+M-1 | M 字节 | 每 1 bit 表示一个线圈状态 |
线圈位映射:每个字节 8 个线圈,LSB 对应最低地址。
字节内位排列(LSB 在右,对应最低地址):
b7 b6 b5 b4 b3 b2 b1 b0
↑ ↑
地址 base+7 ... 地址 base+1 地址 base+0
b0 = 起始地址 + 0 的线圈状态
b1 = 起始地址 + 1 的线圈状态
b2 = 起始地址 + 2 的线圈状态
...
b7 = 起始地址 + 7 的线圈状态
未使用的位(超出 N 的部分)= 0(必须置 0)
线圈状态值:
| 位值 | 状态 |
|---|---|
| 1 (0x01) | ON (闭合) |
| 0 (0x00) | OFF (断开) |
示例 :请求读取地址 0x0000 开始的 10 个线圈
请求:0x01 0x00 0x00 0x00 0x0A
返回:0x01 0x02 0xCD 0x01
0xCD = 1100 1101 (二进制)
b0 = 1 → 地址 0x0000 (ON)
b1 = 0 → 地址 0x0001 (OFF)
b2 = 1 → 地址 0x0002 (ON)
b3 = 1 → 地址 0x0003 (ON)
b4 = 0 → 地址 0x0004 (OFF)
b5 = 0 → 地址 0x0005 (OFF)
b6 = 1 → 地址 0x0006 (ON)
b7 = 1 → 地址 0x0007 (ON)
0x01 = 0000 0001 (二进制)
b0 = 1 → 地址 0x0008 (ON)
b1~b7 未使用,应为 0
10.2 功能码 02 (0x02) --- Read Discrete Inputs
请求 PDU:与功能码 01 完全相同,仅功能码不同
| 字段 | 大小 | 范围 |
|---|---|---|
| 功能码: 0x02 | 1 字节 | |
| 起始地址 | 2 字节 | 0x0000--0xFFFF |
| 输入数量 (N) | 2 字节 | 0x0001--0x07D0 (1--2000) |
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x02 | 1 字节 | |
| 字节计数 (M) | 1 字节 | N ÷ 8 向上取整 |
| 输入状态数据 | M 字节 | 位映射同 Read Coils (LSB = 最低地址) |
限制 :最大读取 2000 个离散输入。
位值含义:
| 位值 | 状态 |
|---|---|
| 1 | 输入为 HIGH (闭合) |
| 0 | 输入为 LOW (断开) |
10.3 功能码 03 (0x03) --- Read Holding Registers
请求 PDU:
| 字段 | 大小 | 范围 |
|---|---|---|
| 功能码: 0x03 | 1 字节 | |
| 起始地址 | 2 字节 (大端) | 0x0000--0xFFFF |
| 寄存器数量 (N) | 2 字节 (大端) | 0x0001--0x007D (1--125) |
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x03 | 1 字节 | |
| 字节计数 | 1 字节 | N × 2 |
| 寄存器数据 | N × 2 字节 | 大端,每寄存器 2 字节 |
寄存器数据格式(大端序,高字节在前):
寄存器值 = 0x12AB → 0x12 (高字节) 0xAB (低字节)
多位数据打包示例(32-bit IEEE 754 浮点 123.456 = 0x42F6E979):
大端序 (AB-CD) : 寄存器 0 = 0x42F6, 寄存器 1 = 0xE979
字节序 (Big-Endian) : 0x42 0xF6 0xE9 0x79
10.4 功能码 04 (0x04) --- Read Input Registers
请求 PDU:同功能码 03,仅功能码不同
| 字段 | 大小 | 范围 |
|---|---|---|
| 功能码: 0x04 | 1 字节 | |
| 起始地址 | 2 字节 (大端) | 0x0000--0xFFFF |
| 寄存器数量 | 2 字节 (大端) | 0x0001--0x007D (1--125) |
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x04 | 1 字节 | |
| 字节计数 | 1 字节 | N × 2 |
| 寄存器数据 | N × 2 字节 | 大端序 |
与 Holding Register 的区别:
| 特性 | 03 Holding Register | 04 Input Register |
|---|---|---|
| 存取类型 | 可读可写 | 只读 |
| 物理来源 | RAM / 逻辑输出映像 | 物理传感器 / 模拟量输入通道 |
| 典型用途 | 配置参数、累计值、控制输出 | 温度、压力、流量、AD 采样值 |
| 可写操作 | 06, 16, 22, 23 | 无 |
10.5 功能码 05 (0x05) --- Write Single Coil
请求 PDU:
| 字段 | 大小 | 范围 | 说明 |
|---|---|---|---|
| 功能码: 0x05 | 1 字节 | ||
| 线圈地址 | 2 字节 (大端) | 0x0000--0xFFFF | |
| 线圈值 | 2 字节 (大端) | 0xFF00 或 0x0000 | 见下方线圈值编码 |
线圈值编码 --- 这是最常被误解的字段之一:
| 写入值 (十六进制) | 含义 | 位模式 |
|---|---|---|
| 0xFF00 | ON (闭合) | 1111 1111 0000 0000 |
| 0x0000 | OFF (断开) | 0000 0000 0000 0000 |
任何其他值都是非法 。标准强制要求使用 0xFF00 和 0x0000 这两个精确值。
为什么使用 0xFF00 而不是 0x0001?
历史原因:早期机电式 PLC 输出模块通过检测高字节 0xFF(全 1)来触发继电器动作,低字节 0x00 保证与传统继电器的兼容性。这个设计沿用至今。
响应 PDU :完全回显请求 PDU(正常响应就是请求的原始内容)。
广播支持:当地址域为 0x00 时,所有从站同时执行写入,从站不返回响应。
10.6 功能码 06 (0x06) --- Write Single Register
请求 PDU:
| 字段 | 大小 | 范围 | 说明 |
|---|---|---|---|
| 功能码: 0x06 | 1 字节 | ||
| 寄存器地址 | 2 字节 (大端) | 0x0000--0xFFFF | |
| 寄存器值 | 2 字节 (大端) | 0x0000--0xFFFF | 写入的 16 位数据 |
响应 PDU:完全回显请求。
典型用途:
- 设置设定值 (Setpoint)
- 控制电机启停(通过寄存器位)
- 写入配置参数(Modbus 寄存器映射)
10.7 功能码 07 (0x07) --- Read Exception Status
仅在 串行线路 (RTU/ASCII) 有效,TCP 不支持。
请求 PDU :仅 1 字节 0x07,无数据字段。
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x07 | 1 字节 | |
| 异常状态字节 | 1 字节 | 8 个状态位 |
异常状态字节位含义(由设备厂商定义,以下为最常见约定):
| 位 | 含义 |
|---|---|
| b0 | 从站状态是否正常 (1=正常) |
| b1 | 通信事件缓冲区是否满 (1=满) |
| b2 | 当前是否有未处理的异常 (1=有) |
| b3--b7 | 厂商自定义,或未使用置 0 |
10.8 功能码 08 (0x08) --- Diagnostic
请求 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x08 | 1 字节 | |
| 子功能码 | 2 字节 (大端) | 0x0000--0x0018 |
| 数据 | 2 字节 (大端) | 取决于子功能 |
响应 PDU:与请求结构一致(回显 + 处理结果数据)。
子功能码速查表:
| 子功能码 | 名称 | 数据字段说明 |
|---|---|---|
| 0x0000 | Return Query Data | 回显请求中的数据(环回测试) |
| 0x0001 | Restart Communications Option | 0x0000 = 无变化重启;0xFF00 = 清除日志后重启 |
| 0x0002 | Return Diagnostic Register | 返回 16 位诊断寄存器值 |
| 0x0003 | Change ASCII Input Delimiter | 设置 ASCII 报文结束符(默认 0x0A) |
| 0x0004 | Force Listen Only Mode | 从站进入仅监听模式(不可恢复,需复位) |
| 0x000A | Clear Counters and Diagnostic Register | 清零所有通信计数器 |
| 0x000B | Return Bus Message Count | 返回接收到的总报文数 |
| 0x000C | Return Bus Communication Error Count | 返回 CRC/帧错误计数 |
| 0x000D | Return Bus Exception Error Count | 返回产生的异常响应计数 |
| 0x000E | Return Server Message Count | 返回从站发出的报文总数 |
| 0x000F | Return Server No Response Count | 返回从站未响应(广播除外)的计数 |
| 0x0010 | Return Server NAK Count | 返回收到非法请求计数 |
| 0x0011 | Return Server Busy Count | 返回从站忙计数 |
| 0x0012 | Return Bus Character Overrun Count | 返回字符超限计数(仅串行线路) |
| 0x0014 | Clear Overrun Counter and Flag | 清除超限计数器和标志 |
| 0x0015--0x0018 | 保留 | 用于扩展诊断 |
子功能码 0x0000 环回测试示例:
请求:0x08 0x00 0x00 0xA5 0x5A
响应:0x08 0x00 0x00 0xA5 0x5A
10.9 功能码 15 (0x0F) --- Write Multiple Coils
请求 PDU:
| 字段 | 大小 | 范围 / 说明 |
|---|---|---|
| 功能码: 0x0F | 1 字节 | |
| 起始地址 | 2 字节 (大端) | 0x0000--0xFFFF |
| 线圈数量 (N) | 2 字节 (大端) | 0x0001--0x07B0 (1--1968) |
| 字节计数 (M) | 1 字节 | N ÷ 8 向上取整,1--246 |
| 线圈数据 | M 字节 | 每 1 bit 一个线圈,LSB = 最低地址 |
线圈数据位打包:
例:N = 20 个线圈,M = 3 字节
字节 0: 线圈 0--7
字节 1: 线圈 8--15
字节 2: 线圈 16--19 (b0--b3 有效,b4--b7 = 0)
响应 PDU(正常):
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x0F | 1 字节 | |
| 起始地址 | 2 字节 | 回显请求的起始地址 |
| 线圈数量 | 2 字节 | 回显请求的线圈数量 |
广播支持:地址域 = 0x00 时广播。
10.10 功能码 16 (0x10) --- Write Multiple Registers
请求 PDU:
| 字段 | 大小 | 范围 / 说明 |
|---|---|---|
| 功能码: 0x10 | 1 字节 | |
| 起始地址 | 2 字节 (大端) | 0x0000--0xFFFF |
| 寄存器数量 (N) | 2 字节 (大端) | 0x0001--0x007C (1--123) |
| 字节计数 (M) | 1 字节 | N × 2,1--246 |
| 寄存器值 | N × 2 字节 (大端) | 每寄存器 2 字节,高字节在前 |
响应 PDU(正常):
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x10 | 1 字节 | |
| 起始地址 | 2 字节 | 回显请求 |
| 寄存器数量 | 2 字节 | 回显请求 |
广播支持:地址域 = 0x00 时广播。
10.11 功能码 22 (0x16) --- Mask Write Register
这是 Modbus 中最原子化 的位操作功能码:通过 AND 掩码和 OR 掩码在 单次总线事务 中修改寄存器的指定位,无需"读-改-写"三步。
请求 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x16 | 1 字节 | |
| 引用地址 | 2 字节 (大端) | 目标寄存器地址 |
| AND_MASK | 2 字节 (大端) | AND 掩码 |
| OR_MASK | 2 字节 (大端) | OR 掩码 |
计算公式:
寄存器新值 = (当前值 & AND_MASK) | (OR_MASK & ~AND_MASK)
位逻辑解释:
AND_MASK = 0 的位 → 该位被 OR_MASK 覆盖
AND_MASK = 1 的位 → 该位保留原值(OR_MASK 无效)
最终效果:
- AND_MASK 为 0 的位 → 由 OR_MASK 决定(OR_MASK=1 则置 1,=0 则置 0)
- AND_MASK 为 1 的位 → 保持原值不变
示例:将寄存器 bit 5 置 1,bit 3 清 0,其余位不变
AND_MASK = 0xFFD7 (二进制: 1101 1111 1101 0111)
bit 3 = 0 → 允许 OR_MASK 覆盖
bit 5 = 0 → 允许 OR_MASK 覆盖
其余位 = 1 → 保留原值
OR_MASK = 0x0020 (二进制: 0000 0000 0010 0000)
bit 5 = 1 → 将 bit 5 置 1
bit 3 = 0 → bit 3 被置 0 (AND_MASK=0 且 OR_MASK=0)
响应 PDU:完全回显请求 PDU。
典型应用:
- 原子式修改 PLC 输出映像字的单个或多个位
- 同时设置/清除标志寄存器中的不同标志位
- 多线程/多主站环境下避免读-改-写竞争
10.12 功能码 23 (0x17) --- Read/Write Multiple Registers
在同一事务中 先读后写 ,读写操作可以重叠同一地址。
请求 PDU:
| 字段 | 大小 | 范围 / 说明 |
|---|---|---|
| 功能码: 0x17 | 1 字节 | |
| 读起始地址 | 2 字节 (大端) | |
| 读寄存器数量 (NR) | 2 字节 (大端) | 0x0001--0x007D (1--125) |
| 写起始地址 | 2 字节 (大端) | |
| 写寄存器数量 (NW) | 2 字节 (大端) | 0x0001--0x0079 (1--121) |
| 写字节计数 | 1 字节 | NW × 2 |
| 写寄存器值 | NW × 2 字节 (大端) | 要写入的数据 |
关键语义 :设备先执行读操作,再执行写操作。即便读写地址重叠,写入值也不影响当前读取的返回值。
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x17 | 1 字节 | |
| 读字节计数 | 1 字节 | NR × 2 |
| 读到的寄存器数据 | NR × 2 字节 (大端) | 执行写入前的寄存器值 |
典型应用:PLC 扫描周期内原子式更新控制参数并读取反馈。
10.13 功能码 20 (0x14) --- Read File Record
用于读写 Modbus 设备内存中抽象的 文件记录(File Record) 。每个文件由 1--10000 个 16 位寄存器 组成。
请求 PDU:功能码 + 1--3 个子请求
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x14 | 1 字节 | |
| 子请求 1 | 7 字节 | 见下方子请求结构 |
| 子请求 2 (可选) | 7 字节 | |
| 子请求 3 (可选) | 7 字节 |
每个子请求结构:
| 字段 | 大小 | 范围 / 说明 |
|---|---|---|
| 引用类型 | 1 字节 | 0x06 (固定值,标准文件记录引用) |
| 文件号 | 2 字节 (大端) | 0x0000--0xFFFF |
| 记录起始地址 | 2 字节 (大端) | 文件内偏移 |
| 记录长度 | 2 字节 (大端) | 0x0001--0x03E8 (1--1000 寄存器) |
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x14 | 1 字节 | |
| 文件响应 1 | 可变 | 见下方文件响应结构 |
| 文件响应 2... | 可变 |
每个文件响应结构:
| 字段 | 大小 | 说明 |
|---|---|---|
| 响应长度 | 1 字节 | 该子请求响应字节总数 |
| 引用类型 | 1 字节 | 回显 0x06 |
| 记录数据 | 响应长度 - 1 字节 | 每个寄存器 2 字节,大端 |
10.14 功能码 24 (0x18) --- Read FIFO Queue
从设备的 FIFO 队列中读取数据,FIFO 深度通过 FIFO Count Register 指示。
请求 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x18 | 1 字节 | |
| FIFO 计数值寄存器地址 | 2 字节 (大端) | 存放当前 FIFO 深度的寄存器地址 |
响应 PDU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x18 | 1 字节 | |
| 数据长度 | 2 字节 (大端) | 当前响应剩余字节总数(队列计数 + 数据) |
| FIFO 队列计数 | 2 字节 (大端) | 队列中的数据条目数 |
| FIFO 数据 | 可变 | 按 FIFO 顺序排列的寄存器值,每 2 字节大端 |
限制 :一次最多读取 31 个寄存器 + 队列计数 = 64 字节数据长度。
10.15 功能码 43 (0x2B) / MEI 类型 14 (0x0E) --- Encapsulated Interface Transport
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码: 0x2B | 1 字节 | |
| MEI 类型 | 1 字节 | 0x0E = CANopen, 0x0D = 其他 MEI |
| MEI 数据 | N 字节 | 取决于上层协议 |
最广泛使用的子功能是 0x0E (CANopen 网关)。
11. 附录:PDU 与 ADU 结构速查
11.1 PDU (Protocol Data Unit)
PDU = 协议独立的数据单元,所有变种一致。
| 字段 | 大小 | 说明 |
|---|---|---|
| 功能码 | 1 字节 | 0x01--0x2B (公共码) |
| 数据 | N 字节 | 取决于功能码 |
11.2 ADU (Application Data Unit)
ADU = PDU + 变种特定的封装。
Modbus RTU ADU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 地址 | 1 字节 | 0x00 = 广播, 1--247 = 从站地址 |
| PDU | N 字节 | 功能码 + 数据 |
| CRC-16 | 2 字节 | 低字节在前 |
| 最大总长 | 256 字节 |
Modbus ASCII ADU:
| 字段 | 大小 | 说明 |
|---|---|---|
| 起始 | 1 字符 | : (0x3A) |
| 地址 | 2 字符 | 十六进制 ASCII |
| PDU | N × 2 字符 | 每个字节展开为 2 个 HEX 字符 |
| LRC | 2 字符 | 纵向冗余校验 |
| 结束 | 2 字符 | CR (0x0D) + LF (0x0A) |
| 最大总长 | 513 字符 |
Modbus TCP ADU:
| 字段 | 大小 | 说明 |
|---|---|---|
| MBAP 头 | 7 字节 | 事务ID(2) + 协议ID(2) + 长度(2) + 单元ID(1) |
| PDU | N 字节 | 功能码 + 数据 |
| 最大总长 | 260 字节 | 无 CRC (TCP 层负责) |
11.3 CRC-16 (Modbus) 计算伪代码
function crc16_modbus(data: byte[]): uint16 {
crc = 0xFFFF
for each byte in data {
crc ^= byte
for i in 0..7 {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001 // 0xA001 = reverse of 0x8005
} else {
crc = crc >> 1
}
}
}
return crc
}
发送顺序:低字节在前,高字节在后。
例:CRC = 0x374B → 发送顺序 0x4B 0x37
11.4 LRC 计算伪代码
function lrc(data: byte[]): byte {
sum = 0
for each byte in data {
sum += byte
}
return ((sum ^ 0xFF) + 1) & 0xFF // 补码
}
参考
- MODBUS Application Protocol Specification V1.1b3
- MODBUS over Serial Line Specification and Implementation Guide V1.02
- MODBUS Messaging on TCP/IP Implementation Guide V1.0b
- Open Modbus Specification
版权声明:本文档中的功能码、异常码、帧结构定义均来自 Modbus.org 公开标准文档。文中所有位级分析和示例均为原创整理,转载请注明出处。