Qt——写一个自己的串口调试助手

Qt——写一个自己的串口调试助手

H_Haozi Lv2

A 引入

软件功能:

  1. 自动扫描电脑连接的串口
  2. 接收消息和发送消息,实现不同编码的通信,比如hex,utf8,gbk
  3. 能够设置串口的一些配置,比如波特率,停止位等

所需环境和软件工具:

  1. Qt Creator 4.12.2
  2. Qt 5.12.9
  3. 现成的能用串口助手,我用的是江科大的串口助手
  4. 虚拟串口工具(用于创建一对虚拟串口),我用的是vpsd
  5. 电脑为win11 64bit

最终界面:

B 具体实现

B.1界面UI布局

在“mainwindow.ui”中布局如下,因为界面比较简单就不写布局的代码了

然后可以在波特率,停止位等下拉框中填入常用的量,后面代码中会使用

B.2实现打开软件时自动读取可用串口

从Qt 5.1版本开始,Qt就有了自己的串口通讯类,之前版本需要使用第三方的串口通信类才行。
要想使用串口通信类,需要在 .pro 文件中添加 QT += serialport

同时要添加头文件:
#include “QSerialPort”
#include “QSerialPortInfo”

显示串口COM的下拉框是一个QcomboBox控件,名称为ui->com_combox我们需要往这个控件中传入电脑连接的所有串口号的列表,所以先通过串口类中的QSerialPortInfo::availablePorts() 可以获得一个 QList ,List中的每一项 QSerialPortInfo 代表一个串口实例,该类中保存了系统中已有串口的端口名称、系统位置、描述和供应商等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

//创建一个串口的对象
my_serialPort = new QSerialPort;

//创建获取串口号列表
QStringList my_serialPortName = getPortNameList();

//清空下拉栏
ui->com_combox->clear();

//添加查询到的串口到窗口中
ui->com_combox->addItems(my_serialPortName);


下面实现一个打开软件时获取串口的函数getPortNameList()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#include <QSerialPort>
#include <QSerialPortInfo>

//获取串口号
QStringList MainWindow::getPortNameList()
{
QStringList my_serialPortName;
auto ports = QSerialPortInfo::availablePorts();
if(ports.isEmpty())
{
qDebug()<<"没有串口连接";
}
else
{
for(const QSerialPortInfo &info : ports)
{
my_serialPortName << info.portName();
}
}
return my_serialPortName;
}


B.2实现点击下拉框之后自动更新串口列表

因为QT并没提供有关点击下拉框之后的信号量,所以我们这里需要重写Combobox的showPopup()函数,所以我们可以创建一个自己的Combobox类,然后在ui界面对com_combox进行提升

先创建一个mycombobox.cpp 和 mycombobox.h文件,并重写showPopup()函数,在这个函数中重写获取一次串口号列表,然后写到这个下拉框之中,这样就可以实现用户点击下拉框之后,就执行这些操作,注意QComboBox::showPopup();//转给父类调用不要忘记,否则下拉框点击后可能不会展开

mycombobox.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

#include "mycombobox.h"
#include "mainwindow.h"
#include <QDebug>
#include <QComboBox>

mycomboBox::mycomboBox(QWidget *parent) : QComboBox(parent)
{

}

//重写下拉后的操作
void mycomboBox::showPopup()
{
qDebug()<<"qqq";

QString current_text = this->currentText();
QStringList my_serialPortName;
my_serialPortName.clear();
QComboBox::clear();

auto ports = QSerialPortInfo::availablePorts();
if(ports.isEmpty())
{
qDebug()<<"没有串口连接";
}
else
{
for(const QSerialPortInfo &info : ports)
{
my_serialPortName << info.portName();
}
}

QComboBox::addItems(my_serialPortName);
setCurrentText(current_text);

QComboBox::showPopup();//转给父类调用
}


mycombobox.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#ifndef MYCOMBOBOX_H
#define MYCOMBOBOX_H

#include <QWidget>
#include <QComboBox>
#include <QSerialPort>
#include <QSerialPortInfo>

class mycomboBox : public QComboBox
{
Q_OBJECT
public:
explicit mycomboBox(QWidget *parent = nullptr);

void showPopup();

signals:

};

#endif // MYCOMBOBOX_H

B.3实现”打开串口”

当用户点击”打开串口”按钮后,函数中需要判断串口号,停止位,波特率等设置是否正确,然后调用下面相关函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//判断串口是否已打开
bool QIODevice::isOpen() const
//清空缓冲区
bool QSerialPort::clear(QSerialPort::Directions directions = AllDirections)
//串口关闭
[override virtual] void QSerialPort::close()
//设置要打开的串口名
void QSerialPort::setPortName(const QString &name)
//设置串口通信的波特率
bool QSerialPort::setBaudRate(qint32 baudRate, QSerialPort::Directions directions = AllDirections)
//设置串口通信的数据位,数据位一般为8位
bool QSerialPort::setDataBits(QSerialPort::DataBits dataBits)
//设置串口通信的流控制,一般无需流控制
bool QSerialPort::setFlowControl(QSerialPort::FlowControl flowControl)
//设置串口通信的奇偶校验,一般选择“无”
bool QSerialPort::setParity(QSerialPort::Parity parity)
//设置串口通信的停止位,停止位一般为1
bool QSerialPort::setStopBits(QSerialPort::StopBits stopBits)

主函数中监听按钮是否点击,然后调用open_SerialCom(my_serialPortName);

1
2
3
4
5
6

//监听“打开串口”按钮
connect(ui->open_radiobut, &QRadioButton::clicked,[=](){
open_SerialCom(my_serialPortName);
});

下面是对 void open_SerialCom(QStringList my_serialPortName) 的实现,其中如果遇到错误会调用err_Open_Serial()函数发出提示框给用户,并且退出函数。

在open_SerialCom()函数中会判断所有下拉框,比如ui->btl_comboxui->stop_combox等的值,需要确保下拉框设置的值与函数中使用的值一一对应,否则可能会发生错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

//打开串口
void MainWindow::open_SerialCom(QStringList my_serialPortName)
{
auto ports = QSerialPortInfo::availablePorts();
if (ui->open_radiobut->text()=="打开串口")
{
//再次检查串口是否有效
if(ui->com_combox->count() == 0||ports.isEmpty()||my_serialPort->isOpen()) //当没有串口来连接
{
err_Open_Serial();
return;
}
if(my_serialPort->isOpen())
{
my_serialPort->clear();
my_serialPort->close();
}

my_serialPort->setPortName(my_serialPortName[ui->com_combox->currentIndex()]);

if(!my_serialPort->open(QIODevice::ReadWrite))
{
err_Open_Serial();
return;
}

//打开成功
my_serialPort->setBaudRate(ui->btl_combox->currentText().toInt(),QSerialPort::AllDirections);//设置波特率和读写方向

switch (ui->data_combox->currentIndex()) {
case 0 : my_serialPort->setDataBits(QSerialPort::Data8); break; //数据位为8位
case 1 : my_serialPort->setDataBits(QSerialPort::Data7); break;
case 2 : my_serialPort->setDataBits(QSerialPort::Data6); break;
case 3 : my_serialPort->setDataBits(QSerialPort::Data5); break;
default: qDebug()<<"数据位错误";
}

switch (ui->stop_combox->currentIndex()) {
case 0 : my_serialPort->setStopBits(QSerialPort::OneStop);break; //一位停止位
case 1 : my_serialPort->setStopBits(QSerialPort::OneAndHalfStop);break;
case 2 : my_serialPort->setStopBits(QSerialPort::TwoStop);break;
default: qDebug()<<"停止位错误";
}

switch (ui->jiaoyan_combox->currentIndex()) {
case 0 : my_serialPort->setParity(QSerialPort::NoParity);break; //无校验位
case 1 : my_serialPort->setParity(QSerialPort::OddParity);break;
case 2 : my_serialPort->setParity(QSerialPort::EvenParity);break;
default: qDebug()<<"校验位错误";
}

my_serialPort->setFlowControl(QSerialPort::NoFlowControl); //无流控制

connect(my_serialPort,SIGNAL(readyRead()),this,SLOT(receiveInfo()));

//禁用部分按钮等
my_setEnabled(false);
ui->open_radiobut->setText("关闭串口");
}
else
{
my_setEnabled(true);
my_serialPort->close();
ui->open_radiobut->setText("打开串口");
}
}

//串口打开失败界面
void MainWindow::err_Open_Serial()
{
QMessageBox::critical(this, "警告", "串口打开失败,请检查串口");
ui->open_radiobut->setChecked(false);
my_setEnabled(true);
}

B.4实现接收和发送数据

当串口打开之后,如果接受到了数据,那么&QSerialPort::readyRead会发出信号,我们需要在主函数监听这个两个信号

1
2
3
4
5
6
7
8
9
10
11

//监听"发送"按钮
connect(ui->send_but,&QPushButton::clicked,[=](){
send_data();
});

//监听"接收"数据
connect(my_serialPort,&QSerialPort::readyRead,[=]() {
receive_data();
});

收到的数据可以通过串口对象的my_serialPort->readAll()方法进行读取,然后我们需要判断接收的格式,进行相应的格式转换,然后将处理好的数据发送到接受数据文本框中 ui->re_text->append()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

//接收数据
void MainWindow::receive_data()
{
QByteArray info = my_serialPort->readAll();

QString strReceiveData = "";
if(ui->hex_re_cheak->isChecked())
{
QByteArray hexData = info.toHex();
strReceiveData = hexData.toUpper();

qDebug()<<"接收到串口数据: "<<strReceiveData;

for(int i=0; i<strReceiveData.size(); i+=2+1)
strReceiveData.insert(i, QLatin1String(" "));
strReceiveData.remove(0, 1);

qDebug()<<"处理后的串口数据: "<<strReceiveData;

ui->re_text->append(strReceiveData);
}
else
{
strReceiveData = info;

if (ui->gbk_re->isChecked())
{
QTextCodec *tc = QTextCodec::codecForName("GBK");
QString tmpQStr = tc->toUnicode(info);
ui->re_text->append(tmpQStr);
}
else
{
QString tmpQStr = QString::fromUtf8(info);
ui->re_text->append(tmpQStr);
}
}
}

发送数据需要调用my_serialPort->write()函数,同样要根据按钮进行格式转换,然后再进行发送,注意在发送之前检查串口的打开情况,其中用到了两个函数,isHexString():判断是否是hex数据,和convertStringToHex()将Qstring转换成hex数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

//发送数据
void MainWindow::send_data()
{
//检查串口打开情况
if (!my_serialPort->isOpen())
{
err_Open_Serial();
return;
}

QString my_strSendData = ui->send_text->toPlainText();

if(ui->hex_send_cheak->isChecked())
{
my_strSendData.remove(" "); // 去掉空格

QByteArray sendBuf;

if (!isHexString(my_strSendData))
{
QMessageBox::critical(this, "警告", "输入内容非16进制");
return;
}

convertStringToHex(my_strSendData, sendBuf); //把QString 转换 为 hex

my_serialPort->write(sendBuf);

}
else
{
if(ui->gbk_send->isChecked())
{
my_serialPort->write(my_strSendData.toLocal8Bit());//发送GBK编码数据
}
else my_serialPort->write(my_strSendData.toUtf8()); // 发送UTF-8编码数据

}
my_serialPort->write("\r\n");

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

bool MainWindow::isHexString(const QString &str)
{
// 正则表达式匹配 0-9、a-f、A-F 和空格
QRegularExpression re("^[0-9a-fA-F ]+$");
return re.match(str).hasMatch();
}

void MainWindow::convertStringToHex(const QString &str, QByteArray &sendBuf)
{
sendBuf.clear();
QString hexStr = str.simplified().replace(" ", ""); // 去掉空格

for (int i = 0; i < hexStr.length(); i += 2) {
bool ok;
char byte = hexStr.mid(i, 2).toUShort(&ok, 16);
if (ok) {
sendBuf.append(byte);
} else {
sendBuf.clear(); // 如果有无效字符,清空结果
break;
}
}
}

当这几个基本功能完成之后,就可以利用vpsd(虚拟串口创建工具),测试一下以上功能
先创建一对虚拟串口com1 com2 ,然后利用两个串口助手分别连接1,2,然后相互发送东西

测试发送接收消息

测试在使用其它串口助手打开串口COM1的情况下,用自己写的串口调试助手打开串口COM1

B.5实现其它基础功能及 优化

下面实现“清除文本框”,以及优化当未打开串口时禁用发送按钮,打开串口后禁用部分下拉框,hex模式中禁止gbk选项,直接点击窗口关闭按钮后,主动关闭串口等,部分代码添加在打开串口和关闭串口的代码中,下面主要是函数实现

这是主函数中的监听函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//"发送"默认按钮为关闭
ui->send_but->setEnabled(false);

//监听"清空"按钮
connect(ui->clear_re_but,&QPushButton::clicked,[=]{
ui->re_text->clear();
});
connect(ui->clear_send_but,&QPushButton::clicked,[=]{
ui->send_text->clear();
});

//当hex模式时,禁止选择gbk等编码
connect(ui->hex_re_cheak, &QPushButton::clicked, [=]() {
toggleHexSettings();
});
connect(ui->hex_send_cheak, &QPushButton::clicked, [=]() {
toggleHexSettings();
});


具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

//打开串口后禁用部分下拉框
void MainWindow::my_setEnabled(bool flag)
{
ui->com_combox->setEnabled(flag);
ui->btl_combox->setEnabled(flag);
ui->stop_combox->setEnabled(flag);
ui->data_combox->setEnabled(flag);
ui->jiaoyan_combox->setEnabled(flag);
ui->send_but->setEnabled(!flag);
}

void MainWindow::toggleHexSettings()
{
if (ui->hex_re_cheak->isChecked() || ui->hex_send_cheak->isChecked()) {
// 当 hex_re_cheak 或 hex_send_cheak 被勾选时,禁用相关控件
ui->gbk_re->setEnabled(false);
ui->gbk_send->setEnabled(false);
} else {
// 当都没有勾选时,恢复相关控件
ui->gbk_re->setEnabled(true);
ui->gbk_send->setEnabled(true);
}
}


//直接点击窗口关闭按钮后,主动关闭串口
void MainWindow::closeEvent(QCloseEvent *event)
{
if (my_serialPort->isOpen()) {
my_serialPort->close(); // 关闭串口
}

event->accept(); // 允许窗口关闭
}

到此一个简易的串口调试助手大功告成~,具体源码可以参考后面的工程源码,如果有问题和建议可以提出

C有关软件打包的总结

如果没有exe文件图标需求:
当使用release编译之后,使用QT自带的命令框的指令windeployqt打包(具体步骤可以自行查找资料),然后删除不用的dll文件,减少软件体积,然后可以选择直接压缩,或则使用enigmavb软件封包,或HM NIS Edit软件创建安装向导

如果有exe文件需求:
在工程文件源代码中添加一个图片文件(注意不是添加资源文件),要求必须为.ico格式(可以在网上在线转换格式,不要直接改后缀名)

然后在工程的.pro文件中最后添加一句:RC_ICONS = mouse.ico 注意,后面是自己的图标文件名字,最后再进行编译运行,可以发现自己的debug文件中的exe文件的图标已经改成你的图标了


然后同第一个步骤一样就可以打包成自己的exe可执行文件了!

D结尾

参考文献

  1. QT官网文档

文件地址

  1. github仓库

留言

有问题请指出,你可以选择以下方式:

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: Qt——写一个自己的串口调试助手
  • Author: H_Haozi
  • Created at : 2024-12-09 10:07:30
  • Updated at : 2024-12-10 20:36:06
  • Link: https://redefine.ohevan.com/2024/12/09/Qt_Serial/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments