modbus相关学习

在绝大多数工业自动化场景中,上位机 (Host Computer) 会作为 Modbus 主机 (Master)客户端 (Client) 来与 PLC (可编程逻辑控制器) 进行通信。

🏭 典型的通信架构

复制代码
+-------------------+       Modbus TCP/RTU       +----------------+
|                   |   <===================>    |                |
|   上位机 (PC)     |                            |   PLC (从机)   |
|   - SCADA系统     |                            |   - 控制设备   |
|   - HMI界面       |                            |   - 采集数据   |
|   - 监控软件      |                            |   - 执行命令   |
|   - 数据库        |                            |                |
|   - 用户应用程序  |                            |                |
|                   |                            |                |
+-------------------+                            +----------------+
      (Master/Client)                                  (Slave/Server)

📋 为什么上位机通常是 Master?

  1. 集中控制与监控 :上位机通常运行着 SCADA (监控与数据采集) 系统或 HMI (人机界面)。它的核心任务是收集 PLC的状态信息(如传感器读数、设备运行状态)并下发控制命令(如启动/停止电机、设置参数)。这种"主动查询"和"主动下发"的行为决定了它必须是 Master。
  2. 数据聚合 :一个上位机可能需要同时与多个PLC、仪表、传感器通信。由上位机统一发起请求,可以更好地协调和管理这些通信会话。
  3. 协议设计:Modbus 的主从模式本身就适用于这种"中心化"的控制系统。Master 负责轮询各个 Slave,确保数据的及时获取和指令的有效传达。

