软件设计开发笔记6:基于QT的Modbus RTU从站

news2025/1/21 8:50:00

  Modbus是一种常见的工业系统通讯协议。在我们的设计开发工作中经常使用到它。作为一种主从协议,在上一篇我们实现了Mobus RTU主站工具,接下来这一篇中我们将简单实现一个基于QT的Mobus RTU从站工具。

1、概述

  Modbus RTU从站应用很常见,有一些是通用的,有一些是专用的。而这里我们希望实现一个主要针对我们的产品调试的Modbus RTU从站工具。
  在开始软件设计之前,我们先来简略地分析一下,实现这样一个Modbus RTU从站工具包含的主要内容有哪些。我们认为软件需要如下几个方面的内容:

(1)、串口参数的配置

  Modbus RTU通过串口来实现通讯,所以我们需要对串口相关的参数进行配置。我们希望串口号能够自动搜索,而相应的配置参数我们可以选择。
  对串口的操作我们希望可以适应不同使用情况下的需求,让使用者可以自行选择串口名、波特率、校验位、数据位和停止位等,并控制串口的打开和关闭。

(2)、对数据的配置

  数据的修改及更新,因为我们是从站实例,所以我们需要设置从站的参数类型和数量。我们将实现Modbus协议规定的4中数据类型:线圈、状态、输入寄存器和保持寄存器。
  这四种数据其中线圈和状态是布尔量;输入寄存器和保持寄存器是16位整型量,这是指他的存储形式,事实上多个寄存器可以表示各种应用数据类型;线圈和保持寄存器支持读写;状态和输入寄存器位只读。

(3)、对主站的响应

  对读从站数据,主要是指Modbus的功能码0x01、0x02、0x03和0x04等的处理。收到这几个功能码后,我们根据报文中的首地址和数量返回给主站即可。
  对写从站数据,主要是指Modbus的功能码0x05、0x06、0x0F和0x10等的处理。对于这几个功能码,我们解析到这几个功能码后,根据指定的起始地址和数值对显影的变量值进行修改。

(4)、对信息的显示

  接收信息的显示,作为调试工具,我们肯定希望能够一目了然地看到接收到目标设备发送过来的消息,所以我们需要一个显示区域来对接收的区域进行显示。
  对数据的显示,我们在界面上设计各种数据类型的指示,线圈和状态使用CheckBox来显示其状态。当置位时现实为选中,复位时显示为不选中。而对于输入寄存器和保  持寄存器则可以直接显示它的数值。变量数据改变时则更新界面显示;同时界面状态改变时则更行变量数据。
  运行状态的显示, 我们希望对操作的状态进行反馈以指示操作的动作是否执行,所以我们需要状态栏来实现这一需求。

2、界面设计

  根据上一节中分析的需求,我们先来设计软件的界面。我们在QT中基于QMainWindow类生成一个操作界面,包括菜单栏、工具栏和状态栏以满足需求中对状态显示及操作命令的要求。
  我们将上部区域设置为串口参数及Modbus RTU从站参数的配置于操作区域。而下面我们以5列17行的形式展示各种数据类型的数据状态。具体的界面设置如下图所示:

  完成如上图的布局后,我们可以配置相应的参数,我们在程序初始化时配置好串口及Modbus RTU从站的默认参数。完成整个布局后我们先试着运行程序,正常运行则出现如下的界面:

  上图就是完成布局后的运行界面,不过我们还没有实现相应的编码,所以目前还不能实现我们第一节中所提出来的功能。

3、编码实现

  接下来这一小节,我们将来编码实现相应的功能。我们主要将功能分为参数设置与操作功能、数据的输入与发送功能以及数据的接收与显示功能三个部分来实现。

3.1、串口配置功能

  对于参数的配置除了串口号以外,我们可以在程序中默认设置相应的参数,如果有必要可以修改,如果默认参数能满足要求亦可直接使用。至于串口号默认的不一定时我们所希望的,根据硬件配置情况选择即可。参数选定后我们只需要点击“连接”按钮即可实现Modbus RTU从站的连接。而连接操作的具体实现方式如下:

void MainWindow::on_pushButtonConnect_clicked()
{
    bool intendToConnect = (modbusDevice->state() == QModbusDevice::UnconnectedState);

    statusBar()->clearMessage();

    if (intendToConnect)
    {
        modbusDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,ui->comboBoxPort->currentText());
        modbusDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,QSerialPort::NoParity);
        modbusDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,ui->comboBoxBaud->currentText().toInt());
        modbusDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,QSerialPort::Data8);
        modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::OneStop);

        modbusDevice->setServerAddress(ui->spinBoxStation->text().toInt());
        if (!modbusDevice->connectDevice())
        {
            statusBar()->showMessage(tr("连接失败: ") + modbusDevice->errorString(), 5000);
        }
        else
        {
            ui->pushButtonConnect->setEnabled(false);
            ui->pushButtonDisconnect->setEnabled(true);
        }
    }
}

3.2、对界面操作的响应

  首先我们在界面上操作各个类型的变量时,我们希望变量中的数据会跟随我们的操作而改变,并能被从站访问到。对于4种类型的数据将采用类似的处理方式。
  对于线圈和状态量,当我们在界面上点击复选框改变选中状态时,会触发对应的槽函数,并肩对应的编号作为参数传递进去,槽函数会处理将Modbus RTU从站定义的变量状态改为和复选框的状态一致。具体实现如下:

void MainWindow::coilChanged(int id)
{
    if(!modbusDevice)
    {
        return;
    }

    QAbstractButton *button = coilButtons.button(id);

    if (!modbusDevice->setData(QModbusDataUnit::Coils, quint16(id), button->isChecked()))
    {
        statusBar()->showMessage(tr("Could not set data: ") + modbusDevice->errorString(), 5000);
    }
}

void MainWindow::discreteInputChanged(int id)
{
    if(!modbusDevice)
    {
        return;
    }

    QAbstractButton *button = discreteButtons.button(id);

    if (!modbusDevice->setData(QModbusDataUnit::DiscreteInputs, quint16(id), button->isChecked()))
    {
        statusBar()->showMessage(tr("Could not set data: ") + modbusDevice->errorString(), 5000);
    }
}

  对于输入寄存器和保持寄存器在界面上使用的LineEdit来显示数据值,所以当我们改变LineEdit的内容时,让其触发槽函数。在槽函数中我们将Modbus RTU从站对应的寄存器的值改为和LineEdit的内容一样。具体实现如下:

void MainWindow::setInputRegister(const QString &value)
{
    if(!modbusDevice)
    {
        return;
    }

    const QString objectName = QObject::sender()->objectName();

    if(inputRegisters.contains(objectName))
    {
        bool ok = true;
        const quint16 id = quint16(QObject::sender()->property("ID").toUInt());

        if (objectName.startsWith(QStringLiteral("inReg")))
        {
            if(!modbusDevice->setData(QModbusDataUnit::InputRegisters, id, value.toUShort(&ok, 16)))
            {
                statusBar()->showMessage(tr("Could not set register: ") + modbusDevice->errorString(),5000);
            }
        }
    }
}

void MainWindow::setHoldingRegister(const QString &value)
{
    if(!modbusDevice)
    {
        return;
    }

    const QString objectName = QObject::sender()->objectName();

    if(holdingRegisters.contains(objectName))
    {
        bool ok = true;
        const quint16 id = quint16(QObject::sender()->property("ID").toUInt());
        qDebug()<<id;
        if (objectName.startsWith(QStringLiteral("holdReg")))
        {
            if(!modbusDevice->setData(QModbusDataUnit::HoldingRegisters, id, value.toUShort(&ok, 16)))
            {
                statusBar()->showMessage(tr("Could not set register: ") + modbusDevice->errorString(),5000);
            }
        }
    }
}

  经过上面的处理之后,到我们在界面上操作时就会更新到Modbus RTU从站对应的数据变量。

3.3、对主站设置的响应

  当后台数据被修改时,需要修改显示数据。如主站修改了Coil或者HoldingRegister数据时,需要修改显示信息。而Modbus从站对象有3个信号,分别是:

void errorOccurred(QModbusDevice::Error error)
void stateChanged(QModbusDevice::State state)
void dataWritten(QModbusDataUnit::RegisterType table, int address, int size)

  当主站访问从站时,则会在不同状态下触发相应信号函数,我们需要实现这三个信号函数对应的槽函数就可以实现相应的操作。具体实现如下:

//根据从站的变化更新数据显示
void MainWindow::UpdateDisplayBySlave(QModbusDataUnit::RegisterType table, int address, int size)
{
    qDebug()<<"table:"<<table<<"address:"<<address<<"size:"<<size<<endl;

    for (int i = 0; i < size; ++i)
    {
        quint16 value;
        QString text;
        switch (table)
        {
        case QModbusDataUnit::Coils:
        {
            modbusDevice->data(QModbusDataUnit::Coils, quint16(address + i), &value);
            coilButtons.button(address + i)->setChecked(value);
            break;
        }
        case QModbusDataUnit::HoldingRegisters:
        {
            modbusDevice->data(QModbusDataUnit::HoldingRegisters, quint16(address + i), &value);
            qDebug()<<value;
            holdingRegisters.value(QStringLiteral("holdReg%1").arg(address + i))->setText(text.setNum(value, 16));
            break;
        }
        default:
            break;
        }
    }
}

//设备错误处理
void MainWindow::HandleDeviceError(QModbusDevice::Error newError)
{
    if (newError == QModbusDevice::NoError || !modbusDevice)
        return;

    statusBar()->showMessage(modbusDevice->errorString(), 5000);
}

//从站状态改变响应
void MainWindow::SlaveStateChanged(int state)
{
    bool connected = (state != QModbusDevice::UnconnectedState);
    ui->pushButtonConnect->setEnabled(!connected);
    ui->pushButtonDisconnect->setEnabled(connected);
}

4、测试验证

  完成了编码调试后,我们来对开发的这一工具进行一些测试。首先我们安装一个虚拟串口软件用以虚拟我们用于测试的串口。我们还需要一个Modbus的主站应用来测试从站,这里我们选择Modscan来实现这一过程。
  我们使用虚拟串口来模拟一对串口COM4和COM5。我们在ModScan中打开4个串口分别对应4中数据变量,数量都是16个,并将串口参数设置为和从站默认的一致,然后将其连接到COM5,如下图所示:

  同时设置好从站的参数,然后将其连接到串口COM4,具体如下图:

  这样从站和主站就连接上了。我们接下来在从站界面上操作,看看主站的数据变化。我们在从站界面上将线圈量的0、2、4、6、8、10、12、14等置位,并将状态量的1、3、5、7、9、11、13、15等置位,并在主站中查看,具体如下:

  上图中我们可以看到主站显示的数据与从站设置的数据是一致的,说明我们的设计结果正确。接下来我们在从站界面上的输入寄存器的值修改0x01~0x10,同时在主站将保持寄存器的第10个修改为0x04,具体如下:

  接下来我们在主站中修改保持寄存器的值,我们将保持寄存器的值修改为一系列不同的值,具体如下:

  我们可以看到从站的显示数据与主站的设置是一致的。到此我们就完成了对Modbus RTU从站的测试,结果与我们设计的一致。

源码:https://gitee.com/ErichMoonan/modbus-slave

欢迎关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1064035.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

山西电力市场日前价格预测【2023-10-07】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-10-07&#xff09;山西电力市场全天平均日前电价为422.87元/MWh。其中&#xff0c;最高日前电价为1500.00元/MWh&#xff0c;预计出现在18: 15-18: 45。最低日前电价为221.56元/MWh&#x…

【JavaEE】_HTTP请求与HTTP响应

目录 1. HTTP协议 2. HTTP请求 2.1 HTTP请求首行 2.2 URL 2.3 HTTP方法 2.3.1 GET请求 2.3.2 POST请求 2.3.3 GET与POST的区别 2.3.4 其他方法 2.4 请求报头header 2.4.1 Host&#xff1a; 2.4.2 Content-Length与Content-Type&#xff1a; 2.4.3 User-Agent&…

JavaScript系列从入门到精通系列第十七篇:JavaScript中的全局作用域

文章目录 前言 1&#xff1a;什么叫作用域 一&#xff1a;全局作用域 1&#xff1a;全局变量的声明 2&#xff1a;变量声明和使用的顺序 3&#xff1a;方法声明和使用的顺序 前言 1&#xff1a;什么叫作用域 可以起作用的范围 function fun(){var a 1; } fun();consol…

练[ZJCTF 2019]NiZhuanSiWei

[ZJCTF 2019]NiZhuanSiWei 文章目录 [ZJCTF 2019]NiZhuanSiWei掌握知识解题思路代码分析1代码分析2 关键paylaod 掌握知识 ​ data伪协议和php伪协议的使用&#xff0c;反序列化&#xff0c;代码审计&#xff0c;文件包含,file_get_contents函数绕过 解题思路 打开题目链接&…

【计算机操作系统慕课版】第二章:进程的控制与描述

注&#xff1a;博主斥巨资买到了2021版本慕课版的pdf 如果需要的话可以来私聊我哦~ 操作系统第二章知识点目录&#xff1a; 一、前言&#xff1a;前驱图与程序执行 1.1前驱图&#xff08;看箭头就行&#xff0c;名字高级&#xff0c;底层简单&#xff09; 1.2程序顺序执行 1…

Umijs介绍

今天我们来看 umijs 我们访问官网 https://umijs.org/ 这是一个可 插拔的企业级 React框架 当然 你也可以选择 React 的一个脚手架 但是 这样就有很多需要考虑的东西 用这个umi 很多点 我们就不需要考虑了 框架已经帮我们配置好了 这边 我们点击快速上手的一个 指南 我们可…

vue-img-cutter 实现图片裁剪[vue 组件库]

借助 vue-img-cutter 可以在网页端实现图片裁剪功能&#xff0c;最终功能效果如下&#xff1a; 组件 npm 安装 npm install vue-img-cutter2 --save-dev # for vue2 npm install vue-img-cutter3 --save-dev # for vue3vue-img-cutter使用 template模板标签模块&#xff0c…

用Python操作MySQL教程!干货满满!

python操作数据库介绍 Python 标准数据库接口为 Python DB-API&#xff0c;Python DB-API为开发人员提供了数据库应用编程接口。Python 数据库接口支持非常多的数据库&#xff0c;你可以选择适合你项目的数据库&#xff1a; GadFly mSQL MySQL PostgreSQL Microsoft SQL S…

Spring基础以及核心概念(IoC和DIQ)

1.Spring是什么 Spring是包含了众多工具方法的IoC容器 2.loC&#xff08;Inversion of Control &#xff09;是什么 IoC:控制反转,Spring是一个控制反转容器(控制反转对象的生命周期) Spring是一个loC容器&#xff0c;我们之前学过的List/Map就是数据存储的容器&#xff0c;to…

大数据Doris(五):开始编译 Doris

文章目录 开始编译 Doris 一、下载Doris的安装包 二、解压缩 三、上传配置文件

linux常见命令以及jdk,tomcat环境搭建

目录 Is pwd cd touch cat echo vim 复制粘贴 mkdir rm cp jdk部署 1. yum list | grep jdk进行查找​编辑 2.安装​编辑 3.再次确认 4.判断是否安装成功 tomcat安装 1.下载压缩包&#xff0c;把压缩包上传至linux(可能需要yum install lrzsz) 2.解压缩unzip 压缩包名&…

❓“想创作音乐,但不会编曲?”FL Studio 21 轻松帮你编曲

❓“想创作音乐&#xff0c;但不会编曲&#xff1f;” ❓“不知道如何将各种音乐元素组合起来&#xff1f;” 5个步骤&#xff0c;轻松编曲&#xff01; 想要成为音乐创作高手&#xff0c;编曲是必不可少的技能。今天为大家带来5个编曲的步骤&#xff0c;让你轻松掌握编曲技巧…

[论文工具] LaTeX论文撰写常见用法及实战技巧归纳(持续更新)

祝大家中秋国庆双节快乐&#xff01; 回过头来&#xff0c;我们在编程过程中&#xff0c;经常会遇到各种各样的问题。然而&#xff0c;很多问题都无法解决&#xff0c;网上夹杂着各种冗余的回答&#xff0c;也缺乏系统的实战技巧归纳。为更好地从事科学研究和编程学习&#xff…

GEE错误——Line 2: ee.Image(...).filterBounds is not a function

错误&#xff1a; 我正在尝试通过应用过滤器绑定和过滤器日期来提取多个区域的平均碳含量。我得到的错误是&#xff1a;filterbound 不是一个函数。 我认为问题在于我使用的是 ee.Image 而不是 ee.ImageCollection。我知道如何解决这个问题吗&#xff1f;谢谢 这里的代码&am…

字符串和内存函数

目录 strlen 模拟实现 长度不受限字符串函数 strcpy 模拟实现 ​编辑 strcat 模拟实现 strcmp 模拟实现 长度受限字符串函数 strncpy 模拟实现 strncat strncmp strstr 模拟实现 strtok strerror perror 字符分类函数 字符转换 示例&#xff1a; ​编辑内…

10.4 认识Capstone反汇编引擎

Capstone 是一款开源的反汇编框架&#xff0c;目前该引擎支持的CPU架构包括x86、x64、ARM、MIPS、POWERPC、SPARC等&#xff0c;Capstone 的特点是快速、轻量级、易于使用&#xff0c;它可以良好地处理各种类型的指令&#xff0c;支持将指令转换成AT&T汇编语法或Intel汇编语…

C++:模板进阶与继承

模板进阶与继承 模板进阶1.非类型的模板参数2.模板的特化2.1特化的概念2.2函数模板特化2.3类模板特化2.4全特化和偏特化2.4.1全特化2.4.2偏特化 3.模板的分离编译3.1同文件分离3.2不同文件下分离 继承1.继承的概念和定义1.1继承的概念1.2继承的定义1.2.1定义格式1.2.2继承关系和…

哈希应用之位图

文章目录 1.位图概念2.面试题引入3.代码解决[配注释]4.位图应用4.1找到100亿个整数里只出现一次的整数4.2找两个分别有100亿个整数的文件的交集[只有1G内存]1.法一[使用于数据量<42亿]2.法二[适用于数据量大>42亿]3.在一个有100亿个int的文件中找到出现次数不超过2次的所…

自动驾驶技术的基础知识

自动驾驶技术是现代汽车工业中的一项革命性发展&#xff0c;它正在改变着我们对交通和出行的理解。本文将介绍自动驾驶技术的基础知识&#xff0c;包括其概念、历史发展、分类以及关键技术要素。 1. 自动驾驶概念 自动驾驶是一种先进的交通技术&#xff0c;它允许汽车在没有人…

字符集、编码格式的理解

计算机中只能存储二进制01&#xff0c; 要想存储字符&#xff0c;就要有一个字符与编码的映射关系&#xff0c;这个关系就是字符集。 字符集就是字符与编码的映射关系* 字符集的发展历程&#xff1a; 因为计算机是欧美先发明的&#xff0c;他们的语言就26个字母&#xff0c;所…