一、前言
Modbus 的 标准全称 为 Modbus Application Protocol (Modbus 应用协议),作为工业领域 最通用、最轻量化 的 串行总线(RTU/ASCII)和以太网(TCP/IP)的工业通信协议族,核心适配工业设备的组网、数据采集与远程控制需求,凭借开源免费、 跨厂商兼容、部署简单的特性,成为工业物联网(IIoT)的基础协议之一。 常应用于 工业自动化行业,以及仪器仪表 行业;
modbus的特点:
(1)、 开源免费,无专利限制 , modbus在工业领域品牌兼容度第一;
(2)、 协议简单,易开发 / 调试 :帧结构简洁(主从式通信),仅包含 地址、功能码 、数据、校验位,工程师易编写驱动、现场排查通信故障,调试工具(如 Modbus Poll/ Slave)成熟且多为免费;
(3)、 部署成本低,硬件要求低 : 串行版(RTU/ASCII)仅需 RS-232/485 总线,无需专用通信模块; **以太网版(TCP)**可复用现有工业以太网,无需单独布线;
(4)、 主从架构稳定,适配工业场景 :采用 一主多从 通信模式(1 个主站 + 最多 247 个从站), 主站主动发起请求、从站被动响应,无冲突、延迟可控,符合工业现场 "可靠优先" 的需求;
(5)、 适配性强,协议版本丰富 :分 RTU(串行主流)、TCP(以太网)、ASCII(容错型)、RTU over TCP 等版本, 可适配从近距离串行组网到远距离以太网传输的所有工业场景;
(6)、 设备支持全覆盖:从低端的温湿度传感器、电表,到中高端的 PLC、HMI、变频器、SCADA 系统,几乎所有工业测控设备均内置 Modbus 通信接口。
Modbus 的优势决定了其 核心适配 "工业小数据、主从式、跨品牌、低成本" 的测控场景
优先使用modbud的场景:
- 跨品牌设备互联:现场存在施耐德、西门子、三菱、海康、汇川等不同品牌的 PLC、传感器、执行器,需要统一通信协议时;
- 中低实时性测控:数据刷新要求不高(秒级 / 百毫秒级),如温湿度、液位、压力采集,水泵 / 风机 / 照明的远程启停,无需毫秒级实时控制;
- 低成本小规模组网 :从站设备数量≤32 个(RS485 实际现场建议值,避免轮询过慢),如小型产线、楼宇楼层设备、市政小型泵站;
- 串行总线近距离组网:现场无工业以太网,仅需近距离(RS485 总线最大传输距离 1200 米,加中继可延长)布线,如车间产线、户外分散的传感器;
- 工业物联网(IIoT)边缘采集:作为边缘层协议,将现场设备数据采集至边缘网关,再由网关转成 MQTT/HTTP 上传至云平台(工业现场最常见的 "Modbus+MQTT" 架构);
- 老旧设备升级改造:老旧工业设备无高端通信接口,仅支持 RS232/485,需低成本实现数据上云 / 集中监控时(无需更换设备,仅加通信模块)。
二、编译部署
官网: libmodbus
源码下载: Releases · stephane/libmodbus

1、编译
1.1、生成confIg.h文件

将config.h拷贝到src文件夹下:

1.2、创建libmodbus项目,编译
(1)、将Visual Studio打开创建一个空的控制台程序:


(2)、将1.1中源码下Src文件夹下的头文件跟.c文件拷贝到项目文件夹下,将src/win32/modbus.rc文件拷贝到项目文件夹下:

(3)、将这些头文件,源文件,还有modbus.rc资源文件添加到项目中;



(4)、检查路径是否在 modbus.rc 中为 #include "modbus-version.h"

(5)、进行项目配置(以debug举例)
因为libmodbus,相对比较小,且变化不大,优选静态库的编译方式:

因为 编译的是静态库版本,这里需要修改头文件"modbus.h",如下:

cpp
//#if defined(_MSC_VER)
//# if defined(DLLBUILD)
///* define DLLBUILD when building the DLL */
//# define MODBUS_API __declspec(dllexport)
//# else
//# define MODBUS_API __declspec(dllimport)
//# endif
//#else
//# define MODBUS_API
//#endif
#define MODBUS_API
将项目路径配置到项目属性中,这里"E:\Temp\modbus\modbusSln\libmodbus"为项目文件路径


