Modbus
目录
概述
帧结构
1. 线圈(Coil)
核心本质:
2. 离散输入(Discrete Input)
核心本质:
3. 保持寄存器(Holding Register)
核心本质:
4. 输入寄存器(Input Register)
核心本质:
1. 为什么需要区分这四种区域?
2. 如何在实际项目中选择?
数据结构
一、位操作(线圈、离散输入,1 位数据)
1. 读线圈状态(功能码 0x01)
2. 读离散输入(功能码 0x02)
3. 写单个线圈(功能码 0x05)
4. 写多个线圈(功能码 0x0F)
二、字操作(保持寄存器、输入寄存器,16 位数据)
1. 读保持寄存器(功能码 0x03)
2. 读输入寄存器(功能码 0x04)
3. 写单个保持寄存器(功能码 0x06)
4. 写多个保持寄存器(功能码 0x10)
三、异常响应(通用规则)
C/C++实现
概述
Modbus是一种串行通信协议,顾名思义,它是一个Bus,即总线协议,是一种广泛应用于工业控制领域的通信协议。它支持多种传输方式:
-
Modbus RTU (二进制格式,常用于串口通信)
-
Modbus ASCII (ASCII字符格式)
-
Modbus TCP (基于以太网的实现)
以上三种协议,一个设备只会有一种协议,如果你的设备使用的是Modbus-RTU,只需查看以下对应部分,一般来说大部分的设备都是Modbus-RTU协议的。
Modbus采用的是主从架构:
-
主站(Master):发起通信请求的设备(通常是PC、HMI或主控制器)
-
从站(Slave):响应请求的设备(PLC、传感器、执行器等)
-
一个网络上可以有1个主站和最多247个从站(Modbus RTU/ASCII)
Modbus 通信方式由主站发起,从站不会主动发送数据,采取一问一答,主站发送请求,从站返回响应,且无连接,每次通信都是独立的,没有持久连接概念。
帧结构
在Modbus RTU中,一个典型的应用就是RS-485串行网络,其帧结构为:
数据以二进制形式发送,每个字节包含 8 位信息,使用循环冗余校验(CRC-16)确保数据完整性。
Modbus 协议通过 “数据区域” 管理设备的数字量(开关)和模拟量(数值),线圈、离散输入、保持寄存器、输入寄存器 是四大核心数据区域。
Modbus 的 “功能码” 本质就是 “操作这些区域的指令”。
1. 线圈(Coil)
核心本质:
模拟 物理继电器的触点状态(只有 开(1/ON) 或 关(0/OFF) 两种状态),属于 数字量输出区域。通常控制外部设备的开关状态(如继电器吸合 / 断开、指示灯亮灭),支持 读(功能码 01) 和 写(功能码 05、0F)。
可以把线圈想象成 “可远程控制的继电器”:
- 主站用 写线圈(05/0F) 发送 “闭合 / 断开” 指令;
- 主站用 读线圈(01) 查询继电器当前是否闭合。
2. 离散输入(Discrete Input)
核心本质:
模拟 物理开关或传感器的输入状态,属于 数字量输入区域。仅支持读(功能码 02)(因为状态由外部硬件决定,主站无法修改)。通常用于读取外部设备的状态(如按钮是否按下、光电开关是否检测到物体)。
可以把离散输入想象成 “传感器的状态反馈”:
- 主站只能用 读离散输入(02) 查询 “按钮是否按下”,但无法通过 Modbus 主动改变按钮状态(因为按钮是物理输入)。
3. 保持寄存器(Holding Register)
核心本质:
存储 16 位整数(或组合成 32 位、浮点数),属于 可读写的模拟量 / 参数区域。支持 读(功能码 03) 和 写(功能码 06、10)。通常用于存储设备的 设定参数(如 PID 控制器的目标值、变频器的频率设定)以及存储设备的 模拟量输出(如电压、电流的设定值)。
可以把保持寄存器想象成 “设备的配置文件”:
- 主站用 写寄存器(06/10) 修改设备参数(如把变频器频率设为 50Hz);
- 主站用 读寄存器(03) 查询当前设定值(如确认频率是否成功设为 50Hz)。
4. 输入寄存器(Input Register)
核心本质:
存储 16 位整数,属于 只读的模拟量输入区域。仅支持读(功能码 04)(因为数据由设备内部采集,主站无法修改)。通常用于读取设备的 实时测量值(如温度传感器的读数、电流互感器的采样值)。
可以把输入寄存器想象成 “设备的仪表盘”:
- 主站只能用 读输入寄存器(04) 查询 “当前温度是多少”,但无法通过 Modbus 直接修改传感器的测量值(因为值由传感器硬件决定)。
总结:
1. 为什么需要区分这四种区域?
- 隔离安全:将控制命令(线圈)与监控数据(输入寄存器)分离,避免误操作。
- 优化性能:位操作(线圈 / 离散输入)按位存储,节省带宽;字操作(寄存器)按字节存储,适合数值处理。
- 功能分工:只读区域(离散输入 / 输入寄存器)用于实时反馈,可写区域(线圈 / 保持寄存器)用于配置控制。
2. 如何在实际项目中选择?
- 控制输出(如继电器、电磁阀)→ 线圈(功能码
05/0F
)。 - 状态输入(如按钮、传感器)→ 离散输入(功能码
02
)。 - 参数配置(如温度设定、PID 参数)→ 保持寄存器(功能码
06/10
)。 - 数据采集(如温度测量、电流值)→ 输入寄存器(功能码
04
)。
数据结构
一、位操作(线圈、离散输入,1 位数据)
1. 读线圈状态(功能码 0x01
)
作用:读取从机的 线圈(0x 区,可读写的开关量) 状态(1=ON,0=OFF)。
主机请求帧结构:
字段 | 字节数 | 含义 | 示例(读从机 1 的 0x0013 开始的 27 个线圈) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x01 | 01 |
起始地址高字节 | 1 | 线圈起始地址的高 8 位(大端序) | 00 (对应地址 0x0013 的高字节) |
起始地址低字节 | 1 | 线圈起始地址的低 8 位(大端序) | 13 (对应地址 0x0013 的低字节) |
数量高字节 | 1 | 读取线圈数量的高 8 位(大端序) | 00 (27 个线圈的高字节 0x001B ) |
数量低字节 | 1 | 读取线圈数量的低 8 位(大端序) | 1B (27 个线圈的低字节 0x001B ) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | 8D (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | C4 (计算结果) |
示例请求帧:01 01 00 13 00 1B 8D C4 |
从机响应帧结构:
字段 | 字节数 | 含义 | 示例(返回 27 个线圈状态:0xCD 0x6B 0xB2 0x05 ) |
---|---|---|---|
从机地址 | 1 | 自身地址(如 0x01 ) | 01 |
功能码 | 1 | 0x01 (与请求一致) | 01 |
数据长度 | 1 | 线圈状态的字节数(ceil(数量/8) ) | 04 (27 个线圈 → 4 字节:27÷8=3.375 ,向上取整为 4) |
线圈状态字节 | n | 每 8 位代表 8 个线圈(低位对应低地址) | CD 6B B2 05 (27 个线圈的二进制状态打包) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | xx (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | xx (计算结果) |
示例响应帧:01 01 04 CD 6B B2 05 xx xx |
2. 读离散输入(功能码 0x02
)
作用:读取从机的 离散输入(1x 区,只读的开关量,如传感器输入) 状态。
帧结构:
- 主机请求:与 “读线圈” 完全一致(仅功能码改为
0x02
)。 - 从机响应:与 “读线圈” 完全一致(数据域为离散输入的状态)。
3. 写单个线圈(功能码 0x05
)
作用:将从机的 单个线圈 置为 ON(0xFF00
)或 OFF(0x0000
)。
主机请求帧(从机响应与请求完全一致,回显确认):
字段 | 字节数 | 含义 | 示例(将从机 1 的 0x001A 线圈置为 ON) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x05 | 05 |
线圈地址高字节 | 1 | 线圈地址的高 8 位(大端序) | 00 (对应地址 0x001A 的高字节) |
线圈地址低字节 | 1 | 线圈地址的低 8 位(大端序) | 1A (对应地址 0x001A 的低字节) |
写入值高字节 | 1 | 0xFF (ON)或 0x00 (OFF) | FF (ON) |
写入值低字节 | 1 | 0x00 (固定,与高字节配合) | 00 |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | AD (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | FD (计算结果) |
示例请求 / 响应帧:01 05 00 1A FF 00 AD FD |
4. 写多个线圈(功能码 0x0F
)
作用:批量设置从机的 多个线圈 状态。
主机请求帧结构:
字段 | 字节数 | 含义 | 示例(将从机 1 的 0x0000~0x0003 线圈置为 ON、OFF、ON、OFF ) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x0F | 0F |
起始地址高字节 | 1 | 线圈起始地址的高 8 位(大端序) | 00 (对应地址 0x0000 的高字节) |
起始地址低字节 | 1 | 线圈起始地址的低 8 位(大端序) | 00 (对应地址 0x0000 的低字节) |
数量高字节 | 1 | 线圈数量的高 8 位(大端序) | 00 (4 个线圈的高字节 0x0004 ) |
数量低字节 | 1 | 线圈数量的低 8 位(大端序) | 04 (4 个线圈的低字节 0x0004 ) |
数据长度 | 1 | 线圈状态的字节数(ceil(数量/8) ) | 01 (4 个线圈 → 1 字节:4÷8=0.5 ,向上取整为 1) |
线圈状态字节 | n | 每 8 位代表 8 个线圈(低位对应低地址) | 55 (二进制 01010101 ,对应 4 个线圈:ON、OFF、ON、OFF ) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | xx (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | xx (计算结果) |
示例请求帧:01 0F 00 00 00 04 01 55 xx xx |
从机响应帧结构:
字段 | 字节数 | 含义 | 示例(确认写入 4 个线圈) |
---|---|---|---|
从机地址 | 1 | 自身地址(如 0x01 ) | 01 |
功能码 | 1 | 0x0F (与请求一致) | 0F |
起始地址高字节 | 1 | 线圈起始地址的高 8 位(大端序) | 00 |
起始地址低字节 | 1 | 线圈起始地址的低 8 位(大端序) | 00 |
数量高字节 | 1 | 线圈数量的高 8 位(大端序) | 00 |
数量低字节 | 1 | 线圈数量的低 8 位(大端序) | 04 |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | xx |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | xx |
示例响应帧:01 0F 00 00 00 04 xx xx |
二、字操作(保持寄存器、输入寄存器,16 位数据)
1. 读保持寄存器(功能码 0x03
)
作用:读取从机的 保持寄存器(4x 区,可读写的模拟量 / 参数) 值(16 位整数,可组合为 32 位、浮点数)。
主机请求帧结构:
字段 | 字节数 | 含义 | 示例(读从机 1 的 0x006B 开始的 2 个寄存器) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x03 | 03 |
起始地址高字节 | 1 | 寄存器起始地址的高 8 位(大端序) | 00 (对应地址 0x006B 的高字节) |
起始地址低字节 | 1 | 寄存器起始地址的低 8 位(大端序) | 6B (对应地址 0x006B 的低字节) |
数量高字节 | 1 | 读取寄存器数量的高 8 位(大端序) | 00 (2 个寄存器的高字节 0x0002 ) |
数量低字节 | 1 | 读取寄存器数量的低 8 位(大端序) | 02 (2 个寄存器的低字节 0x0002 ) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | B5 (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | D7 (计算结果) |
示例请求帧:01 03 00 6B 00 02 B5 D7 |
从机响应帧结构:
字段 | 字节数 | 含义 | 示例(返回 2 个寄存器值:0x00C8 、0x012C ) |
---|---|---|---|
从机地址 | 1 | 自身地址(如 0x01 ) | 01 |
功能码 | 1 | 0x03 (与请求一致) | 03 |
数据长度 | 1 | 寄存器值的总字节数(数量×2 ) | 04 (2 个寄存器 → 2×2=4 字节) |
寄存器值高字节 | 1 | 第一个寄存器的高 8 位(大端序) | 00 (0x00C8 的高字节) |
寄存器值低字节 | 1 | 第一个寄存器的低 8 位(大端序) | C8 (0x00C8 的低字节) |
寄存器值高字节 | 1 | 第二个寄存器的高 8 位(大端序) | 01 (0x012C 的高字节) |
寄存器值低字节 | 1 | 第二个寄存器的低 8 位(大端序) | 2C (0x012C 的低字节) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | 7B (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | 80 (计算结果) |
示例响应帧:01 03 04 00 C8 01 2C 7B 80 |
2. 读输入寄存器(功能码 0x04
)
作用:读取从机的 输入寄存器(3x 区,只读的模拟量输入,如传感器数据) 值。
帧结构:
- 主机请求:与 “读保持寄存器” 完全一致(仅功能码改为
0x04
)。 - 从机响应:与 “读保持寄存器” 完全一致(数据域为输入寄存器的值)。
3. 写单个保持寄存器(功能码 0x06
)
作用:向从机的 单个保持寄存器 写入 16 位数据(如参数设定值)。
主机请求帧(从机响应与请求完全一致,回显确认):
字段 | 字节数 | 含义 | 示例(将从机 1 的 0x0010 寄存器设为 0x0300 ) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x06 | 06 |
寄存器地址高字节 | 1 | 寄存器地址的高 8 位(大端序) | 00 (对应地址 0x0010 的高字节) |
寄存器地址低字节 | 1 | 寄存器地址的低 8 位(大端序) | 10 (对应地址 0x0010 的低字节) |
写入值高字节 | 1 | 写入值的高 8 位(大端序) | 03 (0x0300 的高字节) |
写入值低字节 | 1 | 写入值的低 8 位(大端序) | 00 (0x0300 的低字节) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | 88 (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | FF (计算结果) |
示例请求 / 响应帧:01 06 00 10 03 00 88 FF |
4. 写多个保持寄存器(功能码 0x10
)
作用:批量向从机的 多个保持寄存器 写入 16 位数据(如批量参数配置)。
主机请求帧结构:
字段 | 字节数 | 含义 | 示例(将从机 1 的 0x0000~0x0001 寄存器设为 0x0001 、0x0001 ) |
---|---|---|---|
从机地址 | 1 | 目标从机地址(如 0x01 ) | 01 |
功能码 | 1 | 0x10 | 10 |
起始地址高字节 | 1 | 寄存器起始地址的高 8 位(大端序) | 00 (对应地址 0x0000 的高字节) |
起始地址低字节 | 1 | 寄存器起始地址的低 8 位(大端序) | 00 (对应地址 0x0000 的低字节) |
数量高字节 | 1 | 寄存器数量的高 8 位(大端序) | 00 (2 个寄存器的高字节 0x0002 ) |
数量低字节 | 1 | 寄存器数量的低 8 位(大端序) | 02 (2 个寄存器的低字节 0x0002 ) |
数据长度 | 1 | 数据总字节数(数量×2 ) | 04 (2 个寄存器 → 2×2=4 字节) |
寄存器值 1 高字节 | 1 | 第一个寄存器值的高 8 位(大端序) | 00 (0x0001 的高字节) |
寄存器值 1 低字节 | 1 | 第一个寄存器值的低 8 位(大端序) | 01 (0x0001 的低字节) |
寄存器值 2 高字节 | 1 | 第二个寄存器值的高 8 位(大端序) | 00 (0x0001 的高字节) |
寄存器值 2 低字节 | 1 | 第二个寄存器值的低 8 位(大端序) | 01 (0x0001 的低字节) |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | 63 (计算结果) |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | AF (计算结果) |
示例请求帧:01 10 00 00 00 02 04 00 01 00 01 63 AF |
从机响应帧结构:
字段 | 字节数 | 含义 | 示例(确认写入 2 个寄存器) |
---|---|---|---|
从机地址 | 1 | 自身地址(如 0x01 ) | 01 |
功能码 | 1 | 0x10 (与请求一致) | 10 |
起始地址高字节 | 1 | 寄存器起始地址的高 8 位(大端序) | 00 |
起始地址低字节 | 1 | 寄存器起始地址的低 8 位(大端序) | 00 |
数量高字节 | 1 | 寄存器数量的高 8 位(大端序) | 00 |
数量低字节 | 1 | 寄存器数量的低 8 位(大端序) | 02 |
CRC 低字节 | 1 | CRC 校验的低字节(小端序) | 41 |
CRC 高字节 | 1 | CRC 校验的高字节(小端序) | C8 |
示例响应帧:01 10 00 00 00 02 41 C8 |
三、异常响应(通用规则)
当从机无法执行请求时,返回 异常帧:
- 功能码:原功能码 最高位设为 1(如
0x03
→0x83
)。 - 异常码:1 字节,解释错误原因(如
0x02
表示 “非法数据地址”)。
示例:主站发 0x03
读不存在的寄存器,从机响应:
01 83 02 xx xx
(0x83
是异常功能码,0x02
是 “非法地址” 异常码)。
C/C++实现
第三方库采用最流行的开源Modbus库:libmodbus.(vcpkg集成)
从机模拟工具采用Modbus Slave连接本机:
由于 Modbus RTU 通常基于串口(常见 RS-485、RS-232 )进行通信,需要确保主设备具备串口接口。所以当前用Modbus TCP演示。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h> // libmodbus 头文件#define SLAVE_ID 1 // 从站ID(界面显示 ID=1)
#define START_ADDR 0 // 起始地址(Alias 0 对应 Modbus 地址 0)
#define READ_COUNT 10 // 读取数量(Alias 0~9,共10个寄存器)
#define WRITE_ADDR 0 // 写入地址(Alias 0)
#define WRITE_VALUE 100 // 写入值(示例:100)int main() {modbus_t *ctx = NULL; // Modbus 上下文uint16_t regs[READ_COUNT] = {0}; // 存储读取的寄存器值,16 位无符号整数int rc = -1; // 操作返回值// ---------------------- 1. 创建 Modbus TCP 上下文 ----------------------ctx = modbus_new_tcp("127.0.0.1", 502); // IP:127.0.0.1,端口:502if (ctx == NULL) {fprintf(stderr, "Error: 无法创建 Modbus 上下文!\n%s\n", modbus_strerror(errno));return -1;}// ---------------------- 2. 设置从站 ID ----------------------modbus_set_slave(ctx, SLAVE_ID); // 匹配模拟设备的 ID=1// ---------------------- 3. 连接到模拟设备 ----------------------if (modbus_connect(ctx) == -1) {fprintf(stderr, "Error: 连接失败!\n%s\n", modbus_strerror(errno));modbus_free(ctx); // 释放资源return -1;}printf("✅ 成功连接到 Modbus 从站(ID=%d)\n", SLAVE_ID);// ---------------------- 4. 读取保持寄存器(功能码 0x03) ----------------------rc = modbus_read_registers(ctx, START_ADDR, READ_COUNT, regs);if (rc == -1) {fprintf(stderr, "Error: 读取失败!\n%s\n", modbus_strerror(errno));} else {printf("📖 读取 %d 个寄存器(Alias 0~%d):\n", rc, READ_COUNT-1);for (int i = 0; i < rc; i++) {printf(" Alias %d: %d\n", i, regs[i]);}}// ---------------------- 5. 写入单个保持寄存器(功能码 0x06) ----------------------rc = modbus_write_register(ctx, WRITE_ADDR, WRITE_VALUE);if (rc == -1) {fprintf(stderr, "Error: 写入失败!\n%s\n", modbus_strerror(errno));} else {printf("📝 成功写入 Alias %d:值 = %d\n", WRITE_ADDR, WRITE_VALUE);}// ---------------------- 6. 写入多个保持寄存器(功能码 0x10) ----------------------uint16_t values[READ_COUNT-1]; // 定义要写入的多个值for (int i = 0; i < READ_COUNT-1; i++) {values[i] = WRITE_VALUE + i + 1; // 生成递增的值:101, 102, ..., 109}rc = modbus_write_registers(ctx, START_ADDR+1, READ_COUNT-1, values);if (rc == -1) {fprintf(stderr, "Error: 写入失败!\n%s\n", modbus_strerror(errno));} else {printf("📝 成功写入 %d 个寄存器(Alias %d~%d):\n", rc, START_ADDR+1, START_ADDR + READ_COUNT-1);for (int i = 0; i < rc; i++) {printf(" Alias %d: %d\n", START_ADDR + 1 + i, values[i]);}}// ---------------------- 7. 验证写入结果 ----------------------rc = modbus_read_registers(ctx, START_ADDR, READ_COUNT, regs);if (rc == -1) {fprintf(stderr, "Error: 读取验证失败!\n%s\n", modbus_strerror(errno));} else {printf("🔍 验证结果:\n");for (int i = 0; i < rc; i++) {printf(" Alias %d: %d\n", i, regs[i]);}}// ---------------------- 8. 关闭连接并释放资源 ----------------------modbus_close(ctx);modbus_free(ctx);printf("🔌 连接已关闭\n");return 0;
}
运行结果:
流程总结:
🦊🦊🦊.