C + Linux Modbus 通讯攻略

分类: MODBUS

Linux Modbus C 语言开发实战指南

1. 环境准备 (Installation)

在开始写代码之前,我们需要在 Linux 系统中安装 libmodbus 开发库。

使用包管理器安装 (推荐)

对于大多数发行版 (Ubuntu/Debian/Raspbian),可以直接使用 apt

Bash

sudo apt-get update
sudo apt-get install libmodbus-dev

验证安装

安装完成后,可以使用 pkg-config 检查库是否被系统识别:

Bash

pkg-config --cflags --libs libmodbus

如果输出了类似 -I/usr/include/modbus -lmodbus 的内容,说明安装成功。


2. 核心概念速查 (Core Concepts)

在 Modbus 协议中,无论 RTU 还是 TCP,数据模型都是通用的。你需要了解以下四种数据对象:

对象类型 访问权限 说明 对应 PLC/设备概念
Coil (线圈) 读写 单个位 (0/1) 数字输出 (DO)
Discrete Input (离散输入) 只读 单个位 (0/1) 数字输入 (DI)
Holding Register (保持寄存器) 读写 16位字 (Word) 模拟量输出/参数设定
Input Register (输入寄存器) 只读 16位字 (Word) 模拟量输入 (AI)

3. 实战案例 A:Modbus RTU (串口通讯)

场景: 你的 Linux 设备(作为 Master/主机)通过 USB 转 RS485 串口连接到一个温湿度传感器(作为 Slave/从机)。

目标: 读取从机地址 1 的 0x00 号寄存器数据。

代码实现 (modbus_rtu_master.c)

C

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>

int main(void) {
    modbus_t *ctx;
    uint16_t tab_reg[32]; // 用于存储读取到的数据
    int rc;
    int i;

    // 1. 创建 Modbus RTU 上下文
    // 参数: 串口设备路径, 波特率, 校验位('N'=无, 'E'=偶, 'O'=奇), 数据位, 停止位
    ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);

    if (ctx == NULL) {
        fprintf(stderr, "Unable to create the libmodbus context\n");
        return -1;
    }

    // 2. 设置超时时间 (可选,但推荐)
    // 响应超时设置为 1 秒 0 微秒
    modbus_set_response_timeout(ctx, 1, 0);

    // 3. 设置从机 ID (Slave ID)
    modbus_set_slave(ctx, 1);

    // 4. 建立连接
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }

    // 5. 读取保持寄存器 (Function Code 03)
    // 参数: 上下文, 起始地址, 读取数量, 存储数组
    // 读取地址 0 开始的 5 个寄存器
    rc = modbus_read_registers(ctx, 0, 5, tab_reg);

    if (rc == -1) {
        fprintf(stderr, "Read failed: %s\n", modbus_strerror(errno));
    } else {
        // 打印读取到的数据
        for (i = 0; i < rc; i++) {
            printf("Register %d: %d (0x%X)\n", i, tab_reg[i], tab_reg[i]);
        }
    }

    // 6. 清理与释放
    modbus_close(ctx);
    modbus_free(ctx);

    return 0;
}

4. 实战案例 B:Modbus TCP (以太网通讯)

场景: 你的 Linux 程序作为 Client(客户端/主机),连接到一台 PLC(Server/服务器/从机)。

目标: 向 PLC 的地址 100 写入一个数值 1234。

代码实现 (modbus_tcp_client.c)

C

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <modbus.h>

int main(void) {
    modbus_t *ctx;
    int rc;

    // 1. 创建 Modbus TCP 上下文
    // 参数: 目标 IP 地址, 端口号 (Modbus TCP 默认 502)
    ctx = modbus_new_tcp("192.168.1.50", 502);

    if (ctx == NULL) {
        fprintf(stderr, "Unable to create the libmodbus context\n");
        return -1;
    }

    // 2. 建立连接
    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }

    // 3. 写单个寄存器 (Function Code 06)
    // 参数: 上下文, 寄存器地址, 写入的值
    int reg_addr = 100;
    int value_to_write = 1234;

    rc = modbus_write_register(ctx, reg_addr, value_to_write);

    if (rc == -1) {
        fprintf(stderr, "Write failed: %s\n", modbus_strerror(errno));
    } else {
        printf("Successfully wrote %d to register %d\n", value_to_write, reg_addr);
    }

    // 4. 清理与释放
    modbus_close(ctx);
    modbus_free(ctx);

    return 0;
}

5. 编译与运行 (Compilation)

这是新手最容易出错的地方。你需要告诉编译器头文件在哪里,以及链接哪个库。我们使用 pkg-config 来自动处理。

编译命令

编译 RTU 示例:

Bash

gcc modbus_rtu_master.c -o rtu_app $(pkg-config --cflags --libs libmodbus)

编译 TCP 示例:

Bash

gcc modbus_tcp_client.c -o tcp_app $(pkg-config --cflags --libs libmodbus)

运行

Bash

# 对于串口操作,通常需要 sudo 权限来访问 /dev/tty*
sudo ./rtu_app

# TCP 通常不需要 sudo,除非你绑定了小于 1024 的端口(作为 Server 时)
./tcp_app

6. 常用 API 文档 (Quick Reference)

为了方便查阅,以下是 libmodbus 中最常用的函数列表:

初始化与连接

  • modbus_new_rtu(device, baud, parity, data_bit, stop_bit): 创建 RTU 实例。
  • modbus_new_tcp(ip, port): 创建 TCP 实例。
  • modbus_set_slave(ctx, slave_id): 设置从站 ID (RTU 必做)。
  • modbus_connect(ctx): 建立连接。
  • modbus_close(ctx): 断开连接。
  • modbus_free(ctx): 释放内存。

读取数据 (Read)

  • modbus_read_bits(ctx, addr, nb, dest): 读取线圈 (Coils) -> FC 01
  • modbus_read_input_bits(ctx, addr, nb, dest): 读取离散输入 (Discrete Inputs) -> FC 02
  • modbus_read_registers(ctx, addr, nb, dest): 读取保持寄存器 (Holding Regs) -> FC 03
  • modbus_read_input_registers(ctx, addr, nb, dest): 读取输入寄存器 (Input Regs) -> FC 04

写入数据 (Write)

  • modbus_write_bit(ctx, addr, status): 写单个线圈 -> FC 05
  • modbus_write_register(ctx, addr, value): 写单个寄存器 -> FC 06
  • modbus_write_bits(ctx, addr, nb, src): 写多个线圈 -> FC 15
  • modbus_write_registers(ctx, addr, nb, src): 写多个寄存器 -> FC 16

调试与错误

  • modbus_set_debug(ctx, TRUE): 开启调试模式(非常有用!它会打印出所有发送和接收的十六进制原始字节)。
  • modbus_strerror(errno): 获取具体的错误文本信息。

7. 调试技巧与常见问题

提示: 90% 的 Modbus 问题都出现在物理连接或地址偏移上。

  1. 开启 Debug 模式:

在代码中 modbus_new_... 之后加入 modbus_set_debug(ctx, TRUE);。这会在终端打印出类似 [01][03][00][00]... 的报文,方便对照协议手册查错。

  1. 地址偏移 (Offset):

PLC 文档写地址是 40001,通常意味着 Modbus 功能码 03 (4xxxx),且地址是 1 还是 0?

  • libmodbus 使用 0-based 索引。
  • 如果文档说地址 40001,你在代码里通常填 0
  • 如果文档说地址 40002,代码里填 1
  1. 权限问题 (Permission Denied):

如果报错无法打开 /dev/ttyUSB0,可以将当前用户加入 dialout 组,或者直接使用 sudo 运行程序。

  1. 浮点数处理:

Modbus 寄存器是 16 位的。如果要读取 32 位浮点数(Float),通常需要读取 2个连续的寄存器,然后通过指针强转或位移操作合并数据。