当前位置: 首页 > news >正文

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目标从机地址(如 0x0101
功能码10x0101
起始地址高字节1线圈起始地址的高 8 位(大端序)00(对应地址 0x0013 的高字节)
起始地址低字节1线圈起始地址的低 8 位(大端序)13(对应地址 0x0013 的低字节)
数量高字节1读取线圈数量的高 8 位(大端序)00(27 个线圈的高字节 0x001B
数量低字节1读取线圈数量的低 8 位(大端序)1B(27 个线圈的低字节 0x001B
CRC 低字节1CRC 校验的低字节(小端序)8D(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)C4(计算结果)
示例请求帧01 01 00 13 00 1B 8D C4

从机响应帧结构

字段字节数含义示例(返回 27 个线圈状态:0xCD 0x6B 0xB2 0x05
从机地址1自身地址(如 0x0101
功能码10x01(与请求一致)01
数据长度1线圈状态的字节数(ceil(数量/8)04(27 个线圈 → 4 字节:27÷8=3.375,向上取整为 4)
线圈状态字节n每 8 位代表 8 个线圈(低位对应低地址)CD 6B B2 05(27 个线圈的二进制状态打包)
CRC 低字节1CRC 校验的低字节(小端序)xx(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)xx(计算结果)
示例响应帧01 01 04 CD 6B B2 05 xx xx
2. 读离散输入(功能码 0x02

作用:读取从机的 离散输入(1x 区,只读的开关量,如传感器输入) 状态。

帧结构

  • 主机请求:与 “读线圈” 完全一致(仅功能码改为 0x02)。
  • 从机响应:与 “读线圈” 完全一致(数据域为离散输入的状态)。
3. 写单个线圈(功能码 0x05

作用:将从机的 单个线圈 置为 ON(0xFF00)或 OFF(0x0000)。

主机请求帧(从机响应与请求完全一致,回显确认)

字段字节数含义示例(将从机 1 的 0x001A 线圈置为 ON)
从机地址1目标从机地址(如 0x0101
功能码10x0505
线圈地址高字节1线圈地址的高 8 位(大端序)00(对应地址 0x001A 的高字节)
线圈地址低字节1线圈地址的低 8 位(大端序)1A(对应地址 0x001A 的低字节)
写入值高字节10xFF(ON)或 0x00(OFF)FF(ON)
写入值低字节10x00(固定,与高字节配合)00
CRC 低字节1CRC 校验的低字节(小端序)AD(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)FD(计算结果)
示例请求 / 响应帧01 05 00 1A FF 00 AD FD
4. 写多个线圈(功能码 0x0F

作用:批量设置从机的 多个线圈 状态。

主机请求帧结构

字段字节数含义示例(将从机 1 的 0x0000~0x0003 线圈置为 ON、OFF、ON、OFF
从机地址1目标从机地址(如 0x0101
功能码10x0F0F
起始地址高字节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 低字节1CRC 校验的低字节(小端序)xx(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)xx(计算结果)
示例请求帧01 0F 00 00 00 04 01 55 xx xx

从机响应帧结构

字段字节数含义示例(确认写入 4 个线圈)
从机地址1自身地址(如 0x0101
功能码10x0F(与请求一致)0F
起始地址高字节1线圈起始地址的高 8 位(大端序)00
起始地址低字节1线圈起始地址的低 8 位(大端序)00
数量高字节1线圈数量的高 8 位(大端序)00
数量低字节1线圈数量的低 8 位(大端序)04
CRC 低字节1CRC 校验的低字节(小端序)xx
CRC 高字节1CRC 校验的高字节(小端序)xx
示例响应帧01 0F 00 00 00 04 xx xx

二、字操作(保持寄存器、输入寄存器,16 位数据)

1. 读保持寄存器(功能码 0x03

作用:读取从机的 保持寄存器(4x 区,可读写的模拟量 / 参数) 值(16 位整数,可组合为 32 位、浮点数)。

主机请求帧结构

字段字节数含义示例(读从机 1 的 0x006B 开始的 2 个寄存器)
从机地址1目标从机地址(如 0x0101
功能码10x0303
起始地址高字节1寄存器起始地址的高 8 位(大端序)00(对应地址 0x006B 的高字节)
起始地址低字节1寄存器起始地址的低 8 位(大端序)6B(对应地址 0x006B 的低字节)
数量高字节1读取寄存器数量的高 8 位(大端序)00(2 个寄存器的高字节 0x0002
数量低字节1读取寄存器数量的低 8 位(大端序)02(2 个寄存器的低字节 0x0002
CRC 低字节1CRC 校验的低字节(小端序)B5(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)D7(计算结果)
示例请求帧01 03 00 6B 00 02 B5 D7

从机响应帧结构

字段字节数含义示例(返回 2 个寄存器值:0x00C80x012C
从机地址1自身地址(如 0x0101
功能码10x03(与请求一致)03
数据长度1寄存器值的总字节数(数量×204(2 个寄存器 → 2×2=4 字节)
寄存器值高字节1第一个寄存器的高 8 位(大端序)000x00C8 的高字节)
寄存器值低字节1第一个寄存器的低 8 位(大端序)C80x00C8 的低字节)
寄存器值高字节1第二个寄存器的高 8 位(大端序)010x012C 的高字节)
寄存器值低字节1第二个寄存器的低 8 位(大端序)2C0x012C 的低字节)
CRC 低字节1CRC 校验的低字节(小端序)7B(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)80(计算结果)
示例响应帧01 03 04 00 C8 01 2C 7B 80
2. 读输入寄存器(功能码 0x04

作用:读取从机的 输入寄存器(3x 区,只读的模拟量输入,如传感器数据) 值。

帧结构

  • 主机请求:与 “读保持寄存器” 完全一致(仅功能码改为 0x04)。
  • 从机响应:与 “读保持寄存器” 完全一致(数据域为输入寄存器的值)。
3. 写单个保持寄存器(功能码 0x06

作用:向从机的 单个保持寄存器 写入 16 位数据(如参数设定值)。

主机请求帧(从机响应与请求完全一致,回显确认)

字段字节数含义示例(将从机 1 的 0x0010 寄存器设为 0x0300
从机地址1目标从机地址(如 0x0101
功能码10x0606
寄存器地址高字节1寄存器地址的高 8 位(大端序)00(对应地址 0x0010 的高字节)
寄存器地址低字节1寄存器地址的低 8 位(大端序)10(对应地址 0x0010 的低字节)
写入值高字节1写入值的高 8 位(大端序)030x0300 的高字节)
写入值低字节1写入值的低 8 位(大端序)000x0300 的低字节)
CRC 低字节1CRC 校验的低字节(小端序)88(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)FF(计算结果)
示例请求 / 响应帧01 06 00 10 03 00 88 FF
4. 写多个保持寄存器(功能码 0x10

作用:批量向从机的 多个保持寄存器 写入 16 位数据(如批量参数配置)。

主机请求帧结构

字段字节数含义示例(将从机 1 的 0x0000~0x0001 寄存器设为 0x00010x0001
从机地址1目标从机地址(如 0x0101
功能码10x1010
起始地址高字节1寄存器起始地址的高 8 位(大端序)00(对应地址 0x0000 的高字节)
起始地址低字节1寄存器起始地址的低 8 位(大端序)00(对应地址 0x0000 的低字节)
数量高字节1寄存器数量的高 8 位(大端序)00(2 个寄存器的高字节 0x0002
数量低字节1寄存器数量的低 8 位(大端序)02(2 个寄存器的低字节 0x0002
数据长度1数据总字节数(数量×204(2 个寄存器 → 2×2=4 字节)
寄存器值 1 高字节1第一个寄存器值的高 8 位(大端序)000x0001 的高字节)
寄存器值 1 低字节1第一个寄存器值的低 8 位(大端序)010x0001 的低字节)
寄存器值 2 高字节1第二个寄存器值的高 8 位(大端序)000x0001 的高字节)
寄存器值 2 低字节1第二个寄存器值的低 8 位(大端序)010x0001 的低字节)
CRC 低字节1CRC 校验的低字节(小端序)63(计算结果)
CRC 高字节1CRC 校验的高字节(小端序)AF(计算结果)
示例请求帧01 10 00 00 00 02 04 00 01 00 01 63 AF

从机响应帧结构

字段字节数含义示例(确认写入 2 个寄存器)
从机地址1自身地址(如 0x0101
功能码10x10(与请求一致)10
起始地址高字节1寄存器起始地址的高 8 位(大端序)00
起始地址低字节1寄存器起始地址的低 8 位(大端序)00
数量高字节1寄存器数量的高 8 位(大端序)00
数量低字节1寄存器数量的低 8 位(大端序)02
CRC 低字节1CRC 校验的低字节(小端序)41
CRC 高字节1CRC 校验的高字节(小端序)C8
示例响应帧01 10 00 00 00 02 41 C8

三、异常响应(通用规则)

当从机无法执行请求时,返回 异常帧

  • 功能码:原功能码 最高位设为 1(如 0x03 → 0x83)。
  • 异常码:1 字节,解释错误原因(如 0x02 表示 “非法数据地址”)。

示例:主站发 0x03 读不存在的寄存器,从机响应:
01 83 02 xx xx0x83 是异常功能码,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;
}

运行结果:

流程总结:

 

🦊🦊🦊. 

http://www.dtcms.com/a/284286.html

相关文章:

  • PyCharm2024安装包社区版和专业版
  • TESOLLO五指灵巧手遥操作解决方案
  • 使用 .NET Core 的原始 WebSocket
  • Spring整合MyBatis详解
  • 概率论与数理统计(四)
  • WCDB soci 查询语句
  • 缓存雪崩、缓存穿透,缓存击穿
  • 使用IntelliJ IDEA和Maven搭建SpringBoot集成Fastjson项目
  • 【git】使用教程
  • CommonJS和ES模块区别对比
  • API开发提速新方案:SmartBear API Hub与ReadyAPI虚拟化整合实践
  • ESP8266服务器建立TCP连接失败AT+CIPSTART=“TCP“,“192.168.124.1“,8080 ERROR CLOSED
  • JAVA后端开发——success(data) vs toAjax(rows): 何时用
  • 美拍sig逆向
  • 神经网络:模拟人脑的 AI 信息处理系统
  • 代码随想录打卡第十二天
  • Unity | AmplifyShaderEditor插件基础(第十集:噪声的种类+火焰制作-下)
  • 透过结构看时间——若思考清洗则表达有力
  • 开源Agent平台Dify源码剖析系列(六)核心模块core/agent之CotCompletionAgentRunner
  • Web开发 01
  • Vue.js 的 Composition API 深度解析:构建高效可维护的前端逻辑
  • 让大模型输出更加规范化——指定插件
  • LVS部署DR模式集群
  • @Linux搭建DNS-主从服务器
  • Spring原理揭秘--Spring的AOP
  • cuda编程笔记(8)--线程束warp
  • Cookie 与 Session概述
  • AI编程实战:如何让AI生成带参数和返回值的Python函数——以PDF文本提取为例
  • 【橘子分布式】gRPC(理论篇)
  • 要实现在调用  driver.get()  后立即阻止页面自动跳转到 Azure 登录页,可通过以下几种方法实现: