C语言Modbus程序如何实现通信?

99ANYc3cd6
预计阅读时长 29 分钟
位置: 首页 C语言 正文

Modbus协议基础

在写代码之前,必须理解Modbus的核心概念。

c语言modbus程序
(图片来源网络,侵删)

1 通信模型

Modbus通常有两种通信模型:

  • 主从模式:这是最常见的方式,一个“主站”设备可以与一个或多个“从站”设备通信,主站发起请求,从站响应请求,从站之间不能互相通信。
  • 客户端/服务器模式:基于TCP/IP的通信,功能与主从模式类似,但使用的是TCP套接字。

2 寄存器类型

Modbus主要操作以下四种数据对象:

  • Coils (线圈):单个 bit,可读写,继电器的开/关状态。
  • Discrete Inputs (离散输入):单个 bit,只读,传感器的状态。
  • Holding Registers (保持寄存器):16位字,可读写,设置设备的运行速度。
  • Input Registers (输入寄存器):16位字,只读,读取温度传感器的值。

3 功能码

功能码是Modbus消息的一部分,它告诉从站要执行什么操作。

  • 0x01: 读取线圈状态
  • 0x02: 读取离散输入
  • 0x03: 读取保持寄存器
  • 0x04: 读取输入寄存器
  • 0x05: 写单个线圈
  • 0x06: 写单个保持寄存器
  • 0x0F: 写多个线圈
  • 0x10: 写多个保持寄存器

4 消息结构

一个标准的Modbus RTU (串口)消息帧包含:

c语言modbus程序
(图片来源网络,侵删)
  1. 设备地址:1字节,指定要通信的从站地址。
  2. 功能码:1字节,指定要执行的操作。
  3. 数据:n字节,包含请求的具体信息(如寄存器地址、数量)或响应的数据。
  4. CRC校验:2字节,用于检测数据在传输过程中是否出错。

使用Modbus库

自己从头实现Modbus协议的CRC校验、消息解析和封装非常繁琐且容易出错,在工业项目中,通常会使用成熟的第三方库,最著名、最常用的是 libmodbus

1 安装libmodbus

  • 在Linux上

    # 对于基于Debian的系统 (如Ubuntu)
    sudo apt-get update
    sudo apt-get install libmodbus-dev
    # 对于基于RHEL的系统 (如CentOS)
    sudo yum install libmodbus-devel
  • 在Windows上: 你可以从libmodbus的官方网站下载源代码,然后使用CMake进行编译,或者,许多集成开发环境如Visual Studio也提供了预编译的库。


编程示例:主站 和从站

我们将创建两个简单的C程序:一个作为Modbus主站,一个作为Modbus从站,它们可以在同一台机器上通过串口(如 /dev/ttyS0COM3)或TCP/IP(本地回环 0.0.1)进行通信,这里我们使用更通用的TCP/IP方式。

1 从站 程序

从站程序模拟了一个具有4个保持寄存器(地址0-3)的设备,主站可以读取或写入这些寄存器的值。

modbus_slave.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <modbus.h>
#define SERVER_IP   "127.0.0.1" // 本地回环地址
#define SERVER_PORT 502         // Modbus TCP默认端口
#define SLAVE_ID    1           // 从站ID
// 模拟的保持寄存器数据
uint16_t holding_registers[4] = { 0, 0, 0, 0 };
int main() {
    modbus_t *ctx;
    int rc;
    int socket_fd;
    // 1. 创建Modbus TCP上下文
    ctx = modbus_new_tcp(SERVER_IP, SERVER_PORT);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to create TCP context: %s\n", modbus_strerror(errno));
        return -1;
    }
    // 2. 设置从站ID (对于TCP,这主要用于串行网关,但设置它是好习惯)
    modbus_set_slave(ctx, SLAVE_ID);
    // 3. 绑定和监听
    rc = modbus_tcp_listen(ctx, 1);
    if (rc == -1) {
        fprintf(stderr, "Failed to listen: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    printf("Modbus slave listening on %s:%d...\n", SERVER_IP, SERVER_PORT);
    // 4. 等待主站连接
    socket_fd = modbus_tcp_accept(ctx, &socket_fd);
    if (socket_fd == -1) {
        fprintf(stderr, "Failed to accept connection: %s\n", modbus_strerror(errno));
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    printf("Client connected!\n");
    // 5. 循环处理主站请求
    while (1) {
        uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
        int query_length;
        // 接收主站发来的请求
        query_length = modbus_receive(ctx, query);
        if (query_length > 0) {
            // 处理请求并生成响应
            // modbus_reply会自动处理CRC和消息格式
            rc = modbus_reply(ctx, query, query_length, holding_registers, sizeof(holding_registers) / sizeof(holding_registers[0]));
            if (rc == -1) {
                fprintf(stderr, "Failed to reply: %s\n", modbus_strerror(errno));
                break;
            }
        } else {
            // 连接可能已断开
            if (errno == ECONNRESET) {
                printf("Client disconnected.\n");
                break;
            } else {
                perror("modbus_receive");
            }
        }
    }
    // 6. 清理资源
    close(socket_fd);
    modbus_close(ctx);
    modbus_free(ctx);
    printf("Modbus slave shutdown.\n");
    return 0;
}

2 主站 程序

主站程序将连接到从站,执行以下操作:

  1. 读取从站的保持寄存器(地址0-3)。
  2. 向从站的保持寄存器(地址0)写入一个新值(123)。
  3. 再次读取寄存器,验证写入是否成功。

modbus_master.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <modbus.h>
#define SERVER_IP   "127.0.0.1"
#define SERVER_PORT 502
#define SLAVE_ID    1
int main() {
    modbus_t *ctx;
    int rc;
    uint16_t *reg_buffer;
    int num_registers = 4; // 我们要读取/写的寄存器数量
    // 1. 创建Modbus TCP上下文
    ctx = modbus_new_tcp(SERVER_IP, SERVER_PORT);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to create TCP context: %s\n", modbus_strerror(errno));
        return -1;
    }
    // 2. 设置从站ID
    modbus_set_slave(ctx, SLAVE_ID);
    // 3. 连接到从站
    rc = modbus_connect(ctx);
    if (rc == -1) {
        fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }
    printf("Connected to Modbus slave at %s:%d\n", SERVER_IP, SERVER_PORT);
    // 4. 分配一个缓冲区来存储读取的寄存器值
    reg_buffer = malloc(num_registers * sizeof(uint16_t));
    if (reg_buffer == NULL) {
        fprintf(stderr, "Failed to allocate memory for register buffer.\n");
        modbus_close(ctx);
        modbus_free(ctx);
        return -1;
    }
    // --- 操作1: 读取保持寄存器 (功能码 0x03) ---
    // 读取从站地址 0 开始的 4 个保持寄存器
    rc = modbus_read_registers(ctx, 0, num_registers, reg_buffer);
    if (rc == -1) {
        fprintf(stderr, "Failed to read registers: %s\n", modbus_strerror(errno));
    } else {
        printf("Read %d registers from slave %d:\n", rc, SLAVE_ID);
        for (int i = 0; i < rc; i++) {
            printf("  Register %d: %d\n", i, reg_buffer[i]);
        }
    }
    printf("\n");
    // --- 操作2: 写入单个保持寄存器 (功能码 0x06) ---
    // 向从站地址 0 写入值 123
    uint16_t write_value = 123;
    rc = modbus_write_register(ctx, 0, write_value);
    if (rc == -1) {
        fprintf(stderr, "Failed to write register: %s\n", modbus_strerror(errno));
    } else {
        printf("Successfully wrote value %d to register 0 on slave %d.\n", write_value, SLAVE_ID);
    }
    printf("\n");
    // --- 操作3: 再次读取寄存器以验证写入 ---
    rc = modbus_read_registers(ctx, 0, num_registers, reg_buffer);
    if (rc == -1) {
        fprintf(stderr, "Failed to read registers (verification): %s\n", modbus_strerror(errno));
    } else {
        printf("Read %d registers again (verification):\n", rc);
        for (int i = 0; i < rc; i++) {
            printf("  Register %d: %d\n", i, reg_buffer[i]);
        }
    }
    // 5. 清理资源
    free(reg_buffer);
    modbus_close(ctx);
    modbus_free(ctx);
    printf("\nModbus master shutdown.\n");
    return 0;
}

如何编译和运行

  1. 保存文件:将上面的两个代码分别保存为 modbus_slave.cmodbus_master.c

  2. 编译:打开终端,使用 gccpkg-config 来编译链接libmodbus库。

    # 编译从站
    gcc modbus_slave.c -o modbus_slave $(pkg-config --cflags --libs libmodbus)
    # 编译主站
    gcc modbus_master.c -o modbus_master $(pkg-config --cflags --libs libmodbus)

    注意:如果pkg-config找不到,你可能需要手动指定编译和链接参数,

    • gcc modbus_slave.c -o modbus_slave -lmodbus
  3. 运行

    • 第一步:先运行从站程序,它会启动并等待主站的连接。

      ./modbus_slave

      你会看到输出:

      Modbus slave listening on 127.0.0.1:502...
    • 第二步:在另一个新的终端窗口中,运行主站程序,它会连接到从站并执行读写操作。

      ./modbus_master

      你会看到类似下面的输出:

      Connected to Modbus slave at 127.0.0.1:502
      Read 4 registers from slave 1:
        Register 0: 0
        Register 1: 0
        Register 2: 0
        Register 3: 0
      Successfully wrote value 123 to register 0 on slave 1.
      Read 4 registers again (verification):
        Register 0: 123
        Register 1: 0
        Register 2: 0
        Register 3: 0
      Modbus master shutdown.
    • 第三步:回到运行从站的终端,你会看到它检测到客户端断开连接:

      Client disconnected.
      Modbus slave shutdown.

总结与进阶

  • 核心思想:Modbus通信是请求/响应模式,主站发起请求,从站解析请求、执行操作、返回响应。
  • 使用库libmodbus 等库极大地简化了开发,让你专注于业务逻辑,而不是底层的协议细节。
  • 调试:在实际硬件环境中,串口调试助手(如 minicom, cutecom)和Modbus调试工具(如 ModbusPoll / ModbusSlave)非常有用。
  • 进阶
    • 多线程:如果主站需要同时与多个从站通信,可以使用多线程,每个连接一个线程。
    • RTU over TCP:许多设备支持“串行隧道”模式,即在TCP报文中封装RTU帧。libmodbus也支持这种模式。
    • 错误处理:在实际应用中,需要更健壮的错误处理和日志记录机制。
    • 实时性:对于要求高实时性的应用,可能需要考虑Modbus Plus或Profinet等其他协议。

这个例子为你提供了一个完整的C语言Modbus程序框架,你可以基于此进行修改和扩展,以满足你的具体项目需求。

-- 展开阅读全文 --
头像
Monkey织梦教程,新手如何快速上手?
« 上一篇 04-05
dede统计注册数量
下一篇 » 04-05

相关文章

取消
微信二维码
支付宝二维码

目录[+]