【Qt Modbus通信】libmodbus实现modbus的主机功能/从机功能 源码分享

前言

modbus在上下位机数据交互时被广泛使用,因此写了这篇笔记和大家一起学习。

参考文献

Libmodbus源码分析(二)-常用接口函数分析
Libmodbus源码分析(四)-RTU相关函数分析
【Qt Modbus通信】QModbus实现modbus的主机功能 源码分享

第三方库 libmodbus

网上有一个现成的libmodbus C库,支持Linux, Mac OS X, FreeBSD, QNX 和 Win32。
下载地址为:http://libmodbus.org/download/

基于libmodbus开源的学习例程Qmodbus

亲测源码支持QT4/QT5版本,在window和Linux环境下都可以使用
官网:http://qmodbus.sourceforge.net/
源码:https://github.com/ed-chemnitz/qmodbus/
如果下载不了也可以在我的GitHub上下载
该DEMO支持 RTU/TCP/ASCII模式
在这里插入图片描述

下面我将DEMO中的关键代码移植出来,实现了一个运行在子线程中且包含了主机功能和从机功能的程序。

项目配置

我将需要用到的libmodbus源码放在了这个路径下 大家可以在我的GitHub上自行下载
在这里插入图片描述
需要用到的文件有
在这里插入图片描述

需要注意的地方
加上以下内容就可以在运行在window或ubuntu环境下

unix {
    SOURCES += 3rdparty/qextserialport/posix_qextserialport.cpp	\
           3rdparty/qextserialport/qextserialenumerator_unix.cpp
    DEFINES += _TTY_POSIX_
}

win32 {
    SOURCES += 3rdparty/qextserialport/win_qextserialport.cpp \
           3rdparty/qextserialport/qextserialenumerator_win.cpp
    DEFINES += _TTY_WIN_  WINVER=0x0501
    LIBS += -lsetupapi -lws2_32
}

主机功能

程序运行效果
在这里插入图片描述
完成功能:modbus主机在子线程中每隔一秒钟向modbus Slave 请求 40030-40035寄存器的内容。

关键代码演示

1.将modbusPollThread类放到子线程中
slaveID 指定读取从机的ID地址
为什么要子线程中运行:因为读取从机数据是一个比较耗时的操作如果放在主线程运行会导致QT的UI界面卡顿

modbusPollThread::modbusPollThread(int slaveID, QObject *parent)
{
    qDebug() << "modbusPollThread" << QThread::currentThreadId();
    m_slaveId = slaveID;        //从机ID
    isWork = false;             //modbus是否连接成功
    m_pollThread = new QThread();
    this->moveToThread(m_pollThread);
    connect(m_pollThread,SIGNAL(started()),this,SLOT(initPollThread()));
    connect(m_pollThread,SIGNAL(finished()),this,SLOT(ClosePollThread()));
    m_pollThread->start();
}

2.连接从机
特别注意:
modbus_register_monitor_add_item_fnc
modbus_register_monitor_raw_data_fnc 这两个回调函数可以帮助你获取更多的主从交互信息

void modbusPollThread::connnectModbusPoll(QString portName, int baud)
{
    qDebug() << "connnectModbusPoll" << QThread::currentThreadId();
    my_bus = modbus_new_rtu(portName.toLatin1().data(),baud,'N',8,1);   //无法验证串口端口是否被占用
    modbus_set_slave(my_bus,m_slaveId);                    //设置从机地址为1
    modbus_connect(my_bus);

    //寄存器map初始化
    mb_mapping = modbus_mapping_new(READSTARTADDR, READSTARTADDR,
                                    READSTARTADDR, READSTARTADDR); //依次设置 bits、input_bits、registers、input_registers寄存器的大小,他们默认起始地址均为0
    if (mb_mapping == NULL) {
        qDebug() << "connect fail" << (stderr, "Failed to allocate the mapping: %s\n",
                                       modbus_strerror(errno));
        modbus_free(my_bus);
        return;
    }

    modbus_register_monitor_add_item_fnc(my_bus, modbusPollThread::stBusMonitorAddItem);
    modbus_register_monitor_raw_data_fnc(my_bus, modbusPollThread::stBusMonitorRawData);
    Time_one->start(1000);
    isWork = true;
}

3.设置modbus回调函数

//modbus回调函数
void modbusPollThread::stBusMonitorAddItem( modbus_t * modbus, uint8_t isRequest, uint8_t slave, uint8_t func, uint16_t addr, uint16_t nb, uint16_t expectedCRC, uint16_t actualCRC )
{
    Q_UNUSED(modbus);
    qDebug() << "成功接受到从机回复" << "isRequest" << isRequest << "slave" << slave << "func" << func\
             << "addr" << addr << "nb" << nb << "expectedCRC" << expectedCRC  << "actualCRC" << actualCRC;
}

// 送成功/接受成功会到此回掉函数
void modbusPollThread::stBusMonitorRawData( modbus_t * modbus, uint8_t * data, uint8_t dataLen, uint8_t addNewline , uint8_t direction)
{
    Q_UNUSED(modbus);
    QString dump;
    for( int i = 0; i < dataLen; ++i )
    {
        dump += QString::asprintf( "%.2x ", data[i] );
    }
    //    if(direction == 0)
    //        qDebug() << "串口发送"  << "data" << dump;
    //    else
    //        qDebug() << "串口接受" << "data" << dump;
}

4.读取从机数据
4.1读取保持寄存器内容使用接口 modbus_read_registers
4.2读取输入寄存器使用接口 modbus_read_input_registers
4.3下面的函数的功能为 向从机ID为m_slaveId的从机读取 40030-40034的寄存器内容
如果modbus_read_registers的返回值不等于读取的寄存器长度 则说明读取失败

uint16_t modbus_hold_reg[100];      //缓存读取到的数据
void modbusPollThread::modbus_update_text()
{
    int readNum = 5;
    modbus_set_slave(my_bus,m_slaveId);//设置需要连接的从机地址

    int ret = modbus_read_registers(my_bus,40030,readNum,modbus_hold_reg);            //读取保持寄存器的第0位开始的前5位
    //modbus_read_input_registers(my_bus,0,50,modbus_input_reg);   //读取输入寄存器的第0位开始的前5位
    QString err;
    if(ret != readNum)      //读取错误
    {
        if( ret < 0 )
        {
            if(
    #ifdef WIN32
                    errno == WSAETIMEDOUT ||
    #endif
                    errno == EIO
                                                                    )
            {
                err += tr( "I/O error" );
                err += ": ";
                err += tr( "did not receive any data from slave." );
            }
            else
            {
                err += tr( "Protocol error" );
                err += ": ";
                err += tr( "Slave threw exception '" );
                err += modbus_strerror( errno );
                err += tr( "' or function not implemented." );
            }
        }
        else
        {
            err += tr( "Protocol error" );
            err += ": ";
            err += tr( "Number of registers returned does not "
                    "match number of registers requested!" );
        }
    }

    if( err.size() > 0 )
            emit SIGNAL_SENDGETSLAVEINFO("读取失败:" + err);
    else        //读取正常
    {
        QString info = QString("从机ID: %1 modbus read : %2  %3  %4  %5  %6 \r\n ")\
                .arg(QString::number(m_slaveId)).arg(modbus_hold_reg[0])\
             .arg(modbus_hold_reg[1]).arg(modbus_hold_reg[2])\
             .arg(modbus_hold_reg[3]).arg(modbus_hold_reg[4]);

        emit SIGNAL_SENDGETSLAVEINFO(info);
    }
}

5.UI主线程中 使用主机功能和从机功能
5.1实例化

	//传入主机ID 1
	m_slave = new modbusSlaveThread(1);
    connect(this,&MainWindow::signal_connectSlave,m_slave,&modbusSlaveThread::connnectModbusSlave);
    connect(this,&MainWindow::signal_disconnectSlave,m_slave,&modbusSlaveThread::disconnnectModbusSlave);
    //传入从机ID 1
    m_poll = new modbusPollThread(1);
    connect(this,&MainWindow::signal_connectPoll,m_poll,&modbusPollThread::connnectModbusPoll);
    connect(this,&MainWindow::signal_disconnectPoll,m_poll,&modbusPollThread::disconnnectModbusPoll);
    connect(m_poll,&modbusPollThread::SIGNAL_SENDGETSLAVEINFO,this,&MainWindow::onShowSlaveInfo);

5.2 开启主机/关闭主机

void MainWindow::on_pushButton_Poll_clicked()
{
    if(ui->pushButton_Poll->text() == "开启主机")
    {
        if(ui->comboBox_name_POLL->currentText().isEmpty()==true)
        {
            ui->textEdit_POLL->setText(ui->textEdit_POLL->toPlainText().append("未设置设备号\r\n"));
            ui->textEdit_POLL->moveCursor(QTextCursor::End);        //textedit 滚动条自动往下滚动
            return;
        }
        QString namestring = ui->comboBox_name_POLL->currentText();
        uint modbus_baud = ui->comboBox_baud_POLL->currentText().toUInt();
		//发送信号 开启主机
        emit signal_connectPoll(namestring,modbus_baud);
        ui->pushButton_Poll->setText("关闭主机");
    }else {
    	//发送信号关闭主机
        emit signal_disconnectPoll();
        ui->pushButton_Poll->setText("开启主机");
    }
}

从机功能

程序运行效果
使用modbus Poll定时向程序读取 寄存器1-5的内容
在这里插入图片描述

关键代码演示

从机代码和主机代码差不多
1.请求连接主机
1.1 设置保持寄存器的值 修改tab_registers数组
1.2 设置输入寄存器的值 修改tab_input_bits数组

void modbusSlaveThread::connnectModbusSlave(QString portName, int baud)
{
    qDebug() << "connnectModbusSlave" << QThread::currentThreadId();
    my_bus = modbus_new_rtu(portName.toLatin1().data(),baud,'N',8,1);
    modbus_set_slave(my_bus,m_Id);                    //设置从机地址为1
    modbus_connect(my_bus);

    //寄存器map初始化
    mb_mapping = modbus_mapping_new(40099, 40099,
                                    40099, 40099); //依次设置 bits、input_bits、registers、input_registers寄存器的大小,他们默认起始地址均为0
    if (mb_mapping == NULL) {
        qDebug() << "connect fail" << (stderr, "Failed to allocate the mapping: %s\n",
        modbus_strerror(errno));
        modbus_free(my_bus);
        return;
    }

    mb_mapping->tab_registers[1] = 77; //设置一下hold寄存器的值
    mb_mapping->tab_registers[2] = 77;
    mb_mapping->tab_registers[3] = 3;
    mb_mapping->tab_registers[4] = 4;
    mb_mapping->tab_registers[5] = 5;

    modbus_register_monitor_add_item_fnc(my_bus, modbusSlaveThread::stBusMonitorAddItem);
    modbus_register_monitor_raw_data_fnc(my_bus, modbusSlaveThread::stBusMonitorRawData);
    Time_one->start(300);
    isWork = true;
}

2.定时轮询判断是否有主机向从机请求数据
modbus_receive 返回值大于0说明主机向从机发送了请求

void modbusSlaveThread::modbus_slave_work()
{
    int rc;
    //qDebug() << "modbus_slave_work" << QThread::currentThreadId();
    uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH];
    //轮询接收数据,并做相应处理
    rc = modbus_receive(my_bus, query);
    if (rc > 0) {
        modbus_reply(my_bus, query, rc, mb_mapping);
    }
}

调试工具

破解好的调试工具大家可以直接在我的GitHub上下载
在没用设备可以调试程序的时候,我们可以使用虚拟串口工具Virtual Serial Port Driver Pro
在这里插入图片描述
Modbus Poll 是Modbus主设备模拟软件
Modbus Slave 是Modbus从设备模拟软件

源码下载:https://github.com/jbyyy/libmodbusDemo

在这里插入图片描述
ModbusSlave_Poll文件夹是该文章的源码
qmodbus-master.zip是Qmodbus软件的开源程序

  • 24
    点赞
  • 124
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
Qt是一个跨平台的C++开发框架,提供了丰富的功能和工具,可以实现各种类型的应用程序开发。要在Qt实现Modbus从机功能,可以使用Qt串口通信库和Modbus协议库进行开发。 首先,我们需要使用Qt串口通信库来建立连接并配置串口。可以通过使用QSerialPort类来访问和控制串口。使用类似下面的代码来创建一个QSerialPort对象,并设置串口的参数: ``` QSerialPort serialPort; serialPort.setPortName("COM1"); // 设置串口名字 serialPort.setBaudRate(QSerialPort::Baud9600); // 设置波特率 serialPort.setDataBits(QSerialPort::Data8); // 设置数据位 serialPort.setParity(QSerialPort::NoParity); // 设置校验位 serialPort.setStopBits(QSerialPort::OneStop); // 设置停止位 ``` 之后,我们需要使用QtModbus协议库来实现Modbus从机功能。可以使用QModbusServer类来创建一个Modbus从机对象,并配置从机的地址和功能码: ``` QModbusServer modbusServer; modbusServer.setConnectionParameter(QModbusServer::SerialPortNameParameter, "COM1"); // 设置Modbus从机连接的串口 modbusServer.setServerAddress(1); // 设置Modbus从机地址 // 设置Modbus从机支持的功能modbusServer.setData(MODBUS_SERVER_READ_HOLDING_REGISTERS, new ModbusServerData()); modbusServer.setData(MODBUS_SERVER_WRITE_SINGLE_REGISTER, new ModbusServerData()); ``` 接下来,我们可以使用Qt的信号槽机制来处理串口数据的接收和发送,并实现Modbus从机功能。可以通过使用QSerialPort的readyRead()信号来接收串口数据,并解析Modbus请求。根据不同的功能码,执行相应的操作,并根据需要发送Modbus响应。 总结起来,通过使用Qt串口通信库和Modbus协议库,我们可以很方便地在Qt实现Modbus从机功能。需要建立串口连接并配置参数,创建Modbus从机对象并设置地址和功能码,使用信号槽机制处理串口数据的接收和发送,实现Modbus从机功能。上述代码仅作为示例,具体实现根据需要进行调整和扩展。
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jbyyy、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值