一款基于Qt的跨平台开源串口调试助手EasySerial
文章目录1.简述2.开发步骤3.Linux系统测试4.其他1.简述串口调试助手在调试串口设备是非常适用的一个工具,在Windows有很多很好用的串口调试助手,但是在Linux系统上好像还没找到一个好用的带界面的软件(用命令行调试的工具还是有很多的),但是总感觉没有那么方便使用。因此基于Qt良好的跨平台特性,开发一个跨平台简易的串口调试助手,满足基本的串口调试需求。最后开源分享出来。目前实现的功能有
文章目录
1. 简述
串口调试助手在调试串口设备是非常适用的一个工具,在Windows有很多很好用的串口调试助手,但是在Linux系统上好像还没找到一个好用的带界面的软件(用命令行调试的工具还是有很多的),但是总感觉没有那么方便使用。因此基于Qt良好的跨平台特性,开发一个跨平台简易的串口调试助手,满足基本的串口调试需求。最后开源分享出来。目前实现的功能有:
- 搜索串口设备;
- ASCII/HEX接收;
- 接收数据保存到txt;
- ASCII/HEX发送;
- 周期发送;
- 读取txt发送;
- 收发字节计数;
源码在Window系统,Qt5.12.1环境下编写,并在树莓派上的Raspbian上进行跨平台测试。
2. 开发步骤
使用串口功能先在工程里添加serialport组件
QT += serialport
2.1 界面布置

新建一个QMainWindow工程,使用Qt Designer布置界面见上图,界面和常见的串口调试助手相似,使用的都是常规控件。首先使用QGroupBox将界面分块。使用QCombox来选取串口号、波特率、校验位、数据位和停止位。QPushButton来搜索串口、打开/关闭串口、发送数据等。单选按钮QRadioButton来旋转ASCII或HEX发送或接收,将多个QRadioButton放在一个QGroupBox里可互斥选择,即只可选中一个,实现单选功能。使用QTextBrowser显示接收的数据,使用QTextEdit来输入待发送的数据…
2.2 串口搜索与打开
Search按钮槽函数实现搜索串口的功能,并添加到QComBox中。
void MainWindow::on_pushButton_search_clicked()
{
this -> ui -> comboBox_name -> clear();
foreach (QSerialPortInfo avaiablePort, QSerialPortInfo::availablePorts()) {
this -> ui -> comboBox_name -> addItem(avaiablePort.portName());
}
}
Open按钮槽函数实现串口打开与关闭功能。
void MainWindow::on_pushButton_open_clicked()
{
if(this->ui->pushButton_open->text() == "Open")
{
serial.setPortName(this->ui->comboBox_name->currentText());
serial.setBaudRate(this->ui->comboBox_baud->currentText().toInt());
switch (this -> ui -> comboBox_paity -> currentIndex()){
case 0: serial.setParity(QSerialPort::NoParity); break;
case 1: serial.setParity(QSerialPort::EvenParity); break;
case 2: serial.setParity(QSerialPort::OddParity); break;
case 3: serial.setParity(QSerialPort::SpaceParity); break;
case 4: serial.setParity(QSerialPort::MarkParity); break;
default: serial.setParity(QSerialPort::UnknownParity); break;
}
switch (this->ui->comboBox_dataBits->currentText().toInt()){
case 5: serial.setDataBits(QSerialPort::Data5); break;
case 6: serial.setDataBits(QSerialPort::Data6); break;
case 7: serial.setDataBits(QSerialPort::Data7); break;
case 8: serial.setDataBits(QSerialPort::Data8); break;
default: serial.setDataBits(QSerialPort::UnknownDataBits); break;
}
switch (this->ui->comboBox_stopBits->currentIndex()){
case 0: serial.setStopBits(QSerialPort::OneStop); break;
case 1: serial.setStopBits(QSerialPort::OneAndHalfStop); break;
case 2: serial.setStopBits(QSerialPort::TwoStop); break;
default: serial.setStopBits(QSerialPort::UnknownStopBits); break;
}
serial.setFlowControl(QSerialPort::NoFlowControl);
if(serial.open(QIODevice::ReadWrite))
{
this -> ui -> pushButton_search -> setEnabled(false);
this -> ui -> comboBox_name -> setEnabled(false);
this -> ui -> comboBox_baud -> setEnabled(false);
this -> ui -> comboBox_paity -> setEnabled(false);
this -> ui -> comboBox_dataBits -> setEnabled(false);
this -> ui -> comboBox_stopBits -> setEnabled(false);
this -> ui -> pushButton_open -> setText("Close");
QSerialPortInfo serialInfo(serial);
}else {
QMessageBox::warning(this,"Open Error","Serialport Open Error!");
}
}else{
serial.close();
this -> ui -> pushButton_search -> setEnabled(true);
this -> ui -> comboBox_name -> setEnabled(true);
this -> ui -> comboBox_baud -> setEnabled(true);
this -> ui -> comboBox_paity -> setEnabled(true);
this -> ui -> comboBox_dataBits -> setEnabled(true);
this -> ui -> comboBox_stopBits -> setEnabled(true);
this -> ui -> pushButton_open -> setText("Open");
}
}
2.3 ASCII/HEX接收
在构造函数中,绑定信号readyRead到自定义槽函数readSerialData,当串口来数据了,会自动调用该槽函数。
connect(&serial,&QSerialPort::readyRead,this,&MainWindow::readSerialData);
在readSerialData中读取串口接收缓存区数据并显示在textBrowser中。
void MainWindow::readSerialData()
{
QByteArray recvData = serial.readAll();
RXCounts += recvData.length();
RXLabel . setText(QString::number(RXCounts)); //更新接收计数
QString newData;
if(recvASCII)
newData = QString(recvData); //ASCII显示
else
newData = QString(recvData.toHex(' ')); //HEX显示
if(display) //如果显示,append到textBrowser
{
this -> ui -> textBrowser -> append(newData);
}
if(recvToFile) //如果勾选了Recv to File,将接收到的数据保存到本地文件中
{
QTextStream out(&recvFile);
out << newData;
}
}
2.4 接收数据保存
使用QCheckBox的checked(bool)槽函数,若选中则新建并打开一个txt文件,在readSerialData()函数中将接收到的数据写入到已打开的file中。若取消勾选,关闭文件。
void MainWindow::on_checkBox_recv_to_file_clicked(bool checked)
{
if(checked)
{
recvFile.setFileName(QString("%1_%2.txt").arg(QDate::currentDate().toString("yy_MM_dd")).arg(QTime::currentTime().toString("hh_mm_ss")));
if(!recvFile.open(QIODevice::WriteOnly))
this -> ui -> statusBar -> showMessage("File open failed, try again!",1000);
else{
recvToFile = true;
}
}else{
recvToFile = false;
recvFile.close();
}
}
2.5 ASCII/HEX发送
Send按钮槽函数实现单次、周期发送,ASCII、HEX发送。
void MainWindow::on_pushButton_send_clicked()
{
if(this -> ui -> pushButton_send -> text() == "Send")
{
if(serial.isOpen())
{
if(sendCyclic)
{
sendTimer.start(period); //若周期发送,打开定时器
this -> ui -> pushButton_send -> setText("Stop");
this -> ui -> checkBox_send_cyclic -> setEnabled(false);
this -> ui -> lineEdit_send_period -> setEnabled(false);
}else{
sendSerialData(); //若非周期,直接发送
}
}
else {
this -> ui -> statusBar -> showMessage("Serial closed, please open first",1000);
}
}
else if(this -> ui -> pushButton_send -> text() == "Stop") {
sendTimer.stop();
this -> ui -> pushButton_send -> setText("Send");
this -> ui -> checkBox_send_cyclic -> setEnabled(true);
this -> ui -> lineEdit_send_period -> setEnabled(true);
}
}
void MainWindow::sendSerialData()
{
QByteArray sendData;
if(sendASCII)
{
sendData = this -> ui -> textEdit -> toPlainText().toLatin1(); //直接发ASCII码
}else{
sendData = QByteArray::fromHex(this -> ui -> textEdit -> toPlainText().toLatin1()); //发送HEX
}
TXCounts += serial.write(sendData);
this -> TXLabel . setText(QString::number(TXCounts)); //更新发送计数
}
2.6 周期发送
使用QTimer定时器定周期调用sendSerialData(),QTimer的开启与关闭在Send按钮槽函数中实现。
connect(&sendTimer,&QTimer::timeout,this,&MainWindow::sendSerialData);
是否周期发送,根据Send Cyclic复选框的勾选情况。
void MainWindow::on_checkBox_send_cyclic_clicked(bool checked)
{
if(checked)
{
if(this->ui->lineEdit_send_period -> text().isEmpty())
{
QMessageBox::information(this,"Waring","Please edit period first");
this -> ui -> checkBox_send_cyclic -> setChecked(false);
}else {
period = this->ui->lineEdit_send_period -> text().toInt();
sendCyclic = true;
}
}else{
sendCyclic = false;
}
}
2.7 读取文件发送
Read按钮槽函数实现读取文件内容,并将内容添加到输入文本框中。
void MainWindow::on_pushButton_read_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this,"Please select file","./","TXT(*.txt)");
QFile sendFile(fileName);
if(!sendFile.open(QIODevice::ReadOnly))
QMessageBox::warning(this,"Error","File open failed, please try again");
QString sendStr = sendFile.readAll();
this -> ui -> textEdit -> setText(sendStr);
}
2.8 收发计数
在MainWindow构造函数中添加系列函数,实现在状态栏中添加收发计数显示。在statusBar中添加QLabel显示接收和发送的字节数,添加QPushButton来重置计数。计数的更新分别在readSerialData()和sendSerialData()函数中实现。
RXCounts = 0;
TXCounts = 0;
TXLabel.setText("0");
RXLabel.setText("0");
this -> ui -> statusBar -> addPermanentWidget(new QLabel("TX"));
this -> ui -> statusBar -> addPermanentWidget(&TXLabel);
this -> ui -> statusBar -> addPermanentWidget(new QLabel("RX"));
this -> ui -> statusBar -> addPermanentWidget(&RXLabel);
pushButton_countClear.setText("Reset");
connect(&pushButton_countClear,&QPushButton::clicked,this,[=](){
RXCounts = 0;
TXCounts = 0;
TXLabel.setText("0");
RXLabel.setText("0");
});
this -> ui -> statusBar -> addPermanentWidget(&pushButton_countClear);
2.8 完整代码
mainwindow.h代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QFile>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void readSerialData();
void sendSerialData();
void on_pushButton_search_clicked();
void on_pushButton_open_clicked();
void on_radioButton_recv_hex_clicked();
void on_radioButton_recv_ascii_clicked();
void on_pushButton_clear_recv_clicked();
void on_checkBox_stop_display_clicked(bool checked);
void on_checkBox_recv_to_file_clicked(bool checked);
void on_radioButton_send_ascii_clicked();
void on_radioButton_send_hex_clicked();
void on_pushButton_clear_send_clicked();
void on_pushButton_send_clicked();
void on_checkBox_send_cyclic_clicked(bool checked);
void on_pushButton_read_clicked();
private:
Ui::MainWindow *ui;
QSerialPort serial;
int TXCounts;
QLabel TXLabel;
int RXCounts;
QLabel RXLabel;
QPushButton pushButton_countClear;
bool recvToFile;
bool display;
bool recvASCII;
bool sendASCII;
QFile recvFile;
bool sendCyclic;
int period; //ms
QTimer sendTimer;
};
#endif // MAINWINDOW_H
mainwindow代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QSerialPortInfo>
#include <QMessageBox>
#include <QTime>
#include <QDate>
#include <QTextStream>
#include <QDebug>
#include <QFileDialog>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
RXCounts = 0;
TXCounts = 0;
TXLabel.setText("0");
RXLabel.setText("0");
this -> ui -> statusBar -> addPermanentWidget(new QLabel("TX"));
this -> ui -> statusBar -> addPermanentWidget(&TXLabel);
this -> ui -> statusBar -> addPermanentWidget(new QLabel("RX"));
this -> ui -> statusBar -> addPermanentWidget(&RXLabel);
pushButton_countClear.setText("Reset");
connect(&pushButton_countClear,&QPushButton::clicked,this,[=](){
RXCounts = 0;
TXCounts = 0;
TXLabel.setText("0");
RXLabel.setText("0");
});
this -> ui -> statusBar -> addPermanentWidget(&pushButton_countClear);
connect(&serial,&QSerialPort::readyRead,this,&MainWindow::readSerialData);
connect(&serial,&QSerialPort::errorOccurred,this,[=](QSerialPort::SerialPortError portErr){
this -> ui -> statusBar -> showMessage(QString("Serial error %1").arg(portErr),1000);
});
recvToFile = false;
display = true;
recvASCII = true;
sendCyclic = false;
connect(&sendTimer,&QTimer::timeout,this,&MainWindow::sendSerialData);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_search_clicked()
{
this -> ui -> comboBox_name -> clear();
foreach (QSerialPortInfo avaiablePort, QSerialPortInfo::availablePorts()) {
this -> ui -> comboBox_name -> addItem(avaiablePort.portName());
}
}
void MainWindow::on_pushButton_open_clicked()
{
if(this->ui->pushButton_open->text() == "Open")
{
serial.setPortName(this->ui->comboBox_name->currentText());
serial.setBaudRate(this->ui->comboBox_baud->currentText().toInt());
switch (this -> ui -> comboBox_paity -> currentIndex()){
case 0: serial.setParity(QSerialPort::NoParity); break;
case 1: serial.setParity(QSerialPort::EvenParity); break;
case 2: serial.setParity(QSerialPort::OddParity); break;
case 3: serial.setParity(QSerialPort::SpaceParity); break;
case 4: serial.setParity(QSerialPort::MarkParity); break;
default: serial.setParity(QSerialPort::UnknownParity); break;
}
switch (this->ui->comboBox_dataBits->currentText().toInt()){
case 5: serial.setDataBits(QSerialPort::Data5); break;
case 6: serial.setDataBits(QSerialPort::Data6); break;
case 7: serial.setDataBits(QSerialPort::Data7); break;
case 8: serial.setDataBits(QSerialPort::Data8); break;
default: serial.setDataBits(QSerialPort::UnknownDataBits); break;
}
switch (this->ui->comboBox_stopBits->currentIndex()){
case 0: serial.setStopBits(QSerialPort::OneStop); break;
case 1: serial.setStopBits(QSerialPort::OneAndHalfStop); break;
case 2: serial.setStopBits(QSerialPort::TwoStop); break;
default: serial.setStopBits(QSerialPort::UnknownStopBits); break;
}
serial.setFlowControl(QSerialPort::NoFlowControl);
if(serial.open(QIODevice::ReadWrite))
{
this -> ui -> pushButton_search -> setEnabled(false);
this -> ui -> comboBox_name -> setEnabled(false);
this -> ui -> comboBox_baud -> setEnabled(false);
this -> ui -> comboBox_paity -> setEnabled(false);
this -> ui -> comboBox_dataBits -> setEnabled(false);
this -> ui -> comboBox_stopBits -> setEnabled(false);
this -> ui -> pushButton_open -> setText("Close");
QSerialPortInfo serialInfo(serial);
}else {
QMessageBox::warning(this,"Open Error","Serialport Open Error!");
}
}else{
serial.close();
this -> ui -> pushButton_search -> setEnabled(true);
this -> ui -> comboBox_name -> setEnabled(true);
this -> ui -> comboBox_baud -> setEnabled(true);
this -> ui -> comboBox_paity -> setEnabled(true);
this -> ui -> comboBox_dataBits -> setEnabled(true);
this -> ui -> comboBox_stopBits -> setEnabled(true);
this -> ui -> pushButton_open -> setText("Open");
}
}
void MainWindow::readSerialData()
{
QByteArray recvData = serial.readAll();
RXCounts += recvData.length();
RXLabel . setText(QString::number(RXCounts));
QString newData;
if(recvASCII)
newData = QString(recvData);
else
newData = QString(recvData.toHex(' '));
if(display)
{
this -> ui -> textBrowser -> append(newData);
}
if(recvToFile)
{
QTextStream out(&recvFile);
out << newData;
}
}
void MainWindow::on_radioButton_recv_hex_clicked()
{
recvASCII = false;
}
void MainWindow::on_radioButton_recv_ascii_clicked()
{
recvASCII = true;
}
void MainWindow::on_pushButton_clear_recv_clicked()
{
this -> ui -> textBrowser -> clear();
}
void MainWindow::on_checkBox_stop_display_clicked(bool checked)
{
display = !checked;
}
void MainWindow::on_checkBox_recv_to_file_clicked(bool checked)
{
if(checked)
{
recvFile.setFileName(QString("%1_%2.txt").arg(QDate::currentDate().toString("yy_MM_dd")).arg(QTime::currentTime().toString("hh_mm_ss")));
if(!recvFile.open(QIODevice::WriteOnly))
this -> ui -> statusBar -> showMessage("File open failed, try again!",1000);
else{
recvToFile = true;
}
}else{
recvToFile = false;
recvFile.close();
}
}
void MainWindow::on_radioButton_send_ascii_clicked()
{
if(!sendASCII)
{
QString hexStr = this -> ui -> textEdit -> toPlainText();
QString str = QByteArray::fromHex(hexStr.toLatin1());
this -> ui -> textEdit -> setText(str);
sendASCII = true;
}
}
void MainWindow::on_radioButton_send_hex_clicked()
{
if(sendASCII)
{
QString str = this -> ui -> textEdit -> toPlainText();
QString hexStr = str.toLatin1().toHex(' ').toUpper();
this -> ui -> textEdit -> setText(hexStr);
sendASCII = false;
}
}
void MainWindow::on_pushButton_clear_send_clicked()
{
this -> ui -> textEdit ->clear();
}
void MainWindow::on_pushButton_send_clicked()
{
if(this -> ui -> pushButton_send -> text() == "Send")
{
if(serial.isOpen())
{
if(sendCyclic)
{
sendTimer.start(period);
this -> ui -> pushButton_send -> setText("Stop");
this -> ui -> checkBox_send_cyclic -> setEnabled(false);
this -> ui -> lineEdit_send_period -> setEnabled(false);
}else{
sendSerialData();
}
}
else {
this -> ui -> statusBar -> showMessage("Serial closed, please open first",1000);
}
}
else if(this -> ui -> pushButton_send -> text() == "Stop") {
sendTimer.stop();
this -> ui -> pushButton_send -> setText("Send");
this -> ui -> checkBox_send_cyclic -> setEnabled(true);
this -> ui -> lineEdit_send_period -> setEnabled(true);
}
}
void MainWindow::on_checkBox_send_cyclic_clicked(bool checked)
{
if(checked)
{
if(this->ui->lineEdit_send_period -> text().isEmpty())
{
QMessageBox::information(this,"Waring","Please edit period first");
this -> ui -> checkBox_send_cyclic -> setChecked(false);
}else {
period = this->ui->lineEdit_send_period -> text().toInt();
sendCyclic = true;
}
}else{
sendCyclic = false;
}
}
void MainWindow::sendSerialData()
{
QByteArray sendData;
if(sendASCII)
{
sendData = this -> ui -> textEdit -> toPlainText().toLatin1();
}else{
sendData = QByteArray::fromHex(this -> ui -> textEdit -> toPlainText().toLatin1());
}
TXCounts += serial.write(sendData);
this -> TXLabel . setText(QString::number(TXCounts));
}
void MainWindow::on_pushButton_read_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this,"Please select file","./","TXT(*.txt)");
QFile sendFile(fileName);
if(!sendFile.open(QIODevice::ReadOnly))
QMessageBox::warning(this,"Error","File open failed, please try again");
QString sendStr = sendFile.readAll();
this -> ui -> textEdit -> setText(sendStr);
}
3. 软件测试
3.1 开发测试
在Windows上开发过程中,需要测试又没有串口设备时,可以使用VSPD虚拟串口软件来模拟,添加COM1和COM2相连,就可方便的验证功能了。

3.2 跨平台测试
在树莓派4B上进行测试,系统Raspbian。
树莓派安装Qt
1. pi@raspberrypi:~ $ sudo apt-get update
2. pi@raspberrypi:~ $ sudo apt-get install qt5-default
3. pi@raspberrypi:~ $ sudo apt-get install qtcreator
4. pi@raspberrypi:~ $ sudo apt-get install qtmultimedia5-dev
5. pi@raspberrypi:~ $ sudo apt-get install libqt5serialport5-dev
跨平台测试,功能完全OK。
4. 其他
4.1 源码
4.2 参考
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)