》》欢迎关注 嵌入式软件客栈 公众号,获取更多实战技巧《《
Modbus是一种广泛应用于工业自动化领域的通信协议,最初由Modicon(现在的施耐德电气)在1979年开发。作为一种开放、简单且健壮的协议,Modbus已成为工业电子设备之间通信的实际标准。
libmodbus是一个免费的、开源的、功能丰富的Modbus协议库,它提供了对各种Modbus变体(如RTU、ASCII和TCP)的支持,可以用于开发主站(客户端)和从站(服务器)应用程序。libmodbus以其高度可移植性、良好的文档和稳定的API而闻名,是开发Modbus通信应用的首选库之一。
Modbus协议基础
在深入了解libmodbus库之前,我们先简要了解Modbus协议的基本概念。
Modbus通信模型
Modbus采用主从(Master-Slave)架构,其中:
- 主站(Master):也称为客户端,发起请求并等待从站响应
- 从站(Slave):也称为服务器,接收并处理主站的请求,然后发送响应
通信过程始终由主站发起,从站无法主动发送数据。一个主站可以与多个从站通信,每个从站需要有唯一的地址(1-247)。
功能码简介
Modbus协议使用功能码来指示从站执行特定操作。以下是一些最常用的功能码:
| 功能码 | 功能名称 | 操作内容 |
|---|---|---|
| 0x01 | Read Coils | 读取线圈状态(读位) |
| 0x02 | Read Discrete Inputs | 读取离散输入(读位) |
| 0x03 | Read Holding Registers | 读取保持寄存器(读字) |
| 0x04 | Read Input Registers | 读取输入寄存器(读字) |
| 0x05 | Write Single Coil | 写单个线圈(写位) |
| 0x06 | Write Single Register | 写单个寄存器(写字) |
| 0x0F | Write Multiple Coils | 写多个线圈(写位) |
| 0x10 | Write Multiple Registers | 写多个寄存器(写字) |
数据模型
Modbus定义了四种数据类型:
- 线圈(Coils):单个二进制位,可读可写,地址范围:00001-09999
- 离散输入(Discrete Inputs):单个二进制位,只读,地址范围:10001-19999
- 输入寄存器(Input Registers):16位字,只读,地址范围:30001-39999
- 保持寄存器(Holding Registers):16位字,可读可写,地址范围:40001-49999
libmodbus库概述
libmodbus是一个功能全面的Modbus协议库,提供了发送/接收Modbus消息的API,支持RTU、ASCII和TCP等不同通信模式。
主要特性
- 开源且免费:基于LGPL v2.1+许可证
- 跨平台:支持Linux、Windows、macOS等多种操作系统
- 多种通信模式:支持RTU、TCP和TCP PI (支持IPv6)
- 完整功能:实现了所有标准Modbus功能码
- 易于使用:提供简单直观的API
- 良好文档:详细的API文档和示例代码
- 活跃社区:持续维护和更新
架构设计
libmodbus采用上下文(context)的概念来处理不同的连接和配置。库的主要组件包括:
- 上下文管理:创建、配置和释放连接上下文
- 连接函数:建立和关闭连接
- 数据交换函数:读取和写入各种类型的数据
- 服务器函数:用于实现Modbus从站
- 辅助函数:错误处理、调试和数据转换
环境搭建
Linux系统安装
在大多数Linux发行版中,可以直接通过包管理器安装libmodbus:
Debian/Ubuntu:
sudo apt-get install libmodbus-dev
Fedora/RHEL:
sudo dnf install libmodbus-devel
Arch Linux:
sudo pacman -S libmodbus
Windows系统安装
在Windows上,您可以通过以下方法安装libmodbus:
- 使用MSYS2/MinGW:
pacman -S mingw-w64-x86_64-libmodbus
- 从源码编译:
- 下载源码:https://github.com/stephane/libmodbus/releases
- 使用Visual Studio打开项目并编译
- 配置项目属性:
- 将Configuration Type设置为Dynamic Library (.dll)
- 在Additional Dependencies中添加ws2_32.lib
跨平台编译
从源码编译libmodbus:
# 下载源码
git clone https://github.com/stephane/libmodbus.git
cd libmodbus
# 配置和编译
./autogen.sh
./configure
make
sudo make install
验证安装:
pkg-config --libs --cflags libmodbus
如果安装成功,上述命令将输出编译参数,例如:
-I/usr/local/include -L/usr/local/lib -lmodbus
RTU模式通信
RTU(Remote Terminal Unit)模式是Modbus协议最初的实现方式,通过串行线(如RS-232、RS-485)进行通信。
RTU主站实现
以下是一个简单的RTU主站示例,用于读取从站的保持寄存器:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
int main() {
modbus_t *ctx;
uint16_t reg_data[10];
int rc;
// 创建新的RTU上下文
ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 设置从站地址
modbus_set_slave(ctx, 1);
// 设置调试模式(可选)
modbus_set_debug(ctx, TRUE);
// 连接设备
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 读取10个保持寄存器,起始地址为0
rc = modbus_read_registers(ctx, 0, 10, reg_data);
if (rc == -1) {
fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
// 打印读取到的数据
printf("读取到%d个寄存器\n", rc);
for (int i = 0; i < rc; i++) {
printf("reg[%d]=%d (0x%X)\n", i, reg_data[i], reg_data[i]);
}
// 清理资源
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
编译命令:
gcc -o rtu_master rtu_master.c -lmodbus
RTU从站实现
下面是一个简单的RTU从站示例,模拟一个具有线圈和寄存器的从站设备:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>
static int run = 1;
void signal_handler(int sig) {
run = 0;
}
int main() {
modbus_t *ctx;
modbus_mapping_t *mb_mapping;
uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH];
int rc;
// 捕获Ctrl+C信号
signal(SIGINT, signal_handler);
// 创建新的RTU上下文
ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 设置从站地址
modbus_set_slave(ctx, 1);
// 设置调试模式(可选)
modbus_set_debug(ctx, TRUE);
// 连接设备
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 分配并初始化Modbus映射
mb_mapping = modbus_mapping_new(100, 100, 100, 100);
if (mb_mapping == NULL) {
fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
// 设置一些初始值
mb_mapping->tab_registers[0] = 1234;
mb_mapping->tab_registers[1] = 5678;
mb_mapping->tab_bits[0] = 1;
mb_mapping->tab_bits[1] = 0;
printf("从站已启动,按Ctrl+C退出...\n");
while (run) {
rc = modbus_receive(ctx, query);
if (rc > 0) {
// 处理收到的请求
modbus_reply(ctx, query, rc, mb_mapping);
} else if (rc == -1) {
// 错误处理
fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
}
}
// 清理资源
modbus_mapping_free(mb_mapping);
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
编译命令:
gcc -o rtu_slave rtu_slave.c -lmodbus
串口参数配置
使用RTU模式时,正确配置串口参数非常重要:
// 创建RTU上下文
modbus_t *ctx = modbus_new_rtu(
"/dev/ttyUSB0", // 串口设备名
9600, // 波特率
'N', // 奇偶校验: 'N'=无, 'E'=偶校验, 'O'=奇校验
8, // 数据位
1 // 停止位
);
// 设置RTS模式(适用于RS485)
modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS485);
// 设置RTS引脚控制
modbus_rtu_set_rts(ctx, MODBUS_RTU_RTS_UP);
modbus_rtu_set_rts_delay(ctx, 2000); // 延迟2毫秒
TCP模式通信
Modbus TCP是Modbus协议的以太网变种,使用TCP/IP协议栈进行通信,端口号通常为502。
TCP主站实现
以下是一个简单的TCP主站示例:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
int main() {
modbus_t *ctx;
uint16_t reg_data[10];
int rc;
// 创建新的TCP上下文
ctx = modbus_new_tcp("192.168.1.100", 502);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 设置从站地址(在TCP中通常不需要,但某些设备可能需要)
modbus_set_slave(ctx, 1);
// 连接设备
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 读取10个保持寄存器,起始地址为0
rc = modbus_read_registers(ctx, 0, 10, reg_data);
if (rc == -1) {
fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
// 打印读取到的数据
printf("读取到%d个寄存器\n", rc);
for (int i = 0; i < rc; i++) {
printf("reg[%d]=%d (0x%X)\n", i, reg_data[i], reg_data[i]);
}
// 清理资源
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
编译命令:
gcc -o tcp_master tcp_master.c -lmodbus
TCP从站实现
下面是一个简单的TCP从站示例:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>
static int run = 1;
void signal_handler(int sig) {
run = 0;
}
int main() {
modbus_t *ctx;
modbus_mapping_t *mb_mapping;
int server_socket;
int rc;
uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
// 捕获Ctrl+C信号
signal(SIGINT, signal_handler);
// 创建新的TCP上下文
ctx = modbus_new_tcp("0.0.0.0", 502);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 分配并初始化Modbus映射
mb_mapping = modbus_mapping_new(100, 100, 100, 100);
if (mb_mapping == NULL) {
fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 设置一些初始值
mb_mapping->tab_registers[0] = 1234;
mb_mapping->tab_registers[1] = 5678;
mb_mapping->tab_bits[0] = 1;
mb_mapping->tab_bits[1] = 0;
// 创建服务器套接字
server_socket = modbus_tcp_listen(ctx, 1);
if (server_socket == -1) {
fprintf(stderr, "监听失败: %s\n", modbus_strerror(errno));
modbus_mapping_free(mb_mapping);
modbus_free(ctx);
return -1;
}
printf("从站已启动,监听端口502,按Ctrl+C退出...\n");
while (run) {
// 接受客户端连接
int client_socket = modbus_tcp_accept(ctx, &server_socket);
if (client_socket == -1) {
fprintf(stderr, "接受连接失败: %s\n", modbus_strerror(errno));
break;
}
printf("新客户端已连接\n");
while (run) {
rc = modbus_receive(ctx, query);
if (rc > 0) {
// 处理收到的请求
modbus_reply(ctx, query, rc, mb_mapping);
} else if (rc == -1) {
// 连接关闭或错误
fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
break;
}
}
}
// 清理资源
modbus_mapping_free(mb_mapping);
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
编译命令:
gcc -o tcp_slave tcp_slave.c -lmodbus
网络参数配置
使用TCP模式时的一些高级配置:
// 创建TCP上下文,IPv4
modbus_t *ctx = modbus_new_tcp("192.168.1.100", 502);
// 创建TCP上下文,支持IPv6(TCP PI)
modbus_t *ctx = modbus_new_tcp_pi("::1", "502");
// 设置响应超时
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
modbus_set_response_timeout(ctx, &timeout);
// 启用/禁用TCP长连接
int option = 1;
modbus_set_socket(ctx, &option, sizeof(int));
高级应用
数据类型转换
libmodbus提供了处理不同数据类型的函数,特别是处理浮点数:
// 单个寄存器操作
uint16_t reg_value = 12345;
modbus_write_register(ctx, 0, reg_value);
// 将两个寄存器解析为浮点数(IEEE 754格式)
float float_value;
uint16_t regs[2];
modbus_read_registers(ctx, 0, 2, regs);
// 不同的字节顺序
float_value = modbus_get_float_abcd(regs); // 最常用格式
// 或其他格式
float_value = modbus_get_float_dcba(regs);
float_value = modbus_get_float_badc(regs);
float_value = modbus_get_float_cdab(regs);
// 写入浮点数
float value_to_write = 123.45;
uint16_t regs[2];
modbus_set_float_abcd(value_to_write, regs);
modbus_write_registers(ctx, 0, 2, regs);
异常处理
适当的错误处理对于健壮的Modbus应用至关重要:
// 设置错误恢复模式
modbus_set_error_recovery(ctx,
MODBUS_ERROR_RECOVERY_LINK |
MODBUS_ERROR_RECOVERY_PROTOCOL);
// 尝试操作并处理错误
int rc = modbus_read_registers(ctx, 0, 10, regs);
if (rc == -1) {
int err = errno;
switch (err) {
case EMBXILFUN:
printf("非法功能\n");
break;
case EMBXILADD:
printf("非法数据地址\n");
break;
case EMBXILVAL:
printf("非法数据值\n");
break;
case EMBXSFAIL:
printf("从站设备故障\n");
break;
case EMBXACK:
printf("确认\n");
break;
case EMBXSBUSY:
printf("从站设备忙\n");
break;
case EMBXNACK:
printf("否认\n");
break;
case EMBXMEMPAR:
printf("内存奇偶校验错误\n");
break;
case EMBXGPATH:
printf("网关路径不可用\n");
break;
case EMBXGTAR:
printf("网关目标设备响应失败\n");
break;
case ETIMEDOUT:
printf("连接超时\n");
break;
default:
printf("未知错误: %s\n", modbus_strerror(err));
}
}
超时设置
控制通信超时对于网络不稳定环境很重要:
// 获取当前超时设置
struct timeval timeout;
modbus_get_response_timeout(ctx, &timeout);
printf("当前响应超时: %ld秒 %ld微秒\n", timeout.tv_sec, timeout.tv_usec);
// 设置新的响应超时
timeout.tv_sec = 1; // 1秒
timeout.tv_usec = 500000; // 500毫秒
modbus_set_response_timeout(ctx, &timeout);
// 设置字节超时(主要用于RTU模式)
timeout.tv_sec = 0;
timeout.tv_usec = 500000; // 500毫秒
modbus_set_byte_timeout(ctx, &timeout);
多个设备通信
通过设置从站地址,可以实现与多个设备的通信:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#define NB_DEVICES 3
#define START_ADDR 1
int main() {
modbus_t *ctx;
uint16_t reg_data[10];
int i, rc;
// 创建新的RTU上下文
ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 连接设备
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 遍历多个从站地址
for (i = 0; i < NB_DEVICES; i++) {
int slave_addr = START_ADDR + i;
// 设置从站地址
if (modbus_set_slave(ctx, slave_addr) == -1) {
fprintf(stderr, "设置从站地址失败: %s\n", modbus_strerror(errno));
continue;
}
printf("查询从站 #%d...\n", slave_addr);
// 读取寄存器
rc = modbus_read_registers(ctx, 0, 10, reg_data);
if (rc == -1) {
fprintf(stderr, "读取失败: %s\n", modbus_strerror(errno));
continue;
}
// 打印读取到的数据
printf("从站 #%d 读取到%d个寄存器\n", slave_addr, rc);
for (int j = 0; j < rc; j++) {
printf("reg[%d]=%d (0x%X)\n", j, reg_data[j], reg_data[j]);
}
printf("\n");
}
// 清理资源
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
与嵌入式设备通信
ESP32示例
以下是在ESP32上使用Arduino框架实现Modbus TCP从站的示例:
#include <WiFi.h>
#include <ModbusTCP.h>
// WiFi凭据
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
// Modbus寄存器定义
#define REG_COUNT 10
uint16_t holdingRegisters[REG_COUNT];
// 创建Modbus TCP服务器实例
ModbusTCP modbusTCP;
void setup() {
Serial.begin(115200);
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi连接成功!");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
// 初始化一些测试数据
for (int i = 0; i < REG_COUNT; i++) {
holdingRegisters[i] = i * 100;
}
// 配置Modbus服务器
modbusTCP.server();
modbusTCP.holdingRegisters(holdingRegisters, REG_COUNT);
// 启动Modbus TCP服务器
modbusTCP.begin();
Serial.println("Modbus TCP服务器已启动");
}
void loop() {
// 处理Modbus请求
modbusTCP.task();
// 更新一些动态数据(例如传感器读数)
holdingRegisters[0] = analogRead(A0);
delay(10);
}
树莓派示例
以下是在树莓派上使用libmodbus实现RTU从站的示例:
#include <stdio.h>
#include <stdlib.h>
#include <modbus.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <wiringPi.h>
#define LED_PIN 0 // BCM GPIO 17
static int run = 1;
void signal_handler(int sig) {
run = 0;
}
int main() {
modbus_t *ctx;
modbus_mapping_t *mb_mapping;
uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH];
int rc;
// 初始化WiringPi
if (wiringPiSetup() == -1) {
fprintf(stderr, "WiringPi初始化失败\n");
return -1;
}
// 配置GPIO
pinMode(LED_PIN, OUTPUT);
// 捕获Ctrl+C信号
signal(SIGINT, signal_handler);
// 创建新的RTU上下文
ctx = modbus_new_rtu("/dev/ttyAMA0", 9600, 'N', 8, 1);
if (ctx == NULL) {
fprintf(stderr, "无法创建modbus上下文\n");
return -1;
}
// 设置从站地址
modbus_set_slave(ctx, 1);
// 连接设备
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "连接失败: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
// 分配并初始化Modbus映射
mb_mapping = modbus_mapping_new(10, 0, 10, 0);
if (mb_mapping == NULL) {
fprintf(stderr, "映射分配失败: %s\n", modbus_strerror(errno));
modbus_close(ctx);
modbus_free(ctx);
return -1;
}
printf("Modbus RTU从站已启动 (地址: 1),按Ctrl+C退出...\n");
while (run) {
rc = modbus_receive(ctx, query);
if (rc > 0) {
// 处理收到的请求
modbus_reply(ctx, query, rc, mb_mapping);
// 根据线圈状态控制LED
digitalWrite(LED_PIN, mb_mapping->tab_bits[0]);
// 更新寄存器值
mb_mapping->tab_registers[0] = digitalRead(LED_PIN);
} else if (rc == -1) {
// 错误处理
fprintf(stderr, "接收失败: %s\n", modbus_strerror(errno));
}
}
// 清理资源
modbus_mapping_free(mb_mapping);
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
常见问题与解决方案
1. 设备不响应
问题症状:通信超时,设备不响应请求
可能原因:
- 串口/网络配置错误
- 设备地址不匹配
- 波特率、数据位、停止位或奇偶校验设置错误
- 电气连接问题(RS485的A/B线连接反了)
解决方案:
- 检查物理连接
- 验证串口/网络参数
- 确认从站地址与程序设置匹配
- 使用串口调试工具监控通信
- 启用调试模式:
modbus_set_debug(ctx, TRUE);
2. 数据读取错误
问题症状:读取到错误的数据或收到异常响应
可能原因:
- 访问的地址超出设备范围
- 使用了不支持的功能码
- 数据格式/字节顺序不正确
解决方案:
- 查阅设备手册,确认正确的地址范围
- 使用正确的功能码读取数据
- 尝试不同的字节顺序(ABCD、DCBA、BADC、CDAB)
3. RTU模式的时序问题
问题症状:RTU通信不稳定,偶尔丢失数据
可能原因:
- 字节超时设置不当
- RS485控制信号(RTS)管理不正确
- 高速通信下的缓冲区溢出
解决方案:
- 调整字节/响应超时设置
- 正确配置RTS控制:
modbus_rtu_set_serial_mode()和modbus_rtu_set_rts() - 减小波特率或增加缓冲区大小
4. TCP连接问题
问题症状:无法建立TCP连接
可能原因:
- 防火墙阻止了Modbus TCP端口(通常是502)
- IP地址错误
- 设备不支持Modbus TCP
解决方案:
- 检查防火墙设置,确保允许Modbus TCP通信
- 验证设备的IP地址和端口配置
- 使用ping测试网络连通性
参考资源
关注 嵌入式软件客栈 公众号,获取更多内容

7122

被折叠的 条评论
为什么被折叠?



