C2—Qt实现串口调试助手2021.10.21

Qt实现串口调试助手

1.目标

实现串口调试助手,界面风格简明,可配置关键通信参数包括,波特率、停止位、数据位、校验位;可以以16进制发送数据,以16进制或者ascii码接收数据,支持将接收数据保存至本地txt。
注:查看FPGA下位机串口代码实现,请查看FPGA串口通信
下载FPGA串口通信工程文件和代码,请查看FPGA串口通信vivado工程

2.步骤

2.1.pro当中添加,串口模块。

该模块Qt5支持,Qt4不支持;

QT += serialport

2.2串口相关类介绍

#include <QtSerialPort/QSerialPortInfo>
#include <QtSerialPort/QSerialPort>

QSerialPortInfo这个类将会获取串口的信息,如下:

        qDebug()<<"serialPortName:"<<info.portName();
        qDebug() << "Description : " << info.description();
        qDebug() << "Manufacturer: " << info.manufacturer();
        qDebug() << "Serial Number: " << info.serialNumber();
        qDebug() << "System Location: " << info.systemLocation();

QSerialPort提供了多种方法,如基本的open、close、read、write方法,以及错误处理、设置串口参数等多个方法,将协调工作,共同完成串口收发的操作。

2.3界面原型搭建

一些界面布局细节,排版原则在下面无限保真度的界面原型中全部体现,这个搭建不需要手工代码。
C2—Qt实现串口调试助手2021.10.21

2.4界面逻辑

(1)调用Init()函数,初始化函数将完成以下功能
上来先查询当前在线的串口设备,并将其写入下拉框;
初始化波特率选择下拉框,以及校验位、停止位、数据位;
设置默认以16进制显示;
清除接收缓冲区(黑色区域)。
(2)做按钮功能连接
”扫描串口“按钮重新扫描当前串口设备,并将其状态更新到对应下拉框中;
“打开串口”按钮将下发串口配置打开串口,添加打开失败模态提示用户,添加打开成功该按钮变为“关闭串口”;
"关闭串口"按钮将clear串口并关闭串口;
“清除接收”将textEdit clear掉;
“保存窗口”将当前接收框中内容保存至工程目录下的txt,并且清空当前接收框;
“发送”将进行数据处理操作并发送至打开的串口,当然会有串口失败的提示;
“清除发送”将清除当前的发送框;
(3)调整界面逻辑细节
“串口选择”QFrame固定大小,不随界面的大小变化;
“串口选择”中水平元素水平布局之后总体垂直布局;
“接收框”为只读模式;
设置“接收框”样式表,黑色背景,绿色字体;
“发送框”默认显示本人logo;

2.5底层逻辑

(1)实现获取当前配置函数,返回值为枚举类型,方便“打开串口操作”时调用,以获取校验位当前配置为例,如下:

enum QSerialPort::Parity MainWindow::getParity()
{
    QString currentParityBit = ui->checkBit_Sel->currentText();
    if (currentParityBit == "None")
        return QSerialPort::NoParity;
    else if (currentParityBit == "Odd")//奇数
        return QSerialPort::OddParity;
    else if (currentParityBit == "Even")//偶校验
        return QSerialPort::EvenParity;
    else
        return QSerialPort::UnknownParity;
}

(2)接收数据,串口数据缓冲区收到数据会发出信号&QSerialPort::readyRead,故用户只需写槽函数并将其链接。
notice:
(2.1)比如对方发送 0x11、0x22、0x33、0x44、0x55,的数据,很可能会触发很多次 ReadSerialData() 调用,并不是收完这 5 个数据,才会触发一次,但是读走的数据,下次不会重复读到,这点可以放心;
(2.2)这里需要去判断读取的 QByteArray 的 buffer 是不是空,调试的时候遇到过,空数据,也触发了一次进入这个槽函数;
(2.3)接收(发送)数据类型为QByteArray的类型,接收侧需要做数据类型转换方便显示处理

 str.append(QString::number((ary[i]&0xff),16));//QByteArray-->QString

(2.4)接收操作read、readAll,以及发送操作write都是非阻塞操作,即为函数执行,无论数据是否被全部接收(发送),函数都会立即返回,程序将会继续执行。
(2.5)支持16进制的显示、Ascii码的显示(两种数据类型处理)

(3)发送数据
默认按照16进制的方式发送数据,因此将只允许16进制相关的字符输入,其他的字符将不允许输入。

3.代码

