Qt实践录:串口调试助手

由于项目需要使用到串口调试及测试,为了练手,使用 Qt 编写一个串口调试助手。本文按开发的过程进行简单介绍,同时也涉及部分用到的模块代码。详细代码参考源码仓库。

工具特性

具体功能

  • 具备串口收发功能。
  • 串口参数设置。默认115200,N,8,1
  • 接收区清空,接收区十六进制显示,接收区时间戳。
  • 发送区清空,十六进制发送,自动追加\r\n,定时发送。
  • 收发计数显示及清零。
  • 串口设备自动检测。运行前串口就绪则自动打开。运行中串口插入不会自动打开。运行过程中拔出设备则自动关闭串口。

已知 Bug

接收区时间戳显示不完善。
串口发送大量乱码时,程序会崩溃。乱码可能是真的乱码,也可能是波特率错误设置。

Qt 相关知识

  • MainWindow设计。
  • Qt串口类。
  • 常用控件:按钮、复选框、文本编辑框、控件贴图。应用程序logo。
  • Qt 检测设备热插拔(Windows)。

运行结果如图1所示:
Qt实践录:串口调试助手_第1张图片
图1

开发过程

工程相关

Qt 使用的串口类为QSerialPort,需要在工程文件中添加对应的库,如下:

QT       += core gui serialport

logo图标,注意是 ico 格式:

RC_ICONS = images/logo.ico

图片资源文件:

RESOURCES += \
    images.qrc

USB 设备检测依赖的库:

win32: LIBS += -lSetupAPI -luser32

信号与槽

在 Qt Creator 中添加的控件,可点击控件右键,选择“转到槽…”,选择适合的槽,点击“OK”,可自动添加槽函数声明,并自动跳转到槽函数实现代码。系统自动添加的形式为on_<控件对象名>_<控件事件名>,如打开串口的按钮单击事件槽函数为on_btnOpen_clicked。类似有on_cbPortName_currentTextChanged(串口设备更改)、on_ckRecvHex_stateChanged(接收十六进制复选框变更),等等。
此机制及操作方式,可类比于 MFC 的界面设计和消息响应。实际上,笔者喜欢将槽函数称为响应函数。

串口相关

串口类声明:

#include 
#include 

QSerialPort serial;

枚举本机串口设备:

QSerialPortInfo::availablePorts()

串口参数设置:

serial.setPortName("com4"); // 串口名称
serial.setBaudRate(115200); // 串口波特率
serial.setDataBits(QSerialPort::Data8); // 数据位
serial.setStopBits(QSerialPort::OneStop); // 停止位
serial.setParity(QSerialPort::NoParity); // 校验位
serial.setFlowControl(QSerialPort::NoFlowControl); // 流控

注:Qt 似乎只有无流控、软件流控、硬件流控这三种,无法区分 RTS、DTR。

串口打开、关闭:

serial.open(QIODevice::ReadWrite);
serial.close();

串口数据发送:

 QByteArray sendData;
 serial.write(sendData);

串口数据接收:

// 串口数据到来时,会触发QSerialPort::readyRead事件,添加相应的响应函数
QObject::connect(&serial, &QSerialPort::readyRead, this, &MainWindow::readyRead);

void MainWindow::readyRead()
{
    QByteArray buffer = serial.readAll();
}

注意,串口数据类型为QByteArray

自动检测 USB

鉴于目前大部分场合使用的是 USB 串口线,所以添加对 USB 设备的检测。这里检测到 USB 设备时,再使用QSerialPortInfo::availablePorts()检测串口设备。

#include 
#include 
#include 
#include 
#include 
#include 

    static const GUID GUID_DEVINTERFACE_LIST[] =
    {
    // GUID_DEVINTERFACE_USB_DEVICE
    { 0xA5DCBF10, 0x6530, 0x11D2, { 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } },
    // GUID_DEVINTERFACE_DISK
    { 0x53f56307, 0xb6bf, 0x11d0, { 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } },
    // GUID_DEVINTERFACE_HID,
    { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } },
    // GUID_NDIS_LAN_CLASS
    { 0xad498944, 0x762f, 0x11d0, { 0x8d, 0xcb, 0x00, 0xc0, 0x4f, 0xc3, 0x35, 0x8c } }
    //// GUID_DEVINTERFACE_COMPORT
    //{ 0x86e0d1e0, 0x8089, 0x11d0, { 0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73 } },
    //// GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR
    //{ 0x4D36E978, 0xE325, 0x11CE, { 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 } },
    //// GUID_DEVINTERFACE_PARALLEL
    //{ 0x97F76EF0, 0xF883, 0x11D0, { 0xAF, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x84, 0x5C } },
    //// GUID_DEVINTERFACE_PARCLASS
    //{ 0x811FC6A5, 0xF728, 0x11D0, { 0xA5, 0x37, 0x00, 0x00, 0xF8, 0x75, 0x3E, 0xD1 } }
    };

    //注册插拔事件
    HDEVNOTIFY hDevNotify;
    DEV_BROADCAST_DEVICEINTERFACE NotifacationFiler;
    ZeroMemory(&NotifacationFiler,sizeof(DEV_BROADCAST_DEVICEINTERFACE));
    NotifacationFiler.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
    NotifacationFiler.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;

    for(int i=0;iGetDriverGUID();

        hDevNotify = RegisterDeviceNotification((HANDLE)this->winId(),&NotifacationFiler,DEVICE_NOTIFY_WINDOW_HANDLE);
        if(!hDevNotify)
        {
            DWORD Err = GetLastError();
        }
    }

响应nativeEvent事件:

bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
    MSG* msg = reinterpret_cast(message);
    int msgType = msg->message;
    if(msgType==WM_DEVICECHANGE) // 设备插入事件
    {
        PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)msg->lParam;
        switch (msg->wParam) {
        case DBT_DEVICEARRIVAL:
            if(lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
            {
                //PDEV_BROADCAST_DEVICEINTERFACE pDevInf  = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;
                //QString strname = QString::fromWCharArray(pDevInf->dbcc_name,pDevInf->dbcc_size);
                //qDebug() << "arrive" + strname;
                printDebugInfo("USB device arrive");
                emit sig_deviceChanged(1);
            }
            break;
        case DBT_DEVICEREMOVECOMPLETE:  // 设备移除事件
            if(lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
            {
                //PDEV_BROADCAST_DEVICEINTERFACE pDevInf  = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;
                //QString strname = QString::fromWCharArray(pDevInf->dbcc_name,pDevInf->dbcc_size);
                printDebugInfo("USB device removed");
                emit sig_deviceChanged(0);
            }
            break;
        }
    }
    return false;
}

这里使用自定义的信号sig_deviceChanged,连接到函数on_deviceChanged

QObject::connect(this, &MainWindow::sig_deviceChanged, this, &MainWindow::on_deviceChanged);

void MainWindow::on_deviceChanged(int flag)
{
    if (flag == 1)
    {
        foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
        {
            if (-1 == ui->cbPortName->findText(info.portName()))
                ui->cbPortName->addItem(info.portName());
        }
    }
    else
    {
        serial.close();
        ui->btnOpen->setText(tr("打开串口"));
        ui->btnOpen->setIcon(QIcon(":images/notopened.ico"));
    }
}

界面逻辑

界面设计

界面使用设计师进行设计,如图2所示。
Qt实践录:串口调试助手_第2张图片
图2

界面基本设置

initMainWindow函数中对窗口进行基本设置,如标题、窗口大小,最小化最大化按钮,等等。

// 对主窗口的初始化
void MainWindow::initMainWindow()
{
    setWindowTitle(tr("QtSerialPort"));
    setMinimumSize(480, 320);

    Qt::WindowFlags winFlags  = Qt::Dialog;
    winFlags = winFlags | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;

    setWindowFlags(winFlags);
}

状态栏

状态栏主要用于调试信息、提示信息的显示,另外显示收发计数(及清零)。
状态栏相关函数和变量声明如下:

    void initStatusBar();

    int m_rxCnt;
    int m_txCnt;

    // 状态栏相关
    QLabel* m_stsPinned;
    QLabel* m_stsDebugInfo;
    QLabel* m_stsRx;
    QLabel* m_stsTx;
    QLabel* m_stsResetCnt;
    QLabel* m_stsCopyright;

    QLabel* m_stsExit;

初始化函数如下:

void MainWindow::initStatusBar()
{
    // 状态栏分别为:
    // 提示信息(可多个)
    // RX、TX
    // 版本信息(或版权声明)
    // 退出图标
    ui->statusbar->setMinimumHeight(22);
    //ui->statusbar->setStyleSheet(QString("QStatusBar::item{border: 0px}")); // 不显示边框
    ui->statusbar->setSizeGripEnabled(false);//去掉状态栏右下角的三角

    m_stsDebugInfo = new QLabel();
    m_stsRx = new QLabel();
    m_stsTx = new QLabel();
    m_stsResetCnt = new QLabel();
    m_stsCopyright = new QLabel();
    m_stsExit = new QLabel();

    m_stsDebugInfo->setMinimumWidth(this->width()/2);
    ui->statusbar->addWidget(m_stsDebugInfo);

    m_stsRx->setMinimumWidth(64);
    ui->statusbar->addWidget(m_stsRx);
    m_stsRx->setText("RX: 0");

    m_stsTx->setMinimumWidth(64);
    ui->statusbar->addWidget(m_stsTx);
    m_stsTx->setText("TX: 0");

    m_stsResetCnt->installEventFilter(this);
    m_stsResetCnt->setFrameStyle(QFrame::Plain);
    m_stsResetCnt->setText("复位计数");
    m_stsResetCnt->setMinimumWidth(32);
    ui->statusbar->addWidget(m_stsResetCnt);

    printDebugInfo("欢迎使用");
    // 版权信息
    m_stsCopyright->setFrameStyle(QFrame::NoFrame);
    m_stsCopyright->setText(tr("  技术主页  "));
    m_stsCopyright->setOpenExternalLinks(true);
    ui->statusbar->addPermanentWidget(m_stsCopyright);

    // 退出图标
    m_stsExit->installEventFilter(this); // 安装事件过滤,以便获取其单击事件
    m_stsExit->setToolTip("Exit App");
    m_stsExit->setMinimumWidth(32);
    // 贴图
    QPixmap exitIcon(":/images/exit.png");
    m_stsExit->setPixmap(exitIcon);
    ui->statusbar->addPermanentWidget(m_stsExit);

    connect(this, &MainWindow::sig_exit, qApp, &QApplication::quit); // 直接关联到全局的退出槽
}

为响应状态栏控件事件,需要重载并实现函数eventFilter

bool eventFilter(QObject *watched, QEvent *event);


bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_stsExit) // 程序退出
    {
        //判断事件
        if(event->type() == QEvent::MouseButtonPress)
        {
            // TODO:直接退出还是发信号?
            emit sig_exit();
            return true; // 事件处理完毕
        }
        else
        {
            return false;
        }
    }
    else if (watched == m_stsResetCnt)
    {
        if(event->type() == QEvent::MouseButtonPress)
        {
            m_stsRx->setText("RX: 0");
            m_stsTx->setText("TX: 0");
            m_rxCnt = m_txCnt = 0;
            return true;
        }
        else
        {
            return false;
        }
    }
    else
    {
        return QWidget::eventFilter(watched, event);
    }
}

目前处理的事件有单击退出图标,以及点击清零计数QLabel。

控件贴图

新建资源文件 images.qrc,内容如下,再在 Qt Creator 中添加该文件:


    
        images/logo.jpg
        images/notopened.ico
        images/opened.ico
        images/unpinned.bmp
        images/pinned.bmp
        images/exit.png
    

也可以在 Qt Creator 新建资源文件,右键添加图片。效果一样。
以打开串口按钮为例,设置文字和图标代码如下:

    ui->btnOpen->setText(tr("打开串口"));
    ui->btnOpen->setIconSize(ui->btnOpen->rect().size());
    ui->btnOpen->setIcon(QIcon(":images/notopened.ico"));

注:资源文件 qrc 的前缀为/,images为工程下的目录,使用 QIcon的参数形式为:images/xxx

QComboBox

串口参数使用 QComboBox 控件,为方便添加数据,使用 QStringList 类,再利用 addItems 添加数据项。其初始化如下:

    QStringList list;
    list.clear();
    list << "2400" << "4800" << "9600" << "14400" << \
         "19200" << "38400" << "43000" << "57600" << "76800" << \
         "115200" << "230400" << "256000" << "460800" << "921600";
    ui->cbBaudrate->addItems(list);
    ui->cbBaudrate->setCurrentText(tr("115200"));

    list.clear();
    list << "5" << "6" << "7" << "8";
    ui->cbDatabit->addItems(list);
    ui->cbDatabit->setCurrentText(tr("8"));

    list.clear();
    list << "1" << "1.5" << "2";
    ui->cbStopbit->addItems(list);
    ui->cbStopbit->setCurrentText(tr("1"));

    list.clear();
    list << "none" << "odd" << "even";
    ui->cbParity->addItems(list);
    ui->cbParity->setCurrentText(tr("none"));

    list.clear();
    list << "off" << "hardware" << "software";
    ui->cbFlow->addItems(list);
    ui->cbFlow->setCurrentText(tr("off"));

串口参数自动更新

为串口打开时,可以实时更改参数,但串口设备除外。响应 QComboBox 的currentTextChangedcurrentIndexChanged事件。如下:

// 串口设备直接用文本文字形式即可
void MainWindow::on_cbPortName_currentTextChanged(const QString &arg1)
{
    serial.setPortName(arg1);
}

// 如停止位等,需要用索引转换
void MainWindow::on_cbStopbit_currentIndexChanged(int index)
{
    //qDebug()<< index;
    //设置停止位
    switch(index)
    {
    case 0: serial.setStopBits(QSerialPort::OneStop); break;
    case 1: serial.setStopBits(QSerialPort::OneAndHalfStop); break;
    case 2: serial.setStopBits(QSerialPort::TwoStop); break;
    default: break;
    }
}

QCheckBox

复选框用于发送、显示的特性设置。如十六进制发送、显示,定时发送,等等。设计上使用标志变量,在进行发送、显示时加以判断。主要响应 QCheckBox 的 stateChanged函数。

void MainWindow::on_ckRecvHex_stateChanged(int arg1)
{
    if (arg1 == Qt::Checked)
    {
        m_recvHex = 1;
    }
    else if (arg1 == Qt::Unchecked)
    {
        m_recvHex = 0;
    }
}

定时器

重载timerEvent函数:

#include 

void timerEvent(QTimerEvent *event);

函数实现:

void MainWindow::timerEvent(QTimerEvent *event)
{
    //qDebug() << "Timer ID:" << event->timerId();
    sendData();
}

开启、停止定时器:

m_sendTimerId = startTimer(ui->txtInterval->text().toInt());

killTimer(m_sendTimerId);

由于本程序只使用一个定时器,故不用判断event->timerId()

十六进制

为方便调试,工具支持字符、十六进制数据的发送和显示。“十六进制字符串”转字符串等函数集如下:


int hexStringToString(QString& hexStr, QString& str)
{
    int ret = 0;
    bool ok;
    QByteArray retByte;
    hexStr = hexStr.trimmed();
    hexStr = hexStr.simplified();
    QStringList sl = hexStr.split(" ");

    foreach (QString s, sl)
    {
        if(!s.isEmpty())
        {
            char c = (s.toInt(&ok,16))&0xFF;
            if (ok)
            {
                retByte.append(c);
            }
            else
            {
                ret = -1;
            }
        }
    }

    str = retByte;

    return ret;
}

int hexStringToHexArray(QString& hexStr, QByteArray& arr)
{
    int ret = 0;
    bool ok;
    hexStr = hexStr.trimmed();
    hexStr = hexStr.simplified();
    QStringList sl = hexStr.split(" ");

    foreach (QString s, sl)
    {
        if(!s.isEmpty())
        {
            char c = (s.toInt(&ok,16))&0xFF;
            if (ok)
            {
                arr.append(c);
            }
            else
            {
                ret = -1;
            }
        }
    }

    return ret;
}

int hexArrayToString(QByteArray& hexArr, QString& str)
{
    int ret = 0;
    str = hexArr.toHex(' ').toLower();
    return ret;
}

其它

实际上,程序难度不大,特别是串口类的操作,因为QSerialPort提供了十分友好、方便的接口进行串口的设置、收发。如果要说有难度,可能在于界面逻辑的设计。如定时发送与单次发送,控件提示文字和图标显示,十六进制与字符串之间的转换,等等。
笔者在此工具基础上实现了对 ESP8266 的操作,包括指示LED灯、继电器、出厂恢复以及运行态的功能测试验证等操作。由于与本文关联不大,不再展开。

代码仓库

代码以仓库为准,本文不一定全部囊括。本工程所有源码均可自由自主使用,包括但不限于添加、删除、修改,商用、自用。由此带来的成果/后果概与作者无关。限于水平能力,本程序无任何质量保证,本程序作者无提供服务之义务。

仓库地址在此 。

你可能感兴趣的:(Qt)