在开始写代码之前,我们需要在 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 的内容,说明安装成功。
在 Modbus 协议中,无论 RTU 还是 TCP,数据模型都是通用的。你需要了解以下四种数据对象:
| 对象类型 | 访问权限 | 说明 | 对应 PLC/设备概念 |
|---|---|---|---|
| Coil (线圈) | 读写 | 单个位 (0/1) | 数字输出 (DO) |
| Discrete Input (离散输入) | 只读 | 单个位 (0/1) | 数字输入 (DI) |
| Holding Register (保持寄存器) | 读写 | 16位字 (Word) | 模拟量输出/参数设定 |
| Input Register (输入寄存器) | 只读 | 16位字 (Word) | 模拟量输入 (AI) |
场景: 你的 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;
}
场景: 你的 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;
}
这是新手最容易出错的地方。你需要告诉编译器头文件在哪里,以及链接哪个库。我们使用 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
为了方便查阅,以下是 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): 释放内存。modbus_read_bits(ctx, addr, nb, dest): 读取线圈 (Coils) -> FC 01modbus_read_input_bits(ctx, addr, nb, dest): 读取离散输入 (Discrete Inputs) -> FC 02modbus_read_registers(ctx, addr, nb, dest): 读取保持寄存器 (Holding Regs) -> FC 03modbus_read_input_registers(ctx, addr, nb, dest): 读取输入寄存器 (Input Regs) -> FC 04modbus_write_bit(ctx, addr, status): 写单个线圈 -> FC 05modbus_write_register(ctx, addr, value): 写单个寄存器 -> FC 06modbus_write_bits(ctx, addr, nb, src): 写多个线圈 -> FC 15modbus_write_registers(ctx, addr, nb, src): 写多个寄存器 -> FC 16modbus_set_debug(ctx, TRUE): 开启调试模式(非常有用!它会打印出所有发送和接收的十六进制原始字节)。modbus_strerror(errno): 获取具体的错误文本信息。提示: 90% 的 Modbus 问题都出现在物理连接或地址偏移上。
在代码中 modbus_new_... 之后加入 modbus_set_debug(ctx, TRUE);。这会在终端打印出类似 [01][03][00][00]... 的报文,方便对照协议手册查错。
PLC 文档写地址是 40001,通常意味着 Modbus 功能码 03 (4xxxx),且地址是 1 还是 0?
libmodbus 使用 0-based 索引。40001,你在代码里通常填 0。40002,代码里填 1。如果报错无法打开 /dev/ttyUSB0,可以将当前用户加入 dialout 组,或者直接使用 sudo 运行程序。
Modbus 寄存器是 16 位的。如果要读取 32 位浮点数(Float),通常需要读取 2个连续的寄存器,然后通过指针强转或位移操作合并数据。