QT5.14串口调试助手:上位机接收数据解析数据帧+多通道波形显示+数据保存为csv文件

Posted 絮沫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了QT5.14串口调试助手:上位机接收数据解析数据帧+多通道波形显示+数据保存为csv文件相关的知识,希望对你有一定的参考价值。

由于业务需要,在上个月做了一个关于qt的设计,在设计中主要需要解决的问题就是接收单片机采集到的数据并在上位机将数字实时的通过波形显示出来,然后上位机要有保存下数据文件的功能,便于后续的软件读取数据做进一步的分析处理

QT第一步:安装软件环境

安装qt5.14,可以在这个网站下载安装包。
下载版本: qt-opensource-windows-x86-5.14.2.exe

安装时需要勾选MinGW 相关选项

安装教程不在重复赘述,网上有很多的例子

第二步:初始QT

qt作为一种开源的UI程序设计框架可以便捷的通过qt提供的各种组件以低代码的方式组件自己需要的ui界面,这对于初步入门的设计人员十分的友好,同时qt官方对每个类、方法、变量的文档说明都非常详细并且提供了实例代码入门非常简单。
安装好qt后直接使用qt官方的Qt Creator程序进行开发,当然你可以使用MSVS进行开发,这还需要在MSVC中安装一下qt的官方插件。我使用的是VS2019+qt5.14.2,在UI设计界面上VS和qt还存在一定的兼容性问题,有好多次出现闪退的问题。所以工程不是特别大的时候还是建议老老实实就用Qt Creator进行开发。

安装好后可以看到qt提供很多的模板程序,当然也可以都不使用,直接从空白模板开始我们的工程

第三步:了解信号与槽机制

Qt利用信号与槽(signals/slots)机制取代传统的callback来进行对象之间的沟通。当操作事件发生的时候,对象会发提交一个信号(signal);而槽(slot)则是一个函数接受特定信号并且执行槽本身设置的动作。信号与槽之间,则透过QObject的静态方法connect来链接。
信号在任何执行点上皆可发射,甚至可以在槽里再发射另一个信号,信号与槽的链接不限定为一对一的链接,一个信号可以链接到多个槽或多个信号链接到同一个槽,甚至信号也可连接到信号。
以往的callback缺乏类型安全,在调用处理函数时,无法确定是传递正确类型的参数。但信号和其接受的槽之间传递的资料类型必须要相符合,否则编译器会提出警告。信号和槽可接受任何数量、任何类型的参数,所以信号与槽机制是完全类型安全。

信号与槽机制也确保了低耦合性,发送信号的类别并不知道是哪个槽会接受,也就是说一个信号可以调用所有可用的槽。此机制会确保当在"连接"信号和槽时,槽会接受信号的参数并且正确执行。

上面的解释来自维基百科,说的简单点呢就是说:当你在qt界面中放置了一个按钮,当你运行程序并移动鼠标点击这个按钮的时候。点击按钮这个动作就是一个信号,当然有了信号我们就要执行命令,我们通过软件定义将这个信号连接上一个槽,这个槽函数执行点击动作所需要的对行功能。信号与槽可以是一一对应也可以是一对多、多对一。
在这里我们可以看到:

QObject::connect(ui->button, SIGNAL(clicked()), this, SLOT(senddata()));

上面这个函数就是一个槽函数,他把button按钮的点击信号链接到了发送数据的响应函数上,只要鼠标点击一次就会发送一次。


在qt中每一个对象都有对应的属性,这些属性的值就对应了这个对象的大小,相对位置,名称等等。

在使用qt时也可以选择pyqt,其中也同样拥有界面设计的功能,但以下的程序默认针对c++版本的qt

开始串口助手

这里默认你已经了解了基本的qt操作和c++语法
首先我们需要一个串口类,用来发送和接收数据;
然后我们需要 两个文本框和几个按钮来实现数据的接收和发送,并且设置串口通信的参数;
实现之后我们就需要设置针对串口数据的解析了:

定时接收串口数据

由于对电脑端接收数据很难做到硬件级的收中断,收到1bit数据就中断处理一次所以我们设置一个定时器,让程序检测当有数据来时就打开定时器开始定时,定时一段时间后关闭中断并接收保存这段时间内的所有数据。
这一段时间根据串口发送一帧数据的时间做合理设置,一般来时这个时候收到的数据里包含着好几帧完整的数据,整段数据的头和尾可能并不是我们设置的帧头和帧尾,所以我们需要从中解析出需要的数据:
这里我使用了关键字索引,从接收到的字符串中查找到第一个我设置的帧头数据,然后从这个位置开始向后继续检索第一个我设置的帧尾,如果帧长度满足要求则把这一段字符串从中提取出来做单独处理:

//串口接收数据帧格式为:帧头'*' 帧尾'#' 数字间间隔符号',' 符号全为英文格式
void Widget::Read_Date()

    int bufferlens = 0;     //帧长
    QString str = ui->Receive_text_window->toPlainText();
    timerserial->stop();//停止定时器,

    qDebug()<<buffer;

    QByteArray bufferbegin = "*";   //定义帧头
    int index=0;
    QByteArray bufferend = "#";     //定义帧尾
    int indexend = 0;
    QByteArray buffercashe;         //缓存数据

    index = buffer.indexOf(bufferbegin,index);  //查找帧头
    indexend = buffer.indexOf(bufferend,indexend);  //查找帧尾
    if((index<buffer.size())&&(indexend<buffer.size()))
    
        bufferlens = indexend - index + 1;
        buffercashe = buffer.mid(index,bufferlens);
    

    char recvdata[buffercashe.size()];
    memset(recvdata,0,sizeof(recvdata));
    memcpy(recvdata,buffercashe.data(),bufferlens-1);
    recvdata[buffercashe.size()-1]=35;      //# 的ascii是35
    //qDebug()<<"cash size: "<<buffercashe.size();
    //std::cout<<"recvdata size: "<< sizeof (recvdata)<<std::endl;
    //std::cout<<"recvdata : " <<recvdata<<std::endl;
    if(recvdata[0]=='*'&&recvdata[buffercashe.size()-1]=='#')   //帧检查
    
        str_to_num(recvdata);       //更新数据并缓存到保存区
        str+="succeed:";
        str+=tr(buffercashe);
        str += "  ";
        ui->Receive_text_window->clear();
        ui->Receive_text_window->append(str);
    
    else
    
        str+="error! ";
        str+=tr(buffercashe);
        str += "  ";
        ui->Receive_text_window->clear();
        ui->Receive_text_window->append(str);
    
    buffer.clear();


void Widget::serial_timerstart()

    timerserial->start(4);
    buffer.append(serialport->readAll());

在上面的程序中,当串口发现有数据进来则引用Widget::serial_timerstart()开始定时接收。当定时到之后开始处理接收到的数据。如果数据正常后则把数据保存到缓冲区并更新当前的波形数据。

波形显示

这里显示模型数据使用了很简单的QChart()实例,定义QSplineSeries()对象,然后不断的更新QSplineSeries对象的数据列表,做好坐标轴的处理后就可以实现出动态曲线的效果了。

//曲线设置初始化
void Widget::Chart_Init()

    //初始化QChart的实例
    chart = new QChart();
    //初始化QSplineSeries的实例
    lineSeries = new QSplineSeries();
    //设置曲线的名称
    lineSeries->setName("曲线1");
    //把曲线添加到QChart的实例chart中
    chart->addSeries(lineSeries);

    //声明并初始化X轴、两个Y轴
    QValueAxis *axisX = new QValueAxis();
    QValueAxis *axisY = new QValueAxis();
    //设置坐标轴显示的范围
    axisX->setMin(0);
    axisX->setMax(MAX_X);
    axisY->setMin(0);
    axisY->setMax(MAX_Y);
    //设置坐标轴上的格点
    axisX->setTickCount(10);
    axisY->setTickCount(10);
    //设置坐标轴显示的名称
    QFont font("Microsoft YaHei",8,QFont::Normal);//微软雅黑。字体大小8
    axisX->setTitleFont(font);
    axisY->setTitleFont(font);
    axisX->setTitleText("X-时间");
    axisY->setTitleText("Y-角度");
    //设置网格不显示
    axisY->setGridLineVisible(false);
    //下方:Qt::AlignBottom,左边:Qt::AlignLeft
    //右边:Qt::AlignRight,上方:Qt::AlignTop
    chart->addAxis(axisX, Qt::AlignBottom);
    chart->addAxis(axisY, Qt::AlignLeft);
    //把曲线关联到坐标轴
    lineSeries->attachAxis(axisX);
    lineSeries->attachAxis(axisY);
    //把chart显示到窗口上
    ui->graphicsView->setChart(chart);
    ui->graphicsView->setRenderHint(QPainter::Antialiasing);      // 设置渲染:抗锯齿,如果不设置那么曲线就显得不平滑


//更新曲线函数
void Widget::DrawLine()

    if(count > MAX_X)
    
        //当曲线上最早的点超出X轴的范围时,剔除最早的点,
        lineSeries->removePoints(0,lineSeries->count() - MAX_X);
        // 更新X轴的范围
        chart->axisX()->setMin(count - MAX_X);
        chart->axisX()->setMax(count);
    
    else
        chart->axisX()->setMin(0);
        chart->axisX()->setMax(MAX_X);
    
    //增加新的点到曲线末端
    lineSeries->append(count, (int)Data.Sensor_1);
    count ++;


文件保存

首先是说明下csv文件的写入格式:以逗号作为分隔符,\\n作为换行符。

那么就可以先写入一个表头,然后根据格式从缓存好的数据容器中逐条加载后逐行写入并打上时间戳
下面的代码举例我们要保存的数据是五通道的。

/*
    函   数:SaveRecvDataFile
    描   述:保存数据按钮点击槽函数
    输   入:无
    输   出:无
*/
void Widget::SaveRecvDataFile()

    if(m_data.size()<1)
    
        QMessageBox::information(this, "提示","当前数据为空");
        return;
    
    serialport->clear();        //清空缓存区
    serialport->close();        //关闭串口
    timerDrawLine->stop();      //关闭波形刷新
    ui->send_button->setEnabled(false);		//禁用部分按键
    ui->open_port->setEnabled(true);
    ui->close_port->setEnabled(false);
    ui->save_data->setEnabled(false);

    QString csvFile = QFileDialog::getExistingDirectory(this);      //获取文件保存路径
    if(csvFile.isEmpty())
       return;
    QDateTime current_date_time =QDateTime::currentDateTime();      //获取系统时间
    QString current_date =current_date_time.toString("MM_dd_hh_mm");    //获取时间字符串
    csvFile += tr("/%1.csv").arg(current_date);
    qDebug()<< csvFile;
    QFile file(csvFile);
    if ( file.exists())
    
            //如果文件存在执行的操作,此处为空,因为文件不可能存在
    
    file.open( QIODevice::ReadWrite | QIODevice::Text );    //以读写模式读取文件
    QTextStream out(&file);
    out<<tr("Time,")<<tr("sensor1,")<<tr("sensor2,")<<tr("sensor3,")<<tr("sensor4,")<<tr("sensor5,\\n");     //写入表头
    // 创建 CSV 文件
    for (const auto &data : m_data)            //测试格式: *111,222,333,444,555#
        out << data << "\\n";        //顺序将缓冲区数据写入文件
    
    file.close();
    QVector<QString>().swap(m_data);        //清空缓存区数据
    QMessageBox::information(this, "提示","数据保存成功");


好的,那我们基本实现了最初的三个功能,下面附上一张完整效果的演示截图:

完整源码将在项目结束后公开在博客里,有需要的话可以点个关注哦

下次离开还是先见一面吧,即使什么也不说

C#上位机开发一:串口通讯之如何制作一个串口调试助手

大家晚上好,今天我们还是直奔主题,讲一讲如何用C#写一个属于自己喜欢的串口调试助手。

想必作为一个工控人,大家应该都接触过许许多多串口调试的辅助工具,比如:小小串口、大傻等等。今天我把我利用业余时间写的一个串口助手软件“蜜蜂工控串口调试助手”的代码分享给大家看看,大家一起学习看看如何写的。首先上图,本人不是专门做UI的,专业人员请忽略。

下附代码:

using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;

using System.IO.Ports;


namespace SerialPortHelperDemo
{
    public partial class FrmHelper : Form
    {
        //创建串口操作助手对象
        private SerialPortHelper serialPortHelper = new SerialPortHelper();

        #region  系统初始化
        public FrmHelper()
        {
            InitializeComponent();

            //串口基本参数初始化
            this.cboBaudRrate.SelectedIndex = 5;   //波特率默认9600
            this.cboCheckBit.SelectedIndex = 0;      //校验位默认NONE
            this.cboDataBit.SelectedIndex = 2;         //数据位默认8
            this.cboStopBit.SelectedIndex = 0;         //停止位默认1

            //获取当前计算机的端口
            if (this.serialPortHelper.PortNames.Length == 0)
            {
                MessageBox.Show("当前计算机上没有找到可用的端口!", "警告信息");
                this.btnOperatePort.Enabled = false;//禁用打开端口按钮
            }
            else
            {
                //将端口添加到下拉框
                this.cboCOMList.Items.AddRange(this.serialPortHelper.PortNames);
                this.cboCOMList.SelectedIndex = 0;
            }

            //串口对象委托和串口接收数据事件关联
            this.serialPortHelper.SerialPortObject.DataReceived +=
                new SerialDataReceivedEventHandler(this.SerialPort_DataReceived);
        }

        #endregion

        #region 串口参数设置

        //波特率的设置
        private void cboBaudRrate_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.serialPortHelper.SerialPortObject.BaudRate = Convert.ToInt32(this.cboBaudRrate.Text);
        }
        //设置奇偶校验
        private void cboCheckBit_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (cboCheckBit.Text == "EVEN")
                serialPortHelper.SerialPortObject.Parity = System.IO.Ports.Parity.Even;
            else if (cboCheckBit.Text == "NONE")
                serialPortHelper.SerialPortObject.Parity = System.IO.Ports.Parity.None;
            else if (cboCheckBit.Text == "0DD")
                serialPortHelper.SerialPortObject.Parity = System.IO.Ports.Parity.Odd;
        }
        //设置数据位
        private void cboDataBit_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.serialPortHelper.SerialPortObject.DataBits = Convert.ToInt32(cboDataBit.Text);
        }
        //设置停止位
        private void cboStopBit_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (cboStopBit.Text == "1")
                serialPortHelper.SerialPortObject.StopBits = System.IO.Ports.StopBits.One;
            else if (cboStopBit.Text == "2")
                serialPortHelper.SerialPortObject.StopBits = System.IO.Ports.StopBits.Two;
        }

        #endregion

        #region 打开与关闭端口

        private void btnOperatePort_Click(object sender, EventArgs e)
        {
            try
            {
                if (this.btnOperatePort.Text.Trim() == "打开端口")
                {
                    this.serialPortHelper.OpenSerialPort(this.cboCOMList.Text.Trim(), 1);
                    this.lblSerialPortStatus.Text = "端口已打开";
                    this.lblStatusShow.BackColor = Color.Green;
                    this.btnOperatePort.Text = "关闭端口";
                    this.btnOperatePort.Image = this.imageList.Images[0];
                }
                else
                {
                    this.serialPortHelper.OpenSerialPort(this.cboCOMList.Text.Trim(), 0);
                    this.lblSerialPortStatus.Text = "端口未打开";
                    this.lblStatusShow.BackColor = Color.Red;
                    this.btnOperatePort.Text = "打开端口";
                    this.btnOperatePort.Image = this.imageList.Images[1];
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("端口操作异常:" + ex.Message);
            }
        }

        #endregion

        #region 发送数据

        private void btnSend_Click(object sender, EventArgs e)
        {
            if (this.txtSender.Text.Trim().Length == 0)
            {
                MessageBox.Show("发送内容不能为空!", "提示信息");
            }
            else
            {
                //开始发送
                SendData(this.txtSender.Text.Trim());
            }
        }

        //这个方法独立出来,是为了后面扩展自动定时发送数据的时候调用
        private void SendData(string data)
        {
            try
            {
                if (this.ckb16Send.Checked)//发送十六进制数据
                {
                    this.serialPortHelper.SendData(data, SendFormat.Hex);
                }
                else  //发送字符串
                {
                    this.serialPortHelper.SendData(data, SendFormat.String);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("发送数据出现错误:" + ex.Message, "错误提示!");
            }
        }

        #endregion

        #region 串口接收数据的事件

        /// <summary>
        /// 串口接收数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                ReceiveData(this.serialPortHelper.ReceiveData());
            }
            catch (Exception ex)
            {
                MessageBox.Show("接收数据出现错误:" + ex.Message);
            }
        }
        /// <summary>
        /// 接收数据的具体实现过程
        /// </summary>
        /// <param name="byteData"></param>
        private void ReceiveData(byte[] byteData)
        {
            string data = string.Empty;
            if (this.ckb16Receive.Checked)//十六机制接收
            {
                data = this.serialPortHelper.AlgorithmHelperObject.BytesTo16(byteData, Enum16Hex.Blank);
                //在这里编写具体的数据处理过程。。。可以保存到数据库,或其他文件...
            }
            else
            {
                data = this.serialPortHelper.AlgorithmHelperObject.BytesToString(byteData, Enum16Hex.None);
            }

            //显示到当年文本框中
            //因为接收数据的事件是一个独立显存,所有必须通过跨线程访问可视化控件的方法来完成展示
            //Invoke()方法的第一个参数必须是返回值为void的委托,第二个参数是给委托对应方法传递的参数
            this.txtReceiver.Invoke(new Action<string>(s => { this.txtReceiver.Text += "   " + s; }), data);

            //屏蔽跨线程访问可视化空间引发的异常(不建议使用这种方式)
            //  Control.CheckForIllegalCrossThreadCalls = false;
        }

        #endregion

        //请空数据按钮
        private void btnClear_Click(object sender, EventArgs e)
        {
            this.txtReceiver.Clear();
            this.txtSender.Clear();
        }

        private void GroupBox2_Enter(object sender, EventArgs e)
        {

        }

        private void FrmHelper_Load(object sender, EventArgs e)
        {

        }
    }
}

如大家可以工具自己的想法完善界面,大神也可以补充优化。最后大家可以回复“蜜蜂串口”,获取。

             最后祝全天下母亲母亲节快乐。

 

以上是关于QT5.14串口调试助手:上位机接收数据解析数据帧+多通道波形显示+数据保存为csv文件的主要内容,如果未能解决你的问题,请参考以下文章

C#上位机开发一:串口通讯之如何制作一个串口调试助手

ZYNQ之FPGA学习----UART串口实验

STC学习:串口通信

串口调试助手,VB6.0开发

单片机串口怎么接收超过255字节的数据,数组只能存放255字节,有啥方法可以实现不间断的接收

串口助手Python从零开始制作温湿度串口上位机