Modbus协议基础
在写代码之前,必须理解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 (串口)消息帧包含:

- 设备地址:1字节,指定要通信的从站地址。
- 功能码:1字节,指定要执行的操作。
- 数据:n字节,包含请求的具体信息(如寄存器地址、数量)或响应的数据。
- 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/ttyS0 或 COM3)或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 主站 程序
主站程序将连接到从站,执行以下操作:
- 读取从站的保持寄存器(地址0-3)。
- 向从站的保持寄存器(地址0)写入一个新值(123)。
- 再次读取寄存器,验证写入是否成功。
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;
}
如何编译和运行
-
保存文件:将上面的两个代码分别保存为
modbus_slave.c和modbus_master.c。 -
编译:打开终端,使用
gcc和pkg-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
-
运行:
-
第一步:先运行从站程序,它会启动并等待主站的连接。
./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程序框架,你可以基于此进行修改和扩展,以满足你的具体项目需求。