3.1.cpp文件代码

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>
#include <QByteArray>
#include <QTextDecoder>
#include <QTextCodec>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    //QAbstractNativeEventFilter  QT系统事件过滤器,例如串口的插拔可以识别
    //即中断的方式实时检测设备状态
    Init();//初始化函数,作用:扫描在线串口;填充波特率、校验位、数据位、停止位窗口部件
    //重新扫描在线串口(显示当前设备状态)
    connect(ui->scanBtn,&QPushButton::clicked,[=](){
        ui->comboBox->clear();
        foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
        {
            ui->comboBox->addItem(info.portName());
        }
    });
    //打开串口按钮
    connect(ui->openBtn,&QPushButton::clicked,[=](){

        if(ui->openBtn->text()=="打开串口")
        {
//            qDebug()<<getBaud()<<getParity()<<getdataBits()<<getstopBits();
            mySerialPort->setPortName(ui->comboBox->currentText());
            mySerialPort->setBaudRate(getBaud());
            mySerialPort->setParity(getParity());
            mySerialPort->setDataBits(getdataBits());
            mySerialPort->setStopBits(getstopBits());
//            mySerialPort->setFlowControl(QSerialPort::NoFlowControl); // TODO: FlowControl
//            mySerialPort->setReadBufferSize(0);  //缓冲区无限大
            if(!(mySerialPort->open(QSerialPort::ReadWrite)))
            {
                //标准对话框提示打开串口失败
                QMessageBox::warning(this,"警告","打开串口失败");

            }
            else
            {
                ui->openBtn->setText("关闭串口");   
            }

        }
        else
        {
            mySerialPort->clear();
            mySerialPort->close();
            ui->openBtn->setText("打开串口");
        }
    });
    //保存按钮
    connect(ui->saveBtn,&QPushButton::clicked,[=](){
            QFile file("UART_Rec.txt");
            file.open(QIODevice::Append);
            //使用QTextStream写入文件
            QTextStream tsm(&file);
            tsm<<ui->reci_Edit->toPlainText();
            ui->reci_Edit->clear();
            file.close();
    });
    //清除接收按钮
    connect(ui->clearRecBtn,&QPushButton::clicked,[=](){
        ui->reci_Edit->clear();
    });
    //清除发送按钮
    connect(ui->clearXferBtn,&QPushButton::clicked,[=](){
        ui->xferEdit->clear();
    });
    //发送按钮
    connect(ui->xferBtn, &QPushButton::clicked, this, [=](){
        if(!mySerialPort->isOpen())
        {
            QMessageBox::information(this,"提示","请先打开串口");
        }
        else
        {
//            //获取发送框字符
            QString str = ui->xferEdit->document()->toPlainText();
            if (str.isEmpty())
            {
                QMessageBox::information(this,"提示","请先输入发送内容");
            }
            //默认16进制发送,过滤无用字符 包括0x
            else
            {
                char ch;
                bool flag = false;
                uint32_t i, len;
                //去掉无用符号
                str = str.replace(' ',"");
                str = str.replace(',',"");
                str = str.replace('\r',"");
                str = str.replace('\n',"");
                str = str.replace('\t',"");
                str = str.replace("0x","");
                str = str.replace("0X","");
                str = str.replace('\\',"");
                str = str.replace(':',"");
                str = str.replace('?',"");
                str = str.replace(';',"");
                //判断数据合法性
                for(i = 0, len = str.length(); i < len; i++)
                {
                    ch = str.at(i).toLatin1();
                    if (ch >= '0' && ch <= '9')
                    {
                        flag = false;
                    }
                    else if (ch >= 'a' && ch <= 'f')
                    {
                        flag = false;
                    }
                    else if (ch >= 'A' && ch <= 'F')
                    {
                        flag = false;
                    }
                    else
                    {
                        flag = true;
                    }
                }
            if(flag)
            {
                QMessageBox::warning(this,"警告","输入内容包含非法16进制字符");
            }
            else
            {
                //QString转QByteArray
                QByteArray xferAry = str.toUtf8();
            mySerialPort->write(xferAry);
            }
            }
        }
    });

    //接收Buffer
    connect(mySerialPort,&QSerialPort::readyRead,[=](){
        if(ui->hex_Rtn->isChecked())
        {
            //以16进制显示
            QByteArray ary = mySerialPort->readAll();
            qDebug()<<ary;
            QString str;
            str.clear();
            for (int i = 0;i<ary.size()-2 ;i++ ) {
                str.append(QString::number((ary[i]&0xff),16));//将16进制原样输出
//                str.append("  ");
            }
            ui->reci_Edit->append(str);
//            ui->reci_Edit->append("\n");
        }
        else
        {
            //以Ascii码显示  Ascii码指的是16进制所对应的字符,就是ascii规定的一个16进制和字符之间的映射
            ui->reci_Edit->append(mySerialPort->readAll());
            ui->reci_Edit->append("\n");
        }
    });
}

MainWindow::~MainWindow()
{
    if(mySerialPort->isOpen())
    {
        mySerialPort->clear();
        mySerialPort->close();
    }
    delete mySerialPort;
    delete ui;
}

enum QSerialPort::Parity MainWindow::getParity()
{
    QString currentParityBit = ui->checkBit_Sel->currentText();
    if (currentParityBit == "None")
        return QSerialPort::NoParity;
    else if (currentParityBit == "Odd")//奇数
        return QSerialPort::OddParity;
    else if (currentParityBit == "Even")//偶校验
        return QSerialPort::EvenParity;
    else
        return QSerialPort::UnknownParity;
}

enum QSerialPort::BaudRate MainWindow::getBaud()
{
    bool ok;
    enum QSerialPort::BaudRate ret;
    switch (ui->baud_Sel->currentText().toLong(&ok, 10))
    {
        case 1200:
            ret = QSerialPort::Baud1200;
        break;
        case 2400:
            ret = QSerialPort::Baud2400;
            break;
        case 4800:
            ret = QSerialPort::Baud4800;
            break;
        case 9600:
            ret = QSerialPort::Baud9600;
            break;
        case 19200:
            ret = QSerialPort::Baud19200;
            break;
        case 38400:
            ret = QSerialPort::Baud38400;
            break;
        case 57600:
            ret = QSerialPort::Baud57600;
            break;
        case 115200:
            ret = QSerialPort::Baud115200;
            break;
        default:
            ret = QSerialPort::UnknownBaud;
            break;
    }
    return ret;
}

enum QSerialPort::StopBits MainWindow::getstopBits()
{
     QString currentStopBits = ui->stopBit_Sel->currentText();
     if (currentStopBits == "1")
         return QSerialPort::OneStop;
     else if (currentStopBits == "1.5")
         return QSerialPort::OneAndHalfStop;
     else if (currentStopBits == "2")
         return QSerialPort::TwoStop;
     else
         return QSerialPort::UnknownStopBits;
}

enum QSerialPort::DataBits MainWindow::getdataBits()
{
    QString currentDataBits = ui->dataBit_Sel->currentText();
    if(currentDataBits == "5")
        return QSerialPort::Data5;
    else if (currentDataBits == "6")
        return QSerialPort::Data6;
    else if (currentDataBits == "7")
        return QSerialPort::Data7;
    else if (currentDataBits == "8")
        return QSerialPort::Data8;
    else
        return QSerialPort::UnknownDataBits;

}

void MainWindow::Init()
{

    //上来先扫描一遍当前串口,初始化COM口选择组件
    QStringList myPortName;
    foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
    {
        myPortName << info.portName();
        qDebug()<<"serialPortName:"<<info.portName();
        qDebug() << "Description : " << info.description();
        qDebug() << "Manufacturer: " << info.manufacturer();
        qDebug() << "Serial Number: " << info.serialNumber();
        qDebug() << "System Location: " << info.systemLocation();
    }
    myPortName.sort();
    ui->comboBox->addItems(myPortName);

    //初始化波特率组件
    QStringList myBaud;
    myBaud.clear();
    myBaud<<"1200"<<"2400"<<"4800"<<"9600"<<"19200"<<"38400"<<"57600"<<"115200";
    ui->baud_Sel->addItems(myBaud);
    ui->baud_Sel->setCurrentText("9600");
    //初始化停止位
    QStringList mystopBits;
    mystopBits.clear();
    mystopBits<<"1"<<"1.5"<<"2";
    ui->stopBit_Sel->addItems(mystopBits);
    ui->stopBit_Sel->setCurrentText("1");
    //初始化校验位
    QStringList mycheckBits;
    mycheckBits.clear();
    mycheckBits<<"None"<<"Even"<<"Odd";
    ui->checkBit_Sel->addItems(mycheckBits);
    ui->checkBit_Sel->setCurrentText("None");
    //初始化数据位
    QStringList mydataBits;
    mydataBits.clear();
    mydataBits<<"5"<<"6"<<"7"<<"8";
    ui->dataBit_Sel->addItems(mydataBits);
    ui->dataBit_Sel->setCurrentText("8");
    
    ui->hex_Rtn->setChecked(true);//设置默认是16进制显示
    ui->reci_Edit->clear();
}

3.2.h代码

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtSerialPort/QSerialPortInfo>
#include <QtSerialPort/QSerialPort>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    enum QSerialPort::Parity getParity();
    enum QSerialPort::BaudRate getBaud();
    enum QSerialPort::StopBits getstopBits();
    enum QSerialPort::DataBits getdataBits();
    void Init();

     //创建一个串口对象
    QSerialPort *mySerialPort = new QSerialPort();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

4.效果

使用虚拟串口软件VSPD虚拟串口,使用成熟软件XCOM V2.0(正点原子串口调试助手)与自己的软件通信,可以清晰的看到,软件收发正常。VSPD软件的获取与安装请查看工具类–虚拟串口工具

C2—Qt实现串口调试助手2021.10.21

5.思考

两个想法,1.USB属于热插拔设备,系统可以动态的检测设备的插入与断开,那么如何让自己的软件动态的查看串口设备的连接状态呢?实际上,Qt提供了“中断”接口,可以做做升级版;
2.现在只支持16进制发送,支持16进制、ascii码的接收,那么是否可以考虑利用串口传输汉字呢?实际上,Qt支持多种字符集,可以尝试一番。

上一篇:Qt 寻找可用串口


下一篇:QT串口QSerialPort解决接收数据不完整问题