七、功能码与数据模型
7.1 四种数据表
Modbus 定义四种独立的数据表,不依赖传输方式(RTU/TCP通用):
| 数据表 | 数据类型 | 访问权限 | 读功能码 | 写功能码 | 数据类型数量 |
|---|---|---|---|---|---|
| 离散输入 (Discrete Input) | 1位 | 只读 | 02 | - | 1-65536 |
| 线圈 (Coil) | 1位 | 读写 | 01 | 05(单)/15(多) | 1-65536 |
| 输入寄存器 (Input Register) | 16位 | 只读 | 04 | - | 1-65536 |
| 保持寄存器 (Holding Register) | 16位 | 读写 | 03 | 06(单)/16(多) | 1-65536 |
地址范围:理论上 0x0000 ~ 0xFFFF(0-65535),某些实现使用 1-based 地址(需转换)。
7.2 基础功能码详解
7.2.1 功能码 01:读线圈
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x01 |
| 起始地址 | 2字节 | 0x0000 ~ 0xFFFF |
| 线圈数量 | 2字节 | 1 ~ 2000 |
响应PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x01 |
| 字节数 | 1字节 | N(=ceil(线圈数/8)) |
| 线圈状态 | N字节 | 每bit对应一个线圈,最低有效位对应起始地址 |
线圈打包规则:
-
线圈0(起始地址)→ 字节0的bit0
-
线圈7 → 字节0的bit7
-
线圈8 → 字节1的bit0
7.2.2 功能码 03:读保持寄存器
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x03 |
| 起始地址 | 2字节 | 0x0000 ~ 0xFFFF |
| 寄存器数量 | 2字节 | 1 ~ 125 |
响应PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x03 |
| 字节数 | 1字节 | N(=2 × 寄存器数) |
| 寄存器值 | N字节 | 每寄存器2字节,大端序 |
7.2.3 功能码 06:写单个寄存器
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x06 |
| 寄存器地址 | 2字节 | 0x0000 ~ 0xFFFF |
| 寄存器值 | 2字节 | 任意 |
响应PDU:回显请求(功能码+地址+值)
7.2.4 功能码 16 (0x10):写多个寄存器
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x10 |
| 起始地址 | 2字节 | 0x0000 ~ 0xFFFF |
| 寄存器数量 | 2字节 | 1 ~ 123 |
| 字节数 | 1字节 | 2 × 寄存器数量 |
| 寄存器值 | N字节 | 每寄存器2字节 |
响应PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x10 |
| 起始地址 | 2字节 | 回显 |
| 寄存器数量 | 2字节 | 回显 |
7.3 高级功能码详解
7.3.1 功能码 23 (0x17):读/写多个寄存器(原子操作)
用途:同时修改和读取,保证中间状态不被其他客户端干扰(如电机位置控制、PID参数调整)。
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x17 |
| 读起始地址 | 2字节 | - |
| 读数量 | 2字节 | 1 ~ 125 |
| 写起始地址 | 2字节 | - |
| 写数量 | 2字节 | 1 ~ 121 |
| 写字节数 | 1字节 | 2 × 写数量 |
| 写数据 | N字节 | 待写入值 |
响应PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x17 |
| 字节数 | 1字节 | 2 × 读数量 |
| 读数据 | N字节 | 读取的寄存器值 |
示例:先写寄存器0x0000=0x1234,0x0001=0x5678,再读0x0002开始的2个寄存器。
7.3.2 功能码 43 (0x2B):设备识别
用途:获取设备信息(厂商名、产品代码、版本号等)。
子功能码 14 (0x0E):读设备标识
请求PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x2B |
| 子功能码 | 1字节 | 0x0E |
| 读取类型 | 1字节 | 01=基本, 02=常规, 03=扩展 |
| 对象ID | 1字节 | 起始对象ID |
| 数量 | 2字节 | 要读取的对象数量 |
响应PDU:
| 字段 | 长度 | 值 |
|---|---|---|
| 功能码 | 1字节 | 0x2B |
| 子功能码 | 1字节 | 0x0E |
| 更多标志 | 1字节 | 0x00=无更多, 0xFF=还有 |
| 下一对象ID | 1字节 | - |
| 对象数量 | 1字节 | 返回的对象数 |
| 对象列表 | 变长 | 每对象:ID+长度+数据 |
预定义对象ID:
| ID | 说明 | 示例 |
|---|---|---|
| 0x00 | 厂商名 | "Schneider Electric" |
| 0x01 | 产品代码 | "TM221CE40R" |
| 0x02 | 版本号 | "V2.1.0" |
| 0x03-0x7F | 保留 | - |
| 0x80-0xFF | 用户自定义 | - |
7.3.3 功能码 08 (0x08):诊断(仅串行线)
子功能码:
| 子功能码 | 说明 | Modbus TCP替代 |
|---|---|---|
| 0x0000 | 回显请求/响应 | TCP ping |
| 0x000A | 计数清零 | 无 |
7.4 异常响应
当服务器/从站无法处理请求时,返回异常响应:
异常响应PDU:
| 字段 | 长度 | 值 |
|-----|-----|------|------|
| 功能码 | 1字节 | 原功能码 | 0x80 |
| 异常码 | 1字节 | 见下表 |
| 异常码 | 名称 | 说明 | 常见原因 |
|---|---|---|---|
| 0x01 | 非法功能 | 不支持该功能码 | 读只写寄存器、功能码不存在 |
| 0x02 | 非法数据地址 | 寄存器地址超范围 | 地址超出设备支持范围 |
| 0x03 | 非法数据值 | 写入值不合法 | 写入值超出允许范围、数量为0 |
| 0x04 | 从站设备故障 | 设备内部错误 | 硬件故障、校验错(已过CRC) |
| 0x05 | 确认 | 正在处理长命令 | 少见,响应后需等待 |
| 0x06 | 从站忙 | 设备正忙 | RS485总线上尝试重试 |
| 0x07 | 否认 | 无法执行 | 权限不足、条件不满足 |
| 0x08 | 内存奇偶错误 | 内存错误 | 极少见(老旧设备) |
| 0x0A | 网关路径不可用 | TCP网关错误 | 单元ID无效 |
| 0x0B | 网关目标无响应 | TCP网关超时 | 下游RTU设备无响应 |
异常响应示例(RTU格式):
请求: 01 05 00 00 00 02 CRC(写线圈地址0x0000,值0x02无效)
响应: 01 85 03 CRC(85=05|0x80, 03=非法数据值)