将软件链接器配置库:

关闭预处理警告: _CRT_SECURE_NO_WARNINGS,以及编译静态库宏定义


二、使用实例
使用tcp通信方式举例:
简单易用的API设计 :libmodbus提供直观的函数接口,几行代码就能实现TCP连接和数据交换。
跨平台兼容性:支持Linux、Windows等主流操作系统,确保应用部署的灵活性。
高性能通信 :优化的协议实现,支持高并发数据采集和处理。
下面以工业自动化控制场景举例:
下面实现从站代码:
编译环境是MSVC2022
main.cpp
cpp
#include "modbus.h"
#include "modbus-tcp.h"
#include <stdio.h>
#include <iostream>
#include <cstring>
#include <thread>
#include <atomic>
#include <chrono>
#include <ctime>
// windows下适配
#include <winsock2.h>
// ===================== PLC(主站)配置 =====================
const char* PLC_SLAVE_IP = "127.0.0.1"; // PLC的IP(本地测试用127.0.0.1,实际改PLC的IP)
const int PLC_SLAVE_PORT = 502; // PLC的Modbus从站端口
const int PLC_SLAVE_ID = 1; // PLC的Slave ID
// ===================== 地址映射(核心)=====================
// PC→PLC:写PLC的200-203寄存器(Modbus协议偏移,对应40001-40004)
const int PC_WRITE_PLC_START = 0; // 起始偏移
const int PC_WRITE_PLC_COUNT = 4; // 写入数量(共4个)
// PLC→PC:读PLC的204-243寄存器(Modbus协议偏移,对应40005-4044)
const int PC_READ_PLC_START = 4; // 起始偏移
const int PC_READ_PLC_COUNT = 40; // 读取数量(共40个)
// ===================== 原有从站配置 =====================
const char* SLAVE_IP = "0.0.0.0"; // 监听所有网卡
const int SLAVE_PORT = 502; // Modbus TCP默认端口
const int SLAVE_ID = 1; // 从站地址 ID
const int REGISTER_START_ADDR = 0; // 保持寄存器起始地址
const int REGISTER_COUNT = 44; // 保持寄存器数量
// 原子变量:跨线程安全的运行标志
std::atomic<bool> g_running(true);
// ===================== 新增:PC写PLC40001-40004寄存器 =====================
/**
* @brief PC主动写PLC的40001-40004寄存器(批量写)
* @param values 要写入的4个数值数组(对应40001-40004)
* @return 是否写入成功
*/
bool pc_write_plc_40001_40004(const uint16_t* values) {
// 1. 创建Modbus主站上下文(PC作为主站)
modbus_t* master_ctx = modbus_new_tcp(PLC_SLAVE_IP, PLC_SLAVE_PORT);
if (!master_ctx) {
std::cerr << "[PC->PLC failed] Failed to create master context:" << modbus_strerror(errno) << std::endl;
return false;
}
// 2. 配置超时和Slave ID
modbus_set_response_timeout(master_ctx, 2, 0); // 2秒超时
if (modbus_set_slave(master_ctx, PLC_SLAVE_ID) == -1) {
std::cerr << "[PC->PLC failed] Failed to set PLC Slave ID: " << modbus_strerror(errno) << std::endl;
modbus_free(master_ctx);
return false;
}
// 3. 连接PLC
if (modbus_connect(master_ctx) == -1) {
std::cerr << "[PC->PLC Failed] Connecting to PLC(" << PLC_SLAVE_IP << ":" << PLC_SLAVE_PORT << ") Failed: "
<< modbus_strerror(errno) << std::endl;
modbus_free(master_ctx);
return false;
}
// 4. 批量写200-203寄存器(16功能码)
int rc = modbus_write_registers(master_ctx, PC_WRITE_PLC_START, PC_WRITE_PLC_COUNT, values);
if (rc == -1) {
std::cerr << "[PC->PLC Failure] Failed to write to registers 200-203:" << modbus_strerror(errno) << std::endl;
modbus_close(master_ctx);
modbus_free(master_ctx);
return false;
}
// 5. 验证写入结果
uint16_t read_back[PC_WRITE_PLC_COUNT] = { 0 };
rc = modbus_read_registers(master_ctx, PC_WRITE_PLC_START, PC_WRITE_PLC_COUNT, read_back);
if (rc == PC_WRITE_PLC_COUNT) {
std::cout << "[PC->PLC Success] Written to registers 40001-40004:";
for (int i = 0; i < PC_WRITE_PLC_COUNT; i++) {
std::cout << (40001 + i) << "=" << read_back[i] << " ";
}
std::cout << std::endl;
}
else {
std::cout << "[PC->PLC warning] Write successful, verification read failed" << std::endl;
}
// 6. 释放资源
modbus_close(master_ctx);
modbus_free(master_ctx);
return true;
}
// ===================== 新增:PC读PLC 40005-40044寄存器 =====================
/**
* @brief PC主动读PLC的40005-40044寄存器
* @param out_values 输出数组(需提前分配40个元素空间)
* @return 是否读取成功
*/
bool pc_read_plc_40005_40044(uint16_t* out_values) {
if (!out_values) return false;
// 1. 创建Modbus主站上下文
modbus_t* master_ctx = modbus_new_tcp(PLC_SLAVE_IP, PLC_SLAVE_PORT);
if (!master_ctx) {
std::cerr << "[PLC->PC failed] Failed to create master context: " << modbus_strerror(errno) << std::endl;
return false;
}
// 2. 配置参数
modbus_set_response_timeout(master_ctx, 2, 0);
modbus_set_slave(master_ctx, PLC_SLAVE_ID);
// 3. 连接PLC
if (modbus_connect(master_ctx) == -1) {
std::cerr << "[PLC->PC Failure] Failed to connect to PLC: " << modbus_strerror(errno) << std::endl;
modbus_free(master_ctx);
return false;
}
// 4. 读取40005-40044寄存器(03功能码)
int rc = modbus_read_registers(master_ctx, PC_READ_PLC_START, PC_READ_PLC_COUNT, out_values);
if (rc == -1) {
std::cerr << "[PLC->PC Failure] Failed to read registers 40005-40044: " << modbus_strerror(errno) << std::endl;
modbus_close(master_ctx);
modbus_free(master_ctx);
return false;
}
// 5. 打印读取结果
std::cout << "[PLC->PC Success] Read registers 40005-40044:";
for (int i = 0; i < PC_READ_PLC_COUNT; i++) {
std::cout << (40005 + i) << "=" << out_values[i] << ", ";
}
std::cout << ";" << std::endl;
// 6. 释放资源
modbus_close(master_ctx);
modbus_free(master_ctx);
return true;
}
// =====================监听从站功能===========================
void listener( bool handleReqEnable) {
modbus_t* pCtx = nullptr;
modbus_mapping_t* pMb_mapping = nullptr;
int listen_socket = -1;
int client_socket = -1;
// 1. 创建Modbus从站上下文
pCtx = modbus_new_tcp(SLAVE_IP, SLAVE_PORT);
if (!pCtx) {
printf("Failed to create Modbus context:%s\n", modbus_strerror(errno));
g_running = false;
return;
}
// 2. 设置从站ID
modbus_set_slave(pCtx, SLAVE_ID);
// 3. 分配寄存器内存空间
pMb_mapping = modbus_mapping_new(
0, 0, // 禁用离散输入和线圈
REGISTER_COUNT, // 保持寄存器数量
0 // 禁用输入寄存器
);
if (nullptr == pMb_mapping) {
std::cerr << "Failed to allocate register memory:" << modbus_strerror(errno) << std::endl;
modbus_free(pCtx);
g_running = false;
return;
}
// 4. 初始化保持寄存器数据
for (int i = 0; i < REGISTER_COUNT; i++) {
pMb_mapping->tab_registers[i] = 0;
}
//// 5. 监听端口
listen_socket = modbus_tcp_listen(pCtx, 1);
if (-1 == listen_socket) {
std::cerr << "Failed to listen on port:" << modbus_strerror(errno) << std::endl;
modbus_mapping_free(pMb_mapping);
modbus_free(pCtx);
g_running = false;
return;
}
//// 打印服务信息
std::cout << "\nModbus TCP Slave service has started" << std::endl;
std::cout << "Slave service address:" << SLAVE_IP << ":" << SLAVE_PORT << ",Slave ID:" << SLAVE_ID << std::endl;
std::cout << "PLC address:" << PLC_SLAVE_IP << ":" << PLC_SLAVE_PORT << ",PLC Slave ID:" << PLC_SLAVE_ID << std::endl;
// 8. 循环处理主站请求(原有逻辑)
fd_set read_fds;
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
while (g_running) {
FD_ZERO(&read_fds);
FD_SET(listen_socket, &read_fds);
int rc = select(listen_socket + 1, &read_fds, nullptr, nullptr, &tv);
if (-1 == rc && errno != EINTR) {
std::cerr << "Select error:" << modbus_strerror(errno) << std::endl;
g_running = false;
break;
}
// 处理新连接
if (rc > 0 && FD_ISSET(listen_socket, &read_fds)) {
if (-1 != client_socket) {
modbus_close(pCtx);
client_socket = -1;
}
client_socket = accept(listen_socket, nullptr, nullptr);
if (-1 == client_socket) {
std::cerr << "Failed to accept connection:" << modbus_strerror(errno) << std::endl;
continue;
}
modbus_set_socket(pCtx, client_socket);
std::cout << "\nThe main site is connected" << std::endl;
}
// 处理主站请求
if (-1 != client_socket && g_running) {
uint8_t req_buf[MODBUS_TCP_MAX_ADU_LENGTH];
int req_len = modbus_receive(pCtx, req_buf);
if (req_len > 0) {
int resp_len = modbus_reply(pCtx, req_buf, req_len, pMb_mapping);
if (-1 == resp_len) {
std::cerr << "Failed to respond to the main site request" << std::endl;
modbus_close(pCtx);
client_socket = -1;
std::cout << "The main site connection has been disconnected" << std::endl;
}
else {
// 解析并打印主站请求(原有逻辑)
int header_len = modbus_get_header_length(pCtx);
uint8_t func_code = req_buf[header_len];
if (handleReqEnable)
{
switch (func_code) {
case MODBUS_FC_READ_HOLDING_REGISTERS: {
std::cout << "\nHandle master station read holding register request" << std::endl;
uint16_t reg_addr = (req_buf[header_len + 1] << 8) | req_buf[header_len + 2];
uint16_t reg_count = (req_buf[header_len + 3] << 8) | req_buf[header_len + 4];
std::cout << "Master read request -> Address:" << reg_addr << ",count:" << reg_count << std::endl;
for (int i = 0; i < reg_count; i++) {
if (reg_addr + i < REGISTER_COUNT) {
std::cout << " Register[" << (reg_addr + i) << "] = "
<< pMb_mapping->tab_registers[reg_addr + i] << std::endl;
}
}
break;
}
case MODBUS_FC_WRITE_SINGLE_REGISTER: {
std::cout << "\nHandling single register write requests from the master station" << std::endl;
uint16_t reg_addr = (req_buf[header_len + 1] << 8) | req_buf[header_len + 2];
uint16_t reg_value = (req_buf[header_len + 3] << 8) | req_buf[header_len + 4];
std::cout << "Main site write request -> Address:" << reg_addr << ", value:" << reg_value << std::endl;
if (reg_addr < REGISTER_COUNT) {
uint16_t oldVal = pMb_mapping->tab_registers[reg_addr];
pMb_mapping->tab_registers[reg_addr] = reg_value;
std::cout << "Slave register" << reg_addr << "]=" << oldVal << " Update to:" << reg_value << std::endl;
}
break;
}
case MODBUS_FC_WRITE_MULTIPLE_REGISTERS: {
std::cout << "\nHandling master station batch write register requests" << std::endl;
uint16_t start_addr = (req_buf[header_len + 1] << 8) | req_buf[header_len + 2];
uint16_t reg_count = (req_buf[header_len + 3] << 8) | req_buf[header_len + 4];
uint8_t byte_count = req_buf[header_len + 5];
std::cout << "Main site batch write request -> Starting address:" << start_addr << ", count:" << reg_count << std::endl;
for (int i = 0; i < reg_count; i++) {
if (start_addr + i < REGISTER_COUNT && header_len + 6 + i * 2 < req_len) {
uint16_t reg_value = (req_buf[header_len + 6 + i * 2] << 8) | req_buf[header_len + 7 + i * 2];
const uint16_t curAddr = start_addr + i;
uint16_t oldVal = pMb_mapping->tab_registers[curAddr];
pMb_mapping->tab_registers[start_addr + i] = reg_value;
std::cout << "Slave register[" << (start_addr + i) << "]=" << oldVal << " Update to:" << reg_value << std::endl;
}
}
break;
}
default:
std::cout << "\nHandling unknown function code requests:" << (int)func_code << std::endl;
break;
}
}
}
}
else if (-1 == req_len) {
modbus_close(pCtx);
client_socket = -1;
std::cout << "The main station connection has been disconnected" << std::endl;
}
}
}
// 9. 释放资源
std::cout << "\nReleasing Modbus resources...." << std::endl;
if (client_socket != -1) {
modbus_close(pCtx);
}
if (listen_socket != -1) {
#if defined(_WIN32) || defined(_WIN64)
closesocket(listen_socket);
#else
close(listen_socket);
#endif
}
if (nullptr != pMb_mapping) {
modbus_mapping_free(pMb_mapping);
}
if (nullptr != pCtx) {
modbus_free(pCtx);
}
std::cout << "Modbus slave service has stopped" << std::endl;
g_running = false;
}
// ===================== 控制台指令监听线程 =====================
void input_listener() {
std::cout << "===== Instruction Description =====" << std::endl;
std::cout << "q/Q:Exit the program" << std::endl;
std::cout << "w/W:PC writes to PLC registers 200-203 (test values: 100, 200, 300, 400)" << std::endl;
std::cout << "r/R:PC reads PLC registers 204-243" << std::endl;
std::cout << "====================\n" << std::endl;
std::string input;
while (g_running) {
std::cin >> input;
if (input == "q" || input == "Q") {
std::cout << "\nReceived exit command, preparing to stop service.." << std::endl;
g_running = false;
break;
}
// 写PLC 200-203
else if (input == "w" || input == "W") {
std::cout << "\n===== Trigger PC to Write PLC 200-203 =====" << std::endl;
uint16_t write_vals[4] = { 100, 200, 300, 400 }; // 测试值
pc_write_plc_40001_40004(write_vals);
}
// 读PLC 204-243
else if (input == "r" || input == "R") {
std::cout << "\n===== Trigger PC to read PLC 204-243 =====" << std::endl;
uint16_t read_vals[40] = { 0 };
pc_read_plc_40005_40044(read_vals);
}
else {
std::cout << "Invalid command! Supported commands: q (quit), w (write 200-203), r (read 204-243)" << std::endl;
}
}
}
// ===================== 定时读写PLC线程(可选)=====================
void timer_read_write_plc() {
while (g_running) {
// 每10秒执行一次:先写40001-40004,再读40005-40044
std::this_thread::sleep_for(std::chrono::seconds(10));
if (!g_running) break;
std::cout << "\n===== 定时执行PC↔PLC通讯 =====" << std::endl;
// 1. 写200-203(模拟业务数据)
static int count = 1;
uint16_t write_vals[4] = { 100 + count, 200 + count, 300 + count, 400 + count };
pc_write_plc_40001_40004(write_vals);
// 2. 读204-243
uint16_t read_vals[40] = { 0 };
pc_read_plc_40005_40044(read_vals);
count++;
}
}
int main() {
// 1. 启动指令监听线程, 实现读写主站寄存器数据
std::thread input_thread(input_listener);
input_thread.detach();
bool handleMainRegEnabled = false;
// 2, 启动持续监听线程
std::thread runListen_thread(listener, handleMainRegEnabled);
runListen_thread.detach();
//// 3. 启动定时读写PLC线程(可选,注释掉则关闭)
//std::thread timer_thread(timer_read_write_plc);
//timer_thread.detach();
while (g_running) {
}
#if defined(_WIN32) || defined(_WIN64)
system("pause");
#endif
return 0;
}
运行效果:

这里采用poll仿真主站

代码资料:
通过网盘分享的文件:mobus
链接: https://pan.baidu.com/s/1HiaEeYPbqgMPY8C_sQEyQQ 提取码: 8888