🧠 上位机上的典型应用

  • SCADA 软件 (如 WinCC, Wonderware, Ignition):用于工厂级的监控和数据历史记录。
  • HMI 软件 (如 Vijeo Designer, Pro-face):提供直观的操作界面供操作员使用。
  • 定制开发的应用程序 (用 C++, C#, Python, Java 等编写):实现特定的数据处理、分析或与企业级系统的集成。你写的 C++ Modbus 客户端就属于这一类。
  • 数据库:用于存储从 PLC 收集来的历史数据。

🔄 通信过程

  1. 上位机 (Master) 向特定地址的 PLC (Slave) 发送请求(例如,读取保持寄存器 40001-40010 的值)。
  2. PLC (Slave) 接收到请求后,根据请求内容(功能码)读取内部数据或执行相应动作。
  3. PLC (Slave) 将结果(或确认信息)发送回 上位机 (Master)
  4. 上位机 (Master) 接收并处理来自 PLC 的数据,更新 HMI 界面或存储到数据库。

总结: 在 Modbus 通信中,上位机(如 PC 上的软件)几乎总是扮演 Master/Client 的角色,而 PLC、仪表等现场设备则扮演 Slave/Server 的角色。 这是工业自动化领域最常见和最基础的通信模式。

好的,我来详细解释一下 Modbus 中的这些核心概念------数据区类型。这是理解 Modbus 协议的关键。

🔧 Modbus 数据模型

Modbus 协议定义了四种主要的 数据区,每种数据区代表不同类型的数据,具有不同的读写特性。你可以把它们想象成 PLC 内部的四个不同类型的"仓库"或"表格"。

数据区类型 (Type) 功能码 (Function Codes) 访问方式 数据单位 描述 典型应用
线圈 (Coils) 读: 0x01, 写: 0x05, 0x0F 读/写 1 位 (Bit) 可以被读取和写入的开关量,只有 ON (1) 或 OFF (0) 两种状态。 控制继电器、启动/停止电机、控制指示灯、读取数字输入开关状态。
离散输入 (Discrete Inputs) 读: 0x02 只读 1 位 (Bit) 只能被读取的开关量,通常对应外部不可改变的输入信号。 读取按钮状态、门开关、传感器干接点输出。
保持寄存器 (Holding Registers) 读: 0x03, 写: 0x06, 0x10 读/写 16 位 (Word) 可以被读取和写入的数值。一个寄存器可以存储一个 16 位的整数 (0 到 65535 或 -32768 到 32767)。 存储和修改设定值、PID 参数、计数器值、温度目标值、读取或设置模拟量输出。
输入寄存器 (Input Registers) 读: 0x04 只读 16 位 (Word) 只能被读取的数值,通常对应外部模拟量输入或传感器的测量值。 读取模拟量传感器数据(如温度、压力、流量、液位)。

🧩 详细讲解

1. 线圈 (Coils)
  • 本质 :一个可以被控制的开关。
  • 数据 :只有两种状态 ------ 1 (ON/TRUE)0 (OFF/FALSE)
  • 地址范围 :通常表示为 0000109999 (注意,这是传统表示法,实际地址从 0 开始)。
  • 作用
    • 写 (Output):上位机可以向 PLC 的某个线圈地址发送指令,将其置为 ON 或 OFF,从而控制外部设备(如打开/关闭阀门、启动/停止马达)。
    • 读 (Input):上位机可以读取 PLC 内部某个线圈的状态,了解之前发出的指令是否被执行,或者查看 PLC 内部逻辑运算的结果。
  • C++ 代码示例关联readCoils()writeSingleRegister() (虽然名字叫寄存器,但功能码 0x05 用于写单个线圈)。
2. 离散输入 (Discrete Inputs)
  • 本质 :一个只能被读取的开关。
  • 数据 :只有两种状态 ------ 1 (ON/TRUE)0 (OFF/FALSE)
  • 地址范围 :通常表示为 1000119999
  • 作用
    • 只读 (Input):用于读取外部连接到 PLC 的输入设备的状态。这些设备的状态不由 PLC 控制,而是由外部物理条件决定(例如,按下按钮、门打开)。
  • C++ 代码示例关联readDiscreteInputs() (虽然上面的代码没有直接体现,但 modbus_read_input_bits 对应此功能)。
3. 保持寄存器 (Holding Registers)
  • 本质 :一个可以被读取和修改的变量。
  • 数据:一个 16 位的数值。可以是无符号整数 (0 到 65535) 或有符号整数 (-32768 到 32767),也可以用来存储两个 ASCII 字符。
  • 地址范围 :通常表示为 4000149999
  • 作用
    • 读/写 (Input/Output):这是最灵活的数据区。上位机可以读取其中的数值(如当前温度、设定的压力值),也可以写入新的数值(如设定新的目标温度、修改 PID 参数)。
  • C++ 代码示例关联readHoldingRegisters()writeSingleRegister() / writeMultipleRegisters()
4. 输入寄存器 (Input Registers)
  • 本质 :一个只能被读取的变量。
  • 数据:一个 16 位的数值。
  • 地址范围 :通常表示为 3000139999
  • 作用
    • 只读 (Input):主要用于读取连接到 PLC 的模拟量输入模块的数据。例如,一个温度传感器连接到 PLC 的 AI (Analog Input) 模块,其测量到的温度值会被转换成一个数字量,存储在某个输入寄存器中,供上位机读取。
  • C++ 代码示例关联readInputRegisters() (虽然上面的代码没有直接体现,但 modbus_read_input_registers 对应此功能)。

📌 重要提示

  • 地址编号 :虽然协议规范中提到 0xxxx, 1xxxx, 3xxxx, 4xxxx 的编号方式,但在实际编程时,例如使用 libmodbus,传递给函数的地址通常是从 0 开始 的偏移量。例如,如果你想读取 40001 号保持寄存器,你传入的地址参数是 0;读取 40002 传入 1,以此类推。请务必查阅你所使用的库的文档。
  • 数据长度 :线圈和离散输入是 位 (Bit) ,而寄存器是 字 (Word, 16位) 。如果需要传输更大的数据(如 32 位浮点数),通常需要占用两个连续的保持寄存器。
    这是个非常好的观察!你的疑惑在于协议规定的数据单位(bit vs word)与代码中使用的数据类型(uint8_t vs uint16_t)之间的关系。让我来解释一下:

🧩 实际编程中的数据打包方式

虽然 Modbus 协议将线圈 (Coils) 和离散输入 (Discrete Inputs) 定义为 位 (Bit) ,但在实际的网络传输和 API 接口设计 中,为了效率和便利性,它们通常被打包成 字节 (Byte) 进行处理。

1. 读操作 (readCoils, readDiscreteInputs)
  • 协议层面 :你想读取 10 个线圈的状态,例如 [1, 0, 1, 1, 0, 0, 0, 1, 1, 0]

  • 传输层面 :Modbus 协议会将这 10 个位打包。前 8 个位 10110001 会被打包成一个字节 0xB1,剩下的 2 个位 10 会被打包成另一个字节的最低两位,例如 0x02

  • API 接口libmodbusmodbus_read_bits 函数就是这样设计的。它接收一个 uint8_t* 数组作为输出缓冲区。

  • 为什么用 uint8_t

    • 效率:CPU 通常按字节处理数据,而不是按位。将 8 个位打包成一个字节可以提高传输和处理效率。
    • 标准化:这是一种行业标准做法。即使原始数据是位,API 也倾向于返回字节数组。
    • 数组大小 :如果你要读取 N 个位,实际需要的字节数是 (N + 7) / 8(向上取整)。std::vector<uint8_t> 很好地适应了这种动态大小的需求。
    cpp 复制代码
    // 示例:读取 10 个线圈
    std::vector<uint8_t> result; // 假设调用 readCoils(0, 10, result)
    // result.size() 将是 ceil(10/8) = 2
    // result[0] 可能是 0xB1 (对应前8个位)
    // result[1] 可能是 0x02 (对应后2个位,其余位为0)
2. 读寄存器操作 (readHoldingRegisters, readInputRegisters)
  • 协议层面:每个寄存器是 16 位 (2 字节)。

  • 传输层面:数据按 16 位为单位打包传输。

  • API 接口libmodbusmodbus_read_registers 函数直接接收一个 uint16_t* 数组作为输出缓冲区。

  • 为什么用 uint16_t

    • 匹配:数据单位完全匹配。每个元素正好代表一个 16 位的寄存器值。
    • 方便 :可以直接使用这些 uint16_t 值进行计算、比较等操作,无需额外的解包步骤。
    cpp 复制代码
    // 示例:读取 3 个保持寄存器
    std::vector<uint16_t> result; // 假设调用 readHoldingRegisters(0, 3, result)
    // result.size() 将是 3
    // result[0], result[1], result[2] 分别对应三个寄存器的值
3. 写操作 (writeSingleCoil, writeMultipleCoils, writeSingleRegister, writeMultipleRegisters)
  • 写单个线圈 (writeSingleCoil)

    • 由于只写一个位,libmodbusmodbus_write_bit 函数期望一个 int 类型的值,0 表示 OFF,非 0 表示 ON。在你的封装中,value ? 1 : 0 就是将 bool 转换为 int
  • 写多个线圈 (writeMultipleCoils)

    • 你将 std::vector<bool> 转换为 std::vector<uint8_t>,这与读操作的打包逻辑类似。libmodbusmodbus_write_bits 期望一个 const uint8_t* 作为输入源。虽然 std::vector<bool> 在 C++ 中是特化的,内部可能也是按位存储,但转换为 uint8_t 保证了数据的正确传递给底层库。
  • 写寄存器 (writeSingleRegister, writeMultipleRegisters)

    • 这些操作非常直观,因为 uint16_t 完全符合寄存器的 16 位宽度要求。

📌 总结

  • 协议定义 :Modbus 规定了 线圈/离散输入寄存器16 位
  • API 实现 :为了高效处理,libmodbus 等库将位打包成 字节 (uint8_t) 进行传输和接收,将寄存器作为 16 位整数 (uint16_t) 进行传输和接收。
  • 你的代码 :你选择 uint8_t 作为线圈和离散输入的容器类型,uint16_t 作为寄存器的容器类型,这是完全正确且符合标准的做法,它准确反映了底层 API 的要求和数据的实际组织形式。

简而言之,协议是抽象的逻辑模型,而代码实现要考虑具体的传输效率和编程接口的设计。

相关推荐
2603_954708313 小时前
如何确保微电网标准化架构设计流程的完整性?
网络·人工智能·物联网·架构·系统架构
STC_USB_CAN_80513 小时前
菜单学习,科学计算器使用【TFT240*320彩屏+实际键盘】@Ai8051U,ST7789
单片机·学习·51单片机
handler013 小时前
拒绝权限报错!三分钟掌握 Linux 权限管理
linux·c语言·c++·笔记·学习
xiaotao1313 小时前
02-机器学习基础: 无监督学习——scikit-learn实战与模型管理
学习·机器学习·scikit-learn
hipolymers6 小时前
C语言怎么样?难学吗?
c语言·数据结构·学习·算法·编程
richxu202510017 小时前
嵌入式学习之路->stm32篇->(11)SPI通信(下)
stm32·嵌入式硬件·学习
xuhaoyu_cpp_java7 小时前
连接池学习
数据库·经验分享·笔记·学习
花无缺就是我8 小时前
2026年最新内网穿透有哪些方案,详细列举
网络·电信专线