快捷搜索:  汽车  科技

qt串口调试工具(Qt实践录串口调试助手)

qt串口调试工具(Qt实践录串口调试助手)- 串口设备自动检测。运行前串口就绪则自动打开。运行中串口插入不会自动打开。运行过程中拔出设备则自动关闭串口。- 收发计数显示及清零。- 串口参数设置。默认115200 N,8,1- 接收区清空,接收区十六进制显示,接收区时间戳。- 发送区清空,十六进制发送,自动追加`\r\n`,定时发送。

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

# 工具特性

## 具体功能

- 具备串口收发功能。

- 串口参数设置。默认115200 N,8,1

- 接收区清空,接收区十六进制显示,接收区时间戳。

- 发送区清空,十六进制发送,自动追加`\r\n`,定时发送。

- 收发计数显示及清零。

- 串口设备自动检测。运行前串口就绪则自动打开。运行中串口插入不会自动打开。运行过程中拔出设备则自动关闭串口。

## 已知 Bug

接收区时间戳显示不完善。

串口发送大量乱码时,程序会崩溃。乱码可能是真的乱码,也可能是波特率错误设置。

## Qt 相关知识

- MainWindow设计。

- Qt串口类。

- 常用控件:按钮、复选框、文本编辑框、控件贴图。应用程序logo。

- Qt 检测设备热插拔(Windows)。

运行结果如图1所示:

qt串口调试工具(Qt实践录串口调试助手)(1)

仓库地址在此: https://github.com/latelee/QtSerialPort。

# 开发过程

## 工程相关

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 <QSerialPort>

#include <QSerialPortInfo>

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 <windows.h>

#include <WinUser.h>

#include <Dbt.h>

#include <devguid.h>

#include <SetupAPI.h>

#include <InitGuid.h>

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;i<sizeof(GUID_DEVINTERFACE_LIST)/sizeof(GUID);i )

{

NotifacationFiler.dbcc_classguid = GUID_DEVINTERFACE_LIST[i];//GetCurrentUSBGUID();//m_usb->GetDriverGUID();

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<MSG*>(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串口调试工具(Qt实践录串口调试助手)(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(" <a href=\"https://www.latelee.org\">技术主页</a> "));

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 中添加该文件:

```

<RCC>

<qresource prefix="/">

<file>images/logo.jpg</file>

<file>images/notopened.ico</file>

<file>images/opened.ico</file>

<file>images/unpinned.bmp</file>

<file>images/pinned.bmp</file>

<file>images/exit.png</file>

</qresource>

</RCC>

```

也可以在 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 的`currentTextChanged`或`currentIndexChanged`事件。如下:

```

// 串口设备直接用文本文字形式即可

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 <QTimer>

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灯、继电器、出厂恢复以及运行态的功能测试验证等操作。由于与本文关联不大,不再展开。

# 代码仓库

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

猜您喜欢: