libmodbus 使用示例

介绍

​​libmodbus​​ 是一个免费、开源的软件库,用于使用 ​​Modbus​​ 协议进行通信。它用 C 语言编写,旨在提供一个简单、稳定且跨平台的解决方案,让应用程序能够轻松地作为 Modbus 主站(客户端)或从站(服务器)运行。

源码链接

Modbus使用示例 (ModbusMaster)

c++ 复制代码
#pragma once

#include <modbus/modbus.h>
#include <unistd.h>

#include <cstring>
#include <memory>
#include <string>

#define LOG_ERROR(fmt, ...)                              \
  do {                                                   \
    fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__); \
  } while (0)

#define LOG_INFO(fmt, ...)                              \
  do {                                                  \
    fprintf(stdout, "[INFO] " fmt "\n", ##__VA_ARGS__); \
  } while (0)

class ModbusMaster {
 public:
  struct Option {
    std::string path = "/dev/ttyUSB0";
    int baud = 9600;
    char parity = 'N';
    int databit = 8;
    int stopbit = 1;
    int debug = 1;
    int modbus_cmd_delay_ms = 5;
  };

 public:
  ModbusMaster(const Option& option) : option_(option) {}
  ~ModbusMaster();

  bool Open() {
    if (ctx == nullptr) {
      return Connect();
    }
    LOG_INFO("modbus already opened");
    return true;
  }

  bool Close() {
    if (ctx != nullptr) {
      modbus_close(ctx);
      modbus_free(ctx);
      ctx = nullptr;
      LOG_INFO("modbus closed");
    }
    return true;
  }

  virtual void RunOnce() {
    if (this->ctx == nullptr) {
      LOG_ERROR("modbus ctx is null, please check connect.");
      return;
    }

    uint8_t data[UINT8_MAX];
    memset(data, 0, sizeof(data));
    int ret = 0;

    // Example: Read/Write Slave 1
    modbus_set_slave(ctx, 0);

    // read
    ret = modbus_read_input_bits(ctx, 0, 4, data);
    if (ret < 0) {
      LOG_ERROR("modbus_read_input_bits failed : %s", modbus_strerror(errno));
    } else {
      LOG_INFO("modbus_read_input_bits success:");
      for (int i = 0; i < ret; i++) {
        LOG_INFO("  bit[%d] = %d", i, data[i]);
      }
    }

    // Add delay between commands
    usleep(option_.modbus_cmd_delay_ms * 1000);

    // write
    uint8_t buff[4] = {0, 1, 0, 1};
    ret = modbus_write_bits(ctx, 0, 4, buff);
    if (ret < 0) {
      LOG_ERROR("modbus_write_bits failed : %s", modbus_strerror(errno));
    }
  }

  //
 private:
  bool Connect() {
    this->ctx =
        modbus_new_rtu(option_.path.c_str(), option_.baud, option_.parity,
                       option_.databit, option_.stopbit);

    if (this->ctx == nullptr) {
      LOG_ERROR("unable creat modbus : %s", option_.path.c_str());
      return false;
    }

    {
      int a = modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS232);
      int c = modbus_rtu_get_serial_mode(ctx);
      if (c == -1) {
        LOG_ERROR("set rs485 failed : %s ", modbus_strerror(errno));
        return false;
      }

      int b = modbus_rtu_set_rts(ctx, MODBUS_RTU_RTS_UP);
      if (b == -1) {
        LOG_ERROR("set rts failed : %s ", modbus_strerror(errno));
        return false;
      }
    }

    {
      int a = modbus_set_slave(ctx, 1);
      if (a == -1) {
        LOG_ERROR("set slave failed : %s ", modbus_strerror(errno));
        return false;
      }

      modbus_set_debug(ctx, option_.debug);

      modbus_set_response_timeout(ctx, 0, 100 * 1000);

      a = modbus_connect(ctx);
      if (a == -1) {
        LOG_ERROR("modbus_connect failed : %s ", modbus_strerror(errno));
        return false;
      }
    }

    LOG_INFO("modbus start");
    return true;
  }

 private:
  Option option_;
  modbus_t* ctx = nullptr;
};

注意事项:

  • 在 Connect() 过程中:

    • mode 需要设置成 MODBUS_RTU_RS232 (实际接口为232或485),设置成 RS485 会报错无法通信;
      • 参考以下源码实现 可以看到设置成 RS232 或 RS485 的区别;
      • 推测 工控机厂家 把 485,232 的驱动都配置成 通用的 232 串口设备;
      • 这一块需要参考 工控机厂家 提供的 资料进行判断;
    c++ 复制代码
    /// src/modbus-rtu.c : modbus_rtu_set_serial_mode
            if (mode == MODBUS_RTU_RS485) {
              // Get
              if (ioctl(ctx->s, TIOCGRS485, &rs485conf) < 0) {
                  return -1;
              }
              // Set
              rs485conf.flags |= SER_RS485_ENABLED;
              if (ioctl(ctx->s, TIOCSRS485, &rs485conf) < 0) {
                  return -1;
              }
    
              ctx_rtu->serial_mode = MODBUS_RTU_RS485;
              return 0;
          } else if (mode == MODBUS_RTU_RS232) {
              /* Turn off RS485 mode only if required */
              if (ctx_rtu->serial_mode == MODBUS_RTU_RS485) {
                  /* The ioctl call is avoided because it can fail on some RS232 ports */
                  if (ioctl(ctx->s, TIOCGRS485, &rs485conf) < 0) {
                      return -1;
                  }
                  rs485conf.flags &= ~SER_RS485_ENABLED;
                  if (ioctl(ctx->s, TIOCSRS485, &rs485conf) < 0) {
                      return -1;
                  }
              }
              ctx_rtu->serial_mode = MODBUS_RTU_RS232;
              return 0;
          }
    • MODBUS_RTU_RTS_UP 一般需要设置,根据 工控机厂家 提供的资料示例来配;
    • debug 测试的时候打开,可以看到详细的日志和数据;
    • timeout 根据实际情况设置,这里默认设置成 100ms;
  • 在运行过程中 RunOnce :

    • 指令发送间需要有间隔时间,不然有的从站会无响应,数值大小根据从站厂家提供 或 自己测试;
    • 运行函数要与实际对象字典命令字对应,可以看 debug 输出的数据看是否正确;
    • 根据实际情况配置要读写的字典对象,注意一点波特率低的时候读写是比较慢的,不要高频进行操作;

Modbus 库连接

  • libmodbus 是通过 pkgconfig 来查找连接的,使用 -lmodbus 进行连接,位置在 ./lib/x86_64-linux-gnu/pkgconfig/libmodbus.pc
  • 使用 cmake 的话一般 给modbus库配置 modbusConfig.cmake , modbusConfigVersion.cmake 方便查找;

Modbus 基本数据区介绍

Modbus 设备中定义的四种基本数据区,这是功能码存在意义的基础:

数据区名称 对象类型 访问权限 含义 举例
线圈 位(1 bit) 读/写 可开关的二进制状态 继电器的开(1)/关(0),灯的亮/灭
离散输入 位(1 bit) 只读 只读的二进制状态 开关状态、故障报警信号
保持寄存器 字(16 bit) 读/写 可读写的数值数据 温度设定值、速度设定值、配置参数
输入寄存器 字(16 bit) 只读 只读的数值数据 温度测量值、压力传感器读数、流量值

功能码就是用来操作这些数据区的指令。


功能码与 libmodbus 函数对照表

下表清晰地展示了标准功能码、其含义以及在 libmodbus 中对应的主站(客户端)常用函数。

功能码(十进制) 功能码(十六进制) 功能名称 操作类型 操作数据区 libmodbus 主站函数
1 0x01 读线圈 读 线圈 modbus_read_bits
2 0x02 读离散输入 读 离散输入 modbus_read_input_bits
3 0x03 读保持寄存器 读 保持寄存器 modbus_read_registers
4 0x04 读输入寄存器 读 输入寄存器 modbus_read_input_registers
5 0x05 写单个线圈 写 线圈 modbus_write_bit
6 0x06 写单个寄存器 写 保持寄存器 modbus_write_register
15 0x0F 写多个线圈 写 线圈 modbus_write_bits
16 0x10 写多个寄存器 写 保持寄存器 modbus_write_registers
相关推荐
瑞克-桑切斯38 分钟前
13、DNS、DHCP 与 FTP 协议详解
linux·运维·云计算
程序员-Benothing41 分钟前
Linux性能分析方法论:USE、RED、火焰图与perf实战
linux·运维·服务器
云中双月1 小时前
瑞芯微rv1106进入Loader模式转储emmc数据
linux·嵌入式·uboot·u-boot·emmc·rockchip·烧录·rv1106·瑞芯微
yunwei371 小时前
eBPF 入门实践教程:BPF Token,安全的委托式权限与程序加载
linux·后端·性能优化
不知语冰1 小时前
达妙 Orin NX V2:JetPack 安装中文拼音输入法
linux
水饺编程1 小时前
第2章:Linux 基础,echo 命令
linux·c语言·汇编
Lsetea1 小时前
curl设置SSL_CERT_FILE仍报60:CURL_CA_BUNDLE与CA路径优先级排查
linux·https·ssl证书·openssl·curl
垆边人似月.1 小时前
找出最小的 K 个数
数据结构·c++·算法
隐擎fox2 小时前
Linux eBPF 与 XDP 高性能网络出口架构:动态连接追踪、丢包诊断与住宅网关 QoS 调优实战
linux·网络·网络协议·tcp/ip·架构
C语言小火车2 小时前
C++ 为什么越来越依赖标准库?
java·开发语言·c++