modbus-通关速成

一、前言

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

相关推荐
寻寻觅觅☆7 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc7 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
ceclar1238 小时前
C++使用format
开发语言·c++·算法
lanhuazui109 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee449 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
老约家的可汗9 小时前
初识C++
开发语言·c++
crescent_悦9 小时前
C++:Product of Polynomials
开发语言·c++
小坏坏的大世界10 小时前
CMakeList.txt模板与 Visual Studio IDE 操作对比表
c++·visual studio
乐观勇敢坚强的老彭10 小时前
c++寒假营day03
java·开发语言·c++
愚者游世11 小时前
brace-or-equal initializers(花括号或等号初始化器)各版本异同
开发语言·c++·程序人生·面试·visual studio