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

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

1、概述

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

(1)、串口参数的配置

  Modbus RTU通过串口来实现通讯,所以我们需要对串口相关的参数进行配置。对串口的配置主要是串口名、波特率、校验位、数据位和停止位等。对于这些参数我们让使用者可以根据需要选择。
  而串口号,我们希望软件可以自动搜索当前可用的串口列表。而且我们可以通过操作更新可用的串口列表。对串口的操作主要是串口的打开与关闭。

(2)、从站信息的配置

  我们实现Modbus RTU主站应用就是访问从站的数据,所以我们需要在主站应用中配置从站的信息。主要有站地址、数据类型、数据格式等,我们将其设置为可以选择。
读取从站的参数配置,主要是起始地址、读取的数量。写从站参数的配置,主要是起始地址、写入的数量以及写入的数值。

(3)、对从站的操作

  Modbus RTU主站对从站的操作无非是读从站数据和写从站数据,我们通过制定读写的寄存器类型、起始地址、数量等通过按钮操作来实现读写命令的发送。
  除了手动操作读写外,很多时候我们可能需要Modbus RTU主站自动周期性的读取从站的数据。所以我们让其可以选择以多长的周期自动循环读取。

(4)、对信息的显示

  接收信息的显示,作为一款工具软件, 我们当然希望看到我们发给从站的命令究竟有没有成功,最简单的和直观的办法就是将接收到的信息显示出来。对于Modbus RTU主站当然是显示对应的地址的值。
  同样的,我们有时候想要看到发送和接收到的原始报文,所以我们对发送和接收到的报文也作相应的显示。
  对于个别数据有时候我们还希望看到他的变化趋势,所以我们可以添加一个图形显示,用以显示我们制定的数据的变化趋势。
  运行状态的显示, 我们希望对操作的状态进行反馈以指示操作的动作是否执行,所以我们需要状态栏来实现这一需求。

2、界面设计

  根据上一节中分析的需求,我们先来设计软件的界面。我们在QT中基于QMainWindow类生成一个操作界面,包括菜单栏、工具栏和状态栏以满足需求中对状态显示及操作命令的要求。
  而在中间显示区域,我们将其划分为2列。在左边的一列从上到下设置:串口配置操作区域和读写从站的交互配置区域。在右侧的一列从上到下设置:动态曲线显示区域、收发消息显示区域以及直接输入报文发送命令的输入区域。具体的界面设置如下图所示:

  完成如上图的布局后,我们可以选择在属性中配置控件的参数,也可以在代码中添加相关的参数。在这里在代码中通过初始化形式完成参数的设置。完成整个布局后我们先试着运行程序,正常运行则出现如下的界面:

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

3、编码实现

  接下来这一小节,我们将来编码实现相应的功能。我们主要将功能分为串口操作功能、从站操作功能以及信息显示功能三个部分来实现。

3.1、串口操作功能

  对串口的操作首先就是对串口参数的设置。我们在代码中对界面上的串口号、波特率、数据位、校验位和停止位的ComboBox控件进行初始化。其中串口号通过自动搜索当前可用的串口来实现。具体的实现方式如下:

//搜索串口
void MainWindow::SearchSerialPorts()
{ui->comboBoxPort->clear();foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts()){ui->comboBoxPort->addItem(info.portName());}
}

  对串口的操作主要是串口的打开和关闭,在这里因为是Modbus RTU主站应用,我们称之为连接和断开。建立或断开与从站的连接实际就是对串口的配置与操作,只是针对Modbus RTU作了一些封装,具体实现如下:

//串口连接
void MainWindow::on_actionConnect_triggered()
{if (!modbusDevice)return;modbusDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,ui->comboBoxPort->currentText());modbusDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,ui->comboBoxBaud->currentText().toInt());switch(ui->comboBoxParity->currentIndex())                   //设置奇偶校验{case 0: modbusDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,QSerialPort::NoParity);break;default: break;}switch(ui->comboBoxData->currentIndex())                   //设置数据位数{case 1:modbusDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,QSerialPort::Data8);break;default: break;}switch(ui->comboBoxStop->currentIndex())                     //设置停止位{case 1: modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::OneStop);break;case 2: modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::TwoStop);break;default: break;}modbusDevice->setTimeout(1000);modbusDevice->setNumberOfRetries(3);if (modbusDevice->connectDevice()){//开启自动读取if(ui->checkBoxAuto->isChecked()){connect(pollTimer,&QTimer::timeout, this, &MainWindow::ReadRequest);pollTimer->setInterval(ui->spinBoxInterval->value());pollTimer->start();}//连接槽函数//QObject::connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::ReadSerialData);// 设置控件可否使用ui->actionConnect->setEnabled(false);ui->actionDisconnect->setEnabled(true);ui->actionRefresh->setEnabled(false);}else    //打开失败提示{QMessageBox::information(this,tr("错误"),tr("连接从站失败!"),QMessageBox::Ok);}
}

3.2、从站操作功能

  在前面一节中我们已经设计过,对从站的操作包括手动按钮读取从站数据、手动按钮写入从站数据以及自动周期读取从站数据。
  手动读取从站数据是指点击按钮时触发一次读从站的操作,而从站的地址、读取的寄存器类型、读取的寄存器起始地址和寄存器的数量均根据界面上相应的设置确定。具体的实现如下:

//读数据请求
void MainWindow::ReadRequest()
{if (!modbusDevice){QMessageBox::information(NULL,  "Title",  "尚未连接从站设备");return;}QModbusDataUnit::RegisterType type;switch(ui->comboBoxDataType->currentIndex()){case 0:type=QModbusDataUnit::Coils;break;case 1:type=QModbusDataUnit::DiscreteInputs;break;case 2:type=QModbusDataUnit::InputRegisters;break;case 3:type=QModbusDataUnit::HoldingRegisters;break;default:type=QModbusDataUnit::Invalid;}int startAddress = ui->spinBoxStartRead->value();Q_ASSERT(startAddress >= 0 && startAddress < 10);// do not go beyond 10 entriesquint16 numberOfEntries = qMin(quint16(ui->spinBoxNumberRead->value()), quint16(10 - startAddress));QModbusDataUnit readUnit=QModbusDataUnit(type, startAddress, numberOfEntries);statusBar()->clearMessage();if (auto *reply = modbusDevice->sendReadRequest(readUnit, ui->spinBoxStation->value())){if (!reply->isFinished())connect(reply, &QModbusReply::finished, this, &MainWindow::ReadSerialData);elsedelete reply; // broadcast replies return immediately}else{statusBar()->showMessage(tr("Read error: ") + modbusDevice->errorString(), 5000);}}

  手动写从站操作是指点击按钮触发一次写从站操作,而从站的地址、写入的寄存器类型、写入的寄存器起始地址、写入的寄存器的数量以及写入的值均根据界面上相应的设置确定。而寄存器的值得输入以“,”分割,具体的实现如下:

//写数据请求
void MainWindow::WriteRequest(QList<quint16> values)
{if (!modbusDevice){QMessageBox::information(NULL,  "Title",  "尚未连接从站设备");return;}QModbusDataUnit::RegisterType type;switch(ui->comboBoxDataType->currentIndex()){case 0:type=QModbusDataUnit::Coils;break;case 1:type=QModbusDataUnit::DiscreteInputs;break;case 2:type=QModbusDataUnit::InputRegisters;break;case 3:type=QModbusDataUnit::HoldingRegisters;break;default:type=QModbusDataUnit::Invalid;}int startAddress = ui->spinBoxStartWrite->value();Q_ASSERT(startAddress >= 0 && startAddress < 10);QModbusDataUnit writeUnit = QModbusDataUnit(type,startAddress, values.size());for(int i=0; i<values.size(); i++){writeUnit.setValue(i, values.at(i));}//serverEdit 发生给slave的IDif (auto *reply = modbusDevice->sendWriteRequest(writeUnit,ui->spinBoxStation->value())){if (!reply->isFinished()){connect(reply, &QModbusReply::finished, this, [this, reply]() {if (reply->error() == QModbusDevice::ProtocolError) {qDebug() << QString("Write response error: %1 (Mobus exception: 0x%2)").arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);} else if (reply->error() != QModbusDevice::NoError) {qDebug() << QString("Write response error: %1 (code: 0x%2)").arg(reply->errorString()).arg(reply->error(), -1, 16);}reply->deleteLater();});}else{reply->deleteLater();}}else{qDebug() << QString(("Write error: ") + modbusDevice->errorString());}
}

  对于自动周期性读取从站数据我们通过一个计时器周期性操作,而从站的地址、读取的寄存器类型、读取的寄存器起始地址、寄存器的数量以及间隔时间通过界面设置。而其操作与手动按钮触发一样。

3.3、信息显示功能

  对于信息的显示我们主要考虑3个方面的内容。一是读取回来的从站数据结果显示;二是上下行报文的监视;三是操作过程及状态的显示。
  首先是对读取回来的从站数据进行显示,在这里我们将读取的寄存器地址及其对应的数据显示在消息框中。同时我们将部分数据在图形显示中以曲线的形式展示出来。

//曲线显示
void MainWindow::ChartDisplay()
{QColor acolor[8]={Qt::red,Qt::blue,Qt::green,Qt::cyan,Qt::yellow,Qt::magenta,Qt::black,Qt::darkRed};QStringList name={"抛物线","正弦值","正弦值","固定值","固定值","固定值","固定值","固定值"};QVector<QPointF> list[8];QVector<QPointF> newlist[8];for(int j=0;j<8;j++){list[j] = lineSeries[j]->pointsVector();//获取现在图中列表if (list[j].size() < 200){//保持原来newlist[j] = list[j];}else{//错位移动for(int i =1 ; i< list[j].size();i++){newlist[j].append(QPointF(i-1,list[j].at(i).y()));}}newlist[j].append(QPointF(newlist[j].size(),values[j]));//最后补上新的数据lineSeries[j]->replace(newlist[j]);//替换更新lineSeries[j]->setName(name[j]);//设置曲线名称lineSeries[j]->setPen(acolor[j]);//设置曲线颜色lineSeries[j]->setUseOpenGL(true);//openGl 加速//mChart->setTitle("Pressure Data");//设置图标标题mChart->removeSeries(lineSeries[j]);mChart->addSeries(lineSeries[j]);mChart->createDefaultAxes();//设置坐标轴}ui->graphicsView->setChart(mChart);
}

  其次对于上下行报文我们也将其显示到消息显示框中。在QT对Modbus协议进行封装后,我们没有办法直接获取上下行的报文,我们可以开启日志答应功能,再从其中截取相应的报文。

QLoggingCategory::setFilterRules(QStringLiteral("qt.modbus* = true"));

  而操作过程及状态显示则比较简单,我们在状态栏显示相应的操作过程和操作的状态。

4、小结

  完成了编码调试后,我们尚需要对这一工具进行一些测试。首先我们安装一个虚拟串口软件用以虚拟我们用于测试的串口,并找到一款Modbus RTU的从站模拟软件。当然有实际的从站和硬件的串行端口更好,在这里我们先用软件模拟。具体的配置如下图所示:

  而Modbus RTU从站我们使用MThings来模拟,当然也可以使用其它Modbus RTU从站模拟软件。我们模拟10个保持寄存器和10个线圈,之所以这么设置是因为这两种数据类型支持读写,方便我们测试。具体的配置如下图所示:

  现在将我们设计的Modbus RTU主站运行起来,并使用它去访问我们刚才配置的Modbus RTU从站。首先我们实验读从站数据操作。测试的结果如下图所示:

  这里我们读取从站从地址0开始的10个保持寄存器,并将值显示在消息框和图形中。我们模拟了2路正弦信号、1路抛物线信号和5路固定值信号。接下来我们测试一下写操作。测试的结果如下图所示:

  这里对从站的从地址3开始的3个保持寄存器的值进行修改。设定的值分别是123、456和789,操作完成后我们查看从站的结果如下:

  上图中与我们设定值的完全符合,说明我们的写从站操作时正确的。到这里我们基于QT的Modbus RTU主站就基本实现了。当然,我们还可以根据需要修改或添加一些功能以适应不同的应用需求。我们已经将代码发布到Gitee,欢迎下载和交流。
下载地址:https://gitee.com/ErichMoonan/ModbusMaster

欢迎关注:

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

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

相关文章

外设驱动库开发笔记45:MS4515DO压力传感器驱动

很多时候我们需要检测流量和压力这些参数&#xff0c;比如我们要检测大气压&#xff0c;或者通过测量差压来获得输送流体的流量等&#xff0c;都需要用到压力传感器。这一篇我们就来讨论MS4515DO压力传感器的数据获取。 1、功能概述 MS4515DO是TE公司推出的一款基于PCB安装的小…

外设驱动库开发笔记48:MCP4725单通道DAC驱动

在产品设计过程中&#xff0c;我们经常会遇到数模转换的应用需求。在本篇种我们就来讨论一下MCP4725单通道数模转换器的驱动设计与实现。 1、功能概述 MCP4725是一个低功耗&#xff0c;高精度&#xff0c;单通道&#xff0c;12位缓冲电压输出数字到模拟转换器(DAC)与非易失性存…

如何确保不使用动态内存

在许多嵌入式应用程序中&#xff0c;内存分配必须是静态的&#xff0c;而不是动态的。意味着在应用程序中不应使用对malloc()或free()等内容的调用&#xff0c;因为它们可能会在运行时失败&#xff08;内存不足、堆碎片&#xff09;。 但是&#xff0c;当与第三方库甚至 C/C 标…

go 单元测试 testing 打印输出_2020,你需掌握go 单元测试进阶篇

本文说明go语言自带的测试框架未提供或者未方便地提供的测试方案&#xff0c;主要是用于解决写单元测试中比较头痛的依赖问题。也就是伪造模式&#xff0c;经典的伪造模式有桩对象(stub),模拟对象(mock)和伪对象(fake)。比较幸运的是&#xff0c;社区有丰富的第三方测试框架支持…

一文读懂Git工作流

Git是目前最流行的代码管理工具&#xff0c;相信大家也都是在用Git来管理自己团队的源代码。 团队一般为了规范开发&#xff0c;保持良好的代码提交记录以及维护 Git 分支结构清晰&#xff0c;方便后续维护等&#xff0c;都会迫切需要一个比较规范的 Git 工作流。 本文就是在…

xbox360fsd更新游戏封面_游戏类短视频创作指南

一&#xff0e;起步阶段1.内容发布垂直&#xff0c;整体风格一致&#xff0c;选定一个品类的游戏内容风格持续更新注意&#xff1a;冷启动时期不要频繁更换游戏类型2.账号IP化 根据自身风格特色打造独特的风格账号。有利延长账号生命周期&#xff0c;提升粉丝转化率。搞笑、中二…

开发者们都在关注的网站

开发者们都在关注的网站 &#x1f609; 综合类&#xff08;5个&#xff09; 1、GitHub 全球最大的编程开源社区&#xff0c;很多优秀的开源项目都在上边&#xff0c;不知道这个都不要说自己是程序员&#x1f602; 访问地址&#xff1a;https://github.com 2、CSDN 全球最大中…

ios framework 调用第三方 framework_Python基础:标准库和常用的第三方库

Python的标准库有&#xff1a;名称作用datetime为日期和时间处理同时提供了简单和复杂的方法。zlib直接支持通用的数据打包和压缩格式&#xff1a;zlib&#xff0c;gzip&#xff0c;bz2&#xff0c;zipfile&#xff0c;以及 tarfile。random提供了生成随机数的工具。math为浮点…

作图神器ProcessOn - 免费好用

因工作需要&#xff0c;我经常需要花一些流程图&#xff0c;时序图&#xff0c;架构图什么的&#xff0c;之前使用的Windows系统&#xff0c;大部分情况下就用的Visio来画图。后来为了工作方便&#xff0c;换成了Mac电脑&#xff0c;结果发现Mac上没有Visio&#xff0c;然后就在…

三电平igbt死区时间计算_基于大功率三电平IGBT模块并联的参考设计

当前的可再生能源行业中&#xff0c;光伏和风力发电均面临着补贴逐步退坡&#xff0c;平价上网时代即将到来的挑战。为应对这一挑战&#xff0c;光伏逆变器和风力变流器厂家研发的新品单机功率越来越高&#xff0c;以取得更低的单位功率成本。市场上1.5MW的集中式光伏逆变器和3…

手把手教你搭建开发环境之Java开发

大家好呀&#xff0c;从今天开始&#xff0c;我们的手把手系列教程就正式开始啦。 如果你觉得本文对你有一些帮助&#xff0c;欢迎大家关注、点赞、分享给需要的小伙伴们&#xff0c;谢谢大家啦。 前言 Java虽然是一个比较老的语言&#xff0c;但到现在依然充满了活力&#x…

opc服务器组态文件已写保护_远程组态软件不仅方便了PLC无线远程监控,也大大降低了工程成本...

远程组态软件不仅方便了PLC无线远程监控&#xff0c;也大大降低了工程成本组态软件远程监控1.本地上位SCADA系统采集分布各地现场PLC等设备运行的数据&#xff0c;并可以下发控制指令&#xff1b;2.提供稳定的OPC接口服务&#xff0c;常年稳定运行&#xff0c;规模可达10万数据…

奇妙的安全旅行之加密算法概述

前言 hi&#xff0c;大家好呀&#xff0c;信息安全作为当前社会中比较重要的一个课题&#xff0c;已经覆盖了人们生活的方方面面&#xff0c;虽然有时候我们可能并没有意识到&#xff0c;其实信息安全防护已经在背后默默的保护我们的信息安全了。例如&#xff0c;当你在互联网…

怎么调节电机启动值_开关式智能充电机-全自动充电机-铅酸电池充电机品牌-济南能华...

开关式智能充电机-全自动充电机-铅酸电池充电机品牌-济南能华NHCD系列 全自动智能充电机&#xff0c;可调智能充电机&#xff0c;可调直流充电机&#xff0c;可调全自动充电机 &#xff0c;可调蓄电池充电机 便携式可调智能充电机 便携式全自动充电机 大功率可调充电机 大功率智…

奇妙的安全旅行之MD算法

hi&#xff0c;大家好&#xff0c;今天我们开始介绍消息摘要算法中的MD&#xff08;Message Digest&#xff09;算法&#xff0c;MD算法家族包括&#xff1a;MD2&#xff0c;MD4&#xff0c;MD5&#xff0c;MD算法生成的消息摘长度要都是128位的。 其中MD5算法是消息摘要算法的…

的图层类型有哪些_东莞都市领航平面设计培训班都学习哪些内容?

平面设计的工作稳定性是很高的&#xff0c;经济繁荣时期毫无疑问&#xff0c;即使经济下滑&#xff0c;仍不会有很大影响&#xff0c;以前两年为例&#xff0c;北美的大规模裁员浪潮&#xff0c;给高科技行业带来巨大冲击&#xff0c;放慢了高科技产品的开发速度&#xff0c;当…

dockerfile拉取私库镜像_还在用Alpine作为你Docker的Python开发基础镜像?其实Ubuntu更好一点...

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_173一般情况下&#xff0c;当你想为你的Python开发环境选择一个基础镜像时&#xff0c;大多数人都会选择Alpine&#xff0c;为什么&#xff1f;因为它太小了&#xff0c;仅仅只有 5 MB 左右&#xff08;对比 Ubuntu 系列镜像接…

2020,再见;2021,我来了!

现在是2021年1月16日下午16点33分&#xff0c;星期六。此时北京正在通报昨日新冠肺炎新增病例情况&#xff0c;这种每天戴口罩的鬼日子还不知道什么时候能结束。最近由于天气变冷&#xff0c;病毒更容易存活和传播&#xff0c;最近一个月就突然又变的非常紧张起来了&#xff0c…

cuda tensorflow版本对应_Windows10下安装tensorflow-gpu(2.2.0)安装教程(避坑+保姆式教学)...

本文实现了Windows10下GPU版本的tensorflow2.2.0的安装&#xff0c;用到的软件主要包括&#xff1a;CUDA 10.2 cuDNN Anaconda tensorflow-gpu 2.2.0。&#xff08;注&#xff1a;此教程在Win7环境下也同样适用&#xff01;另附报错缺少cudart64_101.dll的解决办法&#xff…

奇妙的安全旅行之DES算法(二)

hi&#xff0c;大家好&#xff0c;上一节我们详细介绍了对称加密算法DES的基本内容&#xff0c;由于明文的长度不固定&#xff0c;而加密算法只能处理特定长度的一块数据&#xff0c;所以就需要对比较长的明文进行分组后再加密&#xff0c;但是分组后&#xff0c;最后一组的长度…