使用libmodbus库在相同IP地址和端口上实现多个从机

前言

之前用Python写过Modbus服务端,用的是pymodbus库,但这个库里面使用的是协程,小弟水平不够,有些hold不住,所以决定用C++把之前那块代码给重构了

使用libmodbus库

  1. 将源代码拉下来放到项目里面,源代码下载地址:https://github.com/stephane/libmodbus

  2. 编译源代码,先执行./autogen.sh,再执行./configure

    在这里插入图片描述

  3. 编写CMakeLists.txt,链接的时候加入modbus库,这里是我的一个例子

    set(TARGET modbus_tcp_server_test)
    
    file(GLOB_RECURSE SRC *.cpp)
    
    include_directories(${PROJECT_SOURCE_DIR}/src)
    
    add_executable(${TARGET} ${SRC} ${PROJECT_SOURCE_DIR}/src/server/modbus_tcp_server.cpp)
    
    target_include_directories(${TARGET} PUBLIC 
        ${GTEST_INCLUDE_DIRS})
    message("project source dir: ${PROJECT_SOURCE_DIR}")
    target_link_libraries(${TARGET} PUBLIC gtest gtest_main pthread modbus)  
    

实现modbus服务端

  1. 先定义modbus_tcp_server.h文件,将modbus_server封装成类,定义以下这些接口

    #pragma once
    #include <iostream>
    #include <vector>
    #include <mutex>
    #include "libmodbus/src/modbus-tcp.h"
    
    class ModbusTcpServer
    {
    public:
        ModbusTcpServer(int port, int slaveCount, bool debug);//端口号,从机个数,是否开启调试
        ~ModbusTcpServer();
        // 判断从机地址,寄存器地址是否合法
        bool isAddressValid(int slaveId, int address);
        // 线圈寄存器
        void setCoilValue(int slaveId, int address, uint8_t value);
        uint8_t getCoilValue(int slaveId, int address);
        // 离散输入寄存器
        void setDiscreteInputValue(int slaveId, int address, uint8_t value);
        uint8_t getDiscreteInputValue(int slaveId, int address);
        // 输入寄存器
        void setInputRegisterValue(int slaveId, int address, uint16_t value);
        uint16_t getInputRegisterValue(int slaveId, int address);
        // 保持寄存器
        void setHoldRegisterValue(int slaveId, int address, uint16_t value);
        uint16_t getHoldRegisterValue(int slaveId, int address);
        void recieveMessages();
        void start();
    
    private:
        modbus_t *_ctx;
        uint16_t *_registers; // 假设所有寄存器的值都是0
        int _slaveCount;
        std::mutex _slavemutex;
        int _errCount{0};
        int _modbusSocket{-1};
        std::vector<modbus_mapping_t *> _mappingList;
        bool _initialized{false};
        /*Mapping*/
        int _numBits{65535};
        int _numInputBits{65535};
        int _numRegisters{65535};
        int _numInputRegisters{65535};
    };
    
  2. 构造函数实现

    ModbusTcpServer::ModbusTcpServer(int port, int slaveCount, bool debug)
    {
        this->_slaveCount = slaveCount;
        _ctx = modbus_new_tcp("10.8.0.126", port);	//ip地址+端口号
        modbus_set_debug(_ctx, debug);
        if (_ctx == nullptr)
        {
            LOG_DEBUG("初始化ModbusServer错误!");
            throw -1;
        }
        if (_modbusSocket < 0)
        {
            _modbusSocket = modbus_tcp_listen(_ctx, 1);
        }
        // 设置从机0-slaveCount的地址映射
        for (int i = 0; i <= slaveCount; ++i)
        {
            /*设置线圈, 离散输入, 输入寄存器, 保持寄存器个数(数组元素个数))*/
            modbus_mapping_t *_mapping = modbus_mapping_new(_numBits, _numInputBits, _numInputRegisters, _numRegisters);
            _mappingList.push_back(_mapping);
            if (_mapping == nullptr)
            {
                LOG_ERROR("初始化ModbusMap失败!");
                modbus_free(_ctx);
                _initialized = false;
                return;
            }
        }
        _initialized = true;
    }
    
  3. 析构函数实现

    ModbusTcpServer::~ModbusTcpServer()
    {
        // 清理资源
        modbus_close(_ctx);
        modbus_free(_ctx);
        for (auto &mapping : _mappingList)
        {
            modbus_mapping_free(mapping);
        }
    }
    
  4. 接收消息处理函数(重点)

    之前我在网上搜遍资料,都没有找到一个ip和端口号设置多个从机的教程,后面又看了一整天libmodbus的源码,发现源码里根本就没有处理多个从机地址的情况。最后我发现是要自己手动处理接收到报文里的从机地址,否则所有回复的报文都是一台从机的情况。

    这里支持多个主站连接的代码参考了这篇博客:https://blog.csdn.net/qq_38158479/article/details/120928043

    void ModbusTcpServer::recieveMessages()
    {
        // 初始化
        uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
        fd_set reads, cpy_reads;
        FD_ZERO(&reads);
        FD_SET(_modbusSocket, &reads);
        int fdMax = _modbusSocket;
        while (true)
        {
            memset(query, 0, sizeof(query));
            cpy_reads = reads;
            int fdNum = select(fdMax + 1, &cpy_reads, 0, 0, 0);
            if (fdNum == -1)
                break;
            else if (fdNum == 0)
                continue;
            for (int i = 0; i < fdMax + 1; i++)
            {
                if (FD_ISSET(i, &cpy_reads))
                {
                    if (i == _modbusSocket)
                    {
                        int clntSock = modbus_tcp_accept(_ctx, &_modbusSocket);
                        if ((_modbusSocket == -1) || (clntSock == -1))
                        {
                            LOG_ERROR("accept error!");
                            std::cerr << modbus_strerror(errno) << std::endl;
                            continue;
                        }
                        FD_SET(clntSock, &reads);
                        if (fdMax < clntSock)
                            fdMax = clntSock;
                    }
                    else
                    {
                        int ret = modbus_receive(_ctx, query);
                        if (ret == 0)
                        {
                            _errCount = 0;
                            continue;
                        }
                        else if (ret > 0)
                        {
                            _errCount = 0;
                            uint8_t slave = query[6]; // 获取收到问询的从机号
                            if (slave <= static_cast<uint8_t>(_slaveCount))
                            {
                                // 只回复当前从机的报文,从机号不符合就不回复
                                modbus_set_slave(_ctx, slave);
                                modbus_reply(_ctx, query, sizeof(query), _mappingList[slave]);
                            }
                        }
                        else
                        {
                            modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_NONE);
                            modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_LINK);
                            modbus_close(_ctx);
                            FD_CLR(i, &reads);
                            LOG_ERROR("报文错误,服务端断开连接!");
                            close(i);
                            _errCount++;
                        }
                        if (_errCount > 5)
                        {
                            _initialized = false;
                            break;
                        }
                    }
                }
            }
        }
        _initialized = false;
    }
    
  5. 启动服务

    void ModbusTcpServer::start()
    {
        std::thread loop([this]()
                         {
            while (true)
            {
                if (_initialized)
                {
                    recieveMessages();
                }
                else
                {
                    _initialized = true;
                }
            } });
        // 将线程放在后台运行,防止阻塞主线程
        loop.detach();
    }
    
  6. 判断从机地址和寄存器地址是否合法

    bool ModbusTcpServer::isAddressValid(int slaveId, int address)
    {
        if (slaveId < 0 || slaveId > _slaveCount)
        {
            LOG_ERROR(fmt::format("从机号错误, 从机号: {}!", slaveId));
            return false;
        }
        if (address > (_numRegisters - 1))
        {
            LOG_ERROR(fmt::format("寄存器地址错误, 寄存器地址: {}!", address));
            return true;
        }
        return true;
    }
    
  7. 读写线圈寄存器

    void ModbusTcpServer::setCoilValue(int slaveId, int address, uint8_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_bits[address] = value;
    }
    
    uint8_t ModbusTcpServer::getCoilValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_bits[address];
    }
    
  8. 读写离散输入寄存器

    void ModbusTcpServer::setDiscreteInputValue(int slaveId, int address, uint8_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_input_bits[address] = value;
    }
    
    uint8_t ModbusTcpServer::getDiscreteInputValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_input_bits[address];
    }
    
  9. 读写输入寄存器

    void ModbusTcpServer::setInputRegisterValue(int slaveId, int address, uint16_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_registers[address] = value;
    }
    
    uint16_t ModbusTcpServer::getInputRegisterValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
    
            return 0;
        return _mappingList[slaveId]->tab_registers[address];
    }
    
  10. 读写保持寄存器

    void ModbusTcpServer::setHoldRegisterValue(int slaveId, int address, uint16_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _slavemutex.lock();
        _mappingList[slaveId]->tab_registers[address] = value;
        _slavemutex.unlock();
    }
    
    uint16_t ModbusTcpServer::getHoldRegisterValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_registers[address];
    }
    
  11. modbus_tcp_server.cpp所有代码

    #include "modbus_tcp_server.h"
    #include <cstring>
    #include "log.h"
    
    ModbusTcpServer::ModbusTcpServer(int port, int slaveCount, bool debug)
    {
        this->_slaveCount = slaveCount;
        _ctx = modbus_new_tcp("10.8.0.126", port);
        // 设置从机地址
        modbus_set_slave(_ctx, 1);
        modbus_set_debug(_ctx, debug);
        if (_ctx == nullptr)
        {
            LOG_DEBUG("初始化ModbusServer错误!");
            throw -1;
        }
        if (_modbusSocket < 0)
        {
            _modbusSocket = modbus_tcp_listen(_ctx, 1);
        }
        // 设置从机0-slaveCount的地址映射
        for (int i = 0; i <= slaveCount; ++i)
        {
            /*设置线圈, 离散输入, 输入寄存器, 保持寄存器个数(数组元素个数))*/
            modbus_mapping_t *_mapping = modbus_mapping_new(_numBits, _numInputBits, _numInputRegisters, _numRegisters);
            _mappingList.push_back(_mapping);
            if (_mapping == nullptr)
            {
                LOG_ERROR("初始化ModbusMap失败!");
                modbus_free(_ctx);
                _initialized = false;
                return;
            }
        }
        _initialized = true;
    }
    
    ModbusTcpServer::~ModbusTcpServer()
    {
        // 清理资源
        modbus_close(_ctx);
        modbus_free(_ctx);
        for (auto &mapping : _mappingList)
        {
            modbus_mapping_free(mapping);
        }
    }
    
    bool ModbusTcpServer::isAddressValid(int slaveId, int address)
    {
        if (slaveId < 0 || slaveId > _slaveCount)
        {
            LOG_ERROR(fmt::format("从机号错误, 从机号: {}!", slaveId));
            return false;
        }
        if (address > (_numRegisters - 1))
        {
            LOG_ERROR(fmt::format("寄存器地址错误, 寄存器地址: {}!", address));
            return true;
        }
        return true;
    }
    
    void ModbusTcpServer::setCoilValue(int slaveId, int address, uint8_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_bits[address] = value;
    }
    
    uint8_t ModbusTcpServer::getCoilValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_bits[address];
    }
    
    void ModbusTcpServer::setDiscreteInputValue(int slaveId, int address, uint8_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_input_bits[address] = value;
    }
    
    uint8_t ModbusTcpServer::getDiscreteInputValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_input_bits[address];
    }
    
    void ModbusTcpServer::setInputRegisterValue(int slaveId, int address, uint16_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _mappingList[slaveId]->tab_registers[address] = value;
    }
    
    uint16_t ModbusTcpServer::getInputRegisterValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
    
            return 0;
        return _mappingList[slaveId]->tab_registers[address];
    }
    
    void ModbusTcpServer::setHoldRegisterValue(int slaveId, int address, uint16_t value)
    {
        if (!isAddressValid(slaveId, address))
            return;
        _slavemutex.lock();
        _mappingList[slaveId]->tab_registers[address] = value;
        _slavemutex.unlock();
    }
    
    uint16_t ModbusTcpServer::getHoldRegisterValue(int slaveId, int address)
    {
        if (!isAddressValid(slaveId, address))
            return 0;
        return _mappingList[slaveId]->tab_registers[address];
    }
    
    void ModbusTcpServer::recieveMessages()
    {
        // 初始化
        uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
        fd_set reads, cpy_reads;
        FD_ZERO(&reads);
        FD_SET(_modbusSocket, &reads);
        int fdMax = _modbusSocket;
        while (true)
        {
            memset(query, 0, sizeof(query));
            cpy_reads = reads;
            int fdNum = select(fdMax + 1, &cpy_reads, 0, 0, 0);
            if (fdNum == -1)
                break;
            else if (fdNum == 0)
                continue;
            for (int i = 0; i < fdMax + 1; i++)
            {
                if (FD_ISSET(i, &cpy_reads))
                {
                    if (i == _modbusSocket)
                    {
                        int clntSock = modbus_tcp_accept(_ctx, &_modbusSocket);
                        if ((_modbusSocket == -1) || (clntSock == -1))
                        {
                            LOG_ERROR("accept error!");
                            std::cerr << modbus_strerror(errno) << std::endl;
                            continue;
                        }
                        FD_SET(clntSock, &reads);
                        if (fdMax < clntSock)
                            fdMax = clntSock;
                    }
                    else
                    {
                        int ret = modbus_receive(_ctx, query);
                        if (ret == 0)
                        {
                            _errCount = 0;
                            continue;
                        }
                        else if (ret > 0)
                        {
                            _errCount = 0;
                            uint8_t slave = query[6]; // 获取收到问询的从机子号
                            if (slave <= static_cast<uint8_t>(_slaveCount))
                            {
                                modbus_set_slave(_ctx, slave);
                                modbus_reply(_ctx, query, sizeof(query), _mappingList[slave]);
                            }
                        }
                        else
                        {
                            modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_NONE);
                            modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_LINK);
                            modbus_close(_ctx);
                            FD_CLR(i, &reads);
                            LOG_ERROR("报文错误,服务端断开连接!");
                            close(i);
                            _errCount++;
                        }
                        if (_errCount > 5)
                        {
                            _initialized = false;
                            break;
                        }
                    }
                }
            }
        }
        _initialized = false;
    }
    void ModbusTcpServer::start()
    {
        std::thread loop([this]()
                         {
            while (true)
            {
                if (_initialized)
                {
                    recieveMessages();
                }
                else
                {
                    _initialized = true;
                }
            } });
        loop.detach();
    }
    

单元测试

  1. 使用gtest进行单元测试,测试代码如下

    #include "server/modbus_tcp_server.h"
    #include "gtest/gtest.h"
    
    class ModbusTcpServerTest : public testing::Test
    {
    public:
        std::unique_ptr<ModbusTcpServer> server;
    
    protected:
        virtual void SetUp()
        {
            try
            {
                server = std::make_unique<ModbusTcpServer>(5020, 10, true); // 创建Modbus服务器实例,监听端口5020,配置10台从机
                server->start();                                            // 启动服务器
            }
            catch (const std::exception &e)
            {
                std::cerr << "Exception: " << e.what() << std::endl;
            }
        }
    };
    
    TEST_F(ModbusTcpServerTest, TestSetRegisterValue)
    {
        server->setHoldRegisterValue(1, 1, 100);
        ASSERT_EQ(100, server->getHoldRegisterValue(1, 1));
        
        server->setInputRegisterValue(1, 1, 200);
        ASSERT_EQ(200, server->getInputRegisterValue(1, 1));
        
        server->setCoilValue(1, 1, 1);
        ASSERT_TRUE(server->getCoilValue(1, 1));
        
        server->setDiscreteInputValue(1, 1, 1);
        ASSERT_TRUE(server->getDiscreteInputValue(1, 1));
    }
    
    int main()
    {
        testing::InitGoogleTest();
        return RUN_ALL_TESTS();
    }
    
  2. 测试结果
    在这里插入图片描述

使用网络调试助手测试

​ 为了判断我们多从机是否设置成功,我们可以用网络调试助手手发报文测试

  1. 为了让服务一直运行,我们可以把启动服务里的detach改成join,让其阻塞主线程
    在这里插入图片描述

  2. 用网络调试助手连接modbus服务端
    在这里插入图片描述

  3. 设置从机地址1,输入寄存器地址1的值为1

    报文格式:00 00 00 00 00 06 01 06 00 01 00 01

    重点看后面7位

    06:后面有6个值

    01:从机地址为1

    06:功能码,写输入寄存器

    00 01:输入寄存器的地址,4位16进制数,转换成10进制就是0-65535

    00 01:寄存器值,范围与上面寄存器地址范围相同
    在这里插入图片描述

    使用06功能码的时候,回复的值是一大串数据,这个没有影响

  4. 读取从机地址1,输入寄存器地址1的值,判断刚才手发的报文是否成功将值写入

    报文格式:00 00 00 00 00 06 01 03 00 01 00 01

    在这里插入图片描述

    报文回复的后四位为00 01,值为1,说明值被成功写入了

  5. 读取从机地址2,输入寄存器地址1的值,判断之前的写命令是否干扰了其他从机

    报文格式:00 00 00 00 00 06 02 03 00 01 00 01

    在这里插入图片描述

    报文回复的后四位为00 00,值为0,说明之前的写命令没有干扰其他从机

  • 23
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MC皮蛋侠客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值