1. 简述

串口调试助手在调试串口设备是非常适用的一个工具,在Windows有很多很好用的串口调试助手,但是在Linux系统上好像还没找到一个好用的带界面的软件(用命令行调试的工具还是有很多的),但是总感觉没有那么方便使用。因此基于Qt良好的跨平台特性,开发一个跨平台简易的串口调试助手,满足基本的串口调试需求。最后开源分享出来。目前实现的功能有:

  1. 搜索串口设备;
  2. ASCII/HEX接收;
  3. 接收数据保存到txt;
  4. ASCII/HEX发送;
  5. 周期发送;
  6. 读取txt发送;
  7. 收发字节计数;
    源码在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 源码

源码放在github上,也放在CSDN一份。

4.2 参考

1.VSPD使用教程
2.树莓派Qt安装

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