赞
踩
串口调试助手是一种串口通讯测试工具,它可以用于打开、关闭、配置串口,读写串口数据等常见的串口通信操作。 在嵌入式系统调试、模块测试、通讯协议分析等领域都具有广泛的应用。
串口助手通常提供GUI界面,让用户可以更加方便、直观地进行串口通讯测试和调试。用户可以通过界面上的下拉框和按钮来配置串口参数,打开/关闭串口以及发送和接收串口数据。它还支持16进制显示和发送,方便用户进行二进制数据的调试和测试。
本次设计高仿安信可串口调试助手进行设计与实现相应的功能
实际运行效果UI界面
UI界面整体布局为垂直布局,其中上面为上方为三个groupBox的水平布局接收区、历史记录区以及多文本区。中间部分分为左右两则,左侧为串口参数设置,右边又分为上下两个groupBox的垂直布局。最下方是一栏状态栏标签数据的显示,布局完全模仿安信可串口调试助手设置。
注意: 在布局时候,对于控件的命名尤为重要,方便后期程序的开发与提高辨别度
以下提供种本博文中主要用到的变量命名
QSerialPort类是Qt框架中用于串口通信的类,它允许与串口设备进行通信,如传感器、微控制器、GPS接收器等,可以使用这个类来发送和接收数据,配置传偶参数满足实际通信需求。
以下是一些常见的QSerialPort类的用法和方法:
1. 打开和关闭串口
2. 配置串口参数
3. 读取和写入数据
4. 信号与槽
QSerialPort类提供了一些信号,如readyRead
,用于在接收到新数据时发出信号,可以连接这些信号到槽函数以处理数据。
5. 错误处理
可以使用error()
方法和errorString()
方法来检测和获取串口通信过程中可能发生的错误。
以下是一个简单的实例代码,演示如何使用QSerialPort类来打开串口,发送和接收数据
#include <QApplication> #include <QDebug> #include <QSerialPort> #include <QSerialPortInfo> int main(int argc, char *argv[]) { QApplication app(argc, argv); //创建一个QSerialPort对象 QSerialPort serialPort; //配置串口参数 serialPort.setPortName("COM1"); serialPort.setBaudRate(QSerialPort::Baud9600); serialPort.setDataBits(QSerialPort::Data8); serialPort.setParity(QSerialPort::NoParity); serialPort.setStopBits(QSerialPort::OneStop); //serialPort.setFlowControl(QSerialPort::NoFlowControl); //尝试打开串口 if(serialPort.open(QIODevice::ReadWrite)) { qDebug() << "串口打开成功"; QByteArray data = "hello world!"; //向串口写数据 serialPort.write(data); /***********这里添加其他代码和事件处理逻辑***********/ //连接readyRead信号与槽函数,接收到数据时候执行槽函数 QObject::connect(&serialPort, &QSerialPort::readyRead, [&](){ //读取所有可用的串口数据 QByteArray receiveData = serialPort.readAll(); qDebug() << "Received data: " << receiveData; }); /***********这里添加其他代码和事件处理逻辑***********/ serialPort.close(); } else { qDebug() << "串口打开失败"; } return app.exce(); }
在这个示例中,首先配置了串口参数,然后打开串口,发送数据,并设置了一个槽函数用来处理接收到的数据,最后,在应用程序的事件循环中运行。注意,需要根据实际串口配置和需求来调整代码
在Qt中使用QSerialPort类来扫描系统中的串口设备非常简单,可以使用QSerialPortInfo
类来获取有关系统中可用串口设备的信息,以下是一个实例代码,演示如何扫描系统中的串口。
#include <QSerialPortInfo> #include <QApplication> #include <QDebug> int main(int argc, char *argv[]) { QApplication app(argc, argv); //提取系统中所有可用的串口信息 QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts(); if(serialList.isEmpty()) { qDebug() << "no serial ports found."; } else { qDebug() << "available ports found."; //遍历打印每个串口的信息 for(QSerialPortInfo serialInfo: serialList) { qDebug() << "port: " << serialInfo.portName(); //端口名 qDebug() << "Description: " << serialInfo.description(); qDebug() << "Manufacturer: " << serialInfo.manufacturer(); qDebug() << "System Location: " << serialInfo.systemLocation(); qDebug() << "Vendor ID: " << serialInfo.vendorIdentifier(); qDebug() << "Product ID: " << serialInfo.productIdentifier(); qDebug() << "-------------------------" << endl; } } return app.exce(); }
上述代码会打印出系统中所有可用串口的信息,包括串口名字、描述、制造商、位置等,可以根据这些信息来选择要与之通信的串口,其中主要用于配置的是串口名字
将扫描到的可用串口添加至comboBox选择框:
for(QSerialPortInfo serialInfo: serialList){
ui->comboBox_sreialNum->addItem(serialInfo.portName());
}
扫描可用串口的核心程序如下所示:
//检测系统可用串口,并添加至comboBox控件 QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts(); if(serialList.isEmpty()) { qDebug() << "no serial ports found."; } else { //qDebug() << "available ports found."; //遍历打印每个串口的信息 for(QSerialPortInfo serialInfo: serialList) { //qDebug() << "port: " << serialInfo.portName(); //端口名 ui->comboBox_sreialNum->addItem(serialInfo.portName()); } }
数据的发送: 通过 write()
方法实现数据的发送,其中返回值为成功发送的字节数
//发送按钮槽函数 void Widget::on_btnSend_clicked() { //从行编辑器中获取待发送的数据,注意类型的转化 const char* sendData = ui->lineEditSendContext->text().toStdString().c_str(); //字符串长最好用以下转化方式 //const char* sendData = ui->lineEditSendContext->text().toLocal8Bit().constData(); int nwrite = serialPort->write(sendData); //获取成功发送字节数 if(nwrite == -1){ ui->labelSendStatus->setText("Send error!"); } else //成功发送数据 { qDebug() << "send ok: " << sendData; /***********这里添加其他代码和事件处理逻辑***********/ } }
数据的接收: 在接收到新数据时发出信号 readyRead
,通过绑定信号与槽实现数据的接收
构造函数内绑定信号与槽:
//绑定发送与接收信号与槽
connect(serialPort, &QSerialPort::readyRead, this, &Widget::on_serialData_readyToRead);
.cpp文件内实现槽函数:
//接收数据槽函数
void Widget::on_serialData_readyToRead()
{
//定义存放数据的变量
QString revMessage = serialPort->readAll();
if(revMessage != NULL )
{
//将数据添加至文本框内
ui->textEditRev->append(revMessage);
qDebug() << "rev: " << revMessage;
}
}
参考博文:Qt扫盲-QTimer定时器理论总结
QTimer 其实就是一个定时器工具类,定时器就是间隔一定时间后,去执行某一个任务,这个在很多场景都会使用的,我经常使用的即是弹窗自动关闭,就是消息框自动关闭之类的功能。
Qt封装的定时器是使用很简单的,QTimer 类为定时器提供了高级编程接口。要使用它,需要创建一个QTimer,将其timeout()信号连接到适当的槽位,然后调用start()。从那时起,它将以固定的时间间隔发出timeout()信号。
通过CheckBox控件来完成定时器的启动与关闭示例:
timer = new QTimer(this); //实例化定时器 void Widget::on_timeSend_clicked(bool checked) { //选中 if(checked) { //qDebug() << "on_timeSend_clicked true"; //启动定时器,并设定定时时间为500ms,定时器每500ms将发出timeout信号 timer->start(1000); } else //取消选中 { //qDebug() << "on_timeSend_clicked false"; timer->stop(); /停止定时 } }
绑定timeout信号与槽函数,即每500ms运行一次槽函数(从单片机角度理解为定时器中断服务函数)
//绑定定时器定时槽函数
connect(timer, &QTimer::timeout, [=](){
//直接调用发送按钮的槽函数,每500ms定时发送数据
on_btnSend_clicked();
});
读取文本框数据 QString
类型
将 QString 类型的数据转化成 HEX 格式
QByteArray
类型,利用 toUtf8()
方法QByteArray
类型转成 HEX 格式,利用 toHex()
方法将 QByteArray
类型的 HEX 格式数据再转为 QString
类型用于在文本编辑器上显示
//HEX显示槽函数 void Widget::on_checkBoxHexShow_clicked(bool checked) { //HEX显示 if(checked) { //1. 读取文本框数据 QString tempStr = ui->textEditRev->toPlainText(); //2. 转化成hex QByteArray tempHex= tempStr.toUtf8(); tempHex = tempHex.toHex(); //3. 显示 QString lastShow; tempStr = QString::fromUtf8(tempHex); //12345678 for(int i = 0; i< tempStr.size(); i += 2) { //空格间隔 12 34 56 78 lastShow += tempStr.mid(i,2) + " "; } ui->textEditRev->setText(lastShow.toUpper()); } else //取消HEX显示 { //1. 读取文本框数据 QString 类型 QString tempStr = ui->textEditRev->toPlainText(); //2. 转化成hex格式 //QString 转成 QByteArray 类型 QByteArray tempUtf8 = tempStr.toUtf8(); // QByteArray类型的HEX格式数据转为字符格式 QByteArray lastShow= QByteArray::fromHex(tempUtf8); //3. 显示 ui->textEditRev->setText(QString::fromUtf8(lastShow)); } }
注意: 因为 QString 类型无法直接转为 HEX 格式,因此需要先将数据转为 QByteArray 类型
if(ui->checkBoxHexSend->isChecked()) //HEX发送 { // 读取发送文本框数据 QString temp = ui->lineEditSendContext->text(); //数据转化为QByteArray类型,用toUtf8方法也可以 QByteArray tempArray = temp.toLocal8Bit(); //判断是否为偶数位 if(tempArray.size() % 2 != 0) { ui->labelSendStatus->setText("error input"); return; } //判断每一位是否符合十六进制 for(char c : tempArray) { if(!std::isxdigit(c)) { ui->labelSendStatus->setText("error input"); return; } } //勾选发送新行选项 if(ui->checkBoxSendNewLine->isChecked()) tempArray.append("\r\n"); //转化成16进制发送 QByteArray arraySend = QByteArray::fromHex(tempArray); nwrite = serialPort->write(arraySend); }
在实际应用中,每次插入串口后,在点击串口选择串口号时应检测当下的可用串口,并将其可用串口添加至选择框中。因此在实现的过程中,需要将选项框提升为自定义控件MycomboBox,检测鼠标左键按下事件,在对应的槽函数中重新检测当前可用的串口号,并将添加至选项框中
自定义控件在记事本项目中有涉及,具体可参考博文:C++ QT入门2——记事本功能实现与优化 中的事件自定义按钮
自定义控件MycomboBox
类的声明.h文件:
//自定义控件继承于QComboBox
class MycomboBox : public QComboBox
{
Q_OBJECT
public:
MycomboBox(QWidget *parent);
//重写鼠标按下事件
protected:
void mousePressEvent(QMouseEvent *e) override;
//自定义鼠标按下事件发送的刷新信号
signals:
void refresh();
};
.cpp文件重写自定义控件:当鼠标左键按下时,发出refresh信号
//构造函数
MycomboBox::MycomboBox(QWidget *parent):QComboBox(parent)
{
}
//重写鼠标移动事件函数
void MycomboBox::mousePressEvent(QMouseEvent *e)
{
//如果检测到左键按下,发出一个refresh信号
if(e->button() == Qt::LeftButton)
emit refresh();
//将事件e传回原来的按下的事件操作,否则将无法选择端口,被自定义控件占用
QComboBox::mousePressEvent(e);
}
注意: 记得需要在UI文件中将控件提升为自定义控件
主界面检测refresh信号,绑定槽函数,当检测到该信号时,重新检测当前可用串口号并添加在选项框中
//绑定信号与槽函数,按下comboBox就检测系统可用串口 connect(ui->comboBox_sreialNum, &MycomboBox::refresh, this, &Widget::refreshSerialName); //槽函数实现刷新当前可用串口号并添加至控件 void Widget::refreshSerialName() { ui->comboBox_sreialNum->clear(); //清空原有串口号 //检测系统可用串口,并添加至comboBox控件 QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts(); if(serialList.isEmpty()) { qDebug() << "no serial ports found."; } else { //qDebug() << "available ports found."; //遍历打印每个串口的信息 for(QSerialPortInfo serialInfo: serialList) { //qDebug() << "port: " << serialInfo.portName(); //端口名 //串口名添加至comboBox控件 ui->comboBox_sreialNum->addItem(serialInfo.portName()); } } ui->labelSendStatus->setText("COM refreshed"); }
参考博文:qt – 获取当前时间 QDateTime、QTime、QDate
时间日期类型:
QDateTime:日期时间数据类型,表示日期和时间,如:2024-01-01 05:20:21
QDate: 日期数据类型,表示日期,如:2024-01-01
QTime: 时间数据类型,表示时间,如:05:20:21
获取日期时间数据类型:
QDateTime dateTime = QDateTime::currentDateTime(); //获取系统当前时间
QString str = dateTime .toString("yyyy-MM-dd hh:mm:ss"); //格式化时间
日期时间数据类 QDateTime
存放方法 date
获取日期类(QDate)对象,存放 time
方法获取时间类(QTime)对象
QDate date = dateTime.date(); //通过date方法获取日期类对象
QTime time = dateTime.time(); //通过time方法获取时间类对象
再通过日期对象的方法 year
,month
,day
来获取系统的年月日数据,通过时间对象的方法 hour
,minute
,second
获取系统时间的时、分、秒数据。
QDateTime dateTime = QDateTime::currentDateTime(); //获取系统当前时间
QDate date = dateTime.date(); //通过date方法获取日期类对象
int year = date.year();
int month = date.month();
int day = date.day();
QTime time = dateTime.time(); //通过time方法获取时间类对象
int hour = time.hour();
int min = time.minute();
int sec = time.second();
最后可以将各个数据封装成一个QString类型的字符串,并格式化位宽、进制以及结束字符
realTime = QString("%1-%2-%3 %4:%5:%6")
.arg(year, 2, 10, QChar('0'))
.arg(month, 2, 10, QChar('0'))
.arg(day, 2, 10, QChar('0'))
.arg(hour, 2, 10, QChar('0'))
.arg(min, 2, 10, QChar('0'))
.arg(sec, 2, 10, QChar('0'));
//数据 位宽 进制 结束字符
在右侧多按钮组的发送中,存在10颗按键,对每个按钮分别定义一个槽函数,用于发送对于行编辑器的内容,在实现功能的基础上程序显得十分冗余,因此解决方案是分别获取10个按钮的名称对象将其存入QPushButton数组中,并通过遍历数组来获取每个按键对象。
每个按钮点击时均发出clicked()信号,因此按钮数组通过绑定同一个槽函数,用于检测clicked()信号,并下标索引来确定具体的按钮及其对应的行编辑器里面的内容
定义私有的成员变量:按钮数组
QList<QPushButton *> btns; //按钮数组
在构造函数内
findChild<>()
函数可以通过控件的名称来获取对应的控件对象//遍历右侧按钮名存入数据
for(int i= 1; i <= 10; i++)
{
//格式化按钮名称 获取每个按键名称
QString btnName = QString("pushButton_%1").arg(i);
//通过控件名称找子控件
QPushButton *btn = findChild<QPushButton *>(btnName);
if(btn)
{
btns.append(btn); //将按钮添加至按钮数组
btn->setProperty("btnID",i); //为每个按钮设置btnID属性
//为当前按钮绑定槽函数
connect(btn, SIGNAL(clicked()), this, SLOT(on_command_btns_clicked()));
}
}
在槽函数中
qobject_cast
,并获取按钮ID属性:property
findChild<>()
根据控件名称获取对应的控件对象void Widget::on_command_btns_clicked() { //查找触发槽函数的信号发送者 QPushButton *btn = qobject_cast<QPushButton *>(sender()); if(btn) { //获取ID 通过ID 关联按钮和文本框等控件 int num = btn->property("btnID").toInt(); //获取ID对应的控件名称 QString lineEditName = QString("lineEdit_%1").arg(num); QString checkBoxName = QString("checkBox_%1").arg(num); //根据控件名称锁定实际的控件对象 通过findChild泛型 QLineEdit *lineEdit = findChild<QLineEdit *>(lineEditName); QCheckBox *checkBox = findChild<QCheckBox *>(checkBoxName); if(lineEdit) { //如果有数据才发送 if(lineEdit->text().size() <= 0) return; ui->lineEditSendContext->setText(lineEdit->text()); } if(checkBox) ui->checkBoxHexSend->setChecked(checkBox->isChecked()); //发送槽函数 发送数据 on_btnSend_clicked(); } }
循环发送的实现可以采用定时器实现,每定时一定的时间,在定时器槽函数遍历行编辑器的内容。行编辑器的控件对象通过编辑器名称获得,而编辑器名称的ID号通过按钮组的下标获得
创建定时器并绑定槽函数:
//实例化循环发送定时器
buttonConTimer = new QTimer(this);
//绑定循环发送定时器溢出与槽函数
connect(buttonConTimer, &QTimer::timeout,this, &Widget::btnCon_Handler);
定时器槽函数: 相当于定时轮流按 按钮组
//循环发送定时器中断函数
void Widget::btnCon_Handler()
{
if(buttonIndex < btns.size())
{
//遍历按钮 发出clicked()信号,相当于定时轮流按按钮组
QPushButton *btn = btns[buttonIndex];
emit btn->clicked();
//成员变量,用于控制按钮组下标
buttonIndex++;
}
else //数组遍历溢出 重置下标
buttonIndex = 0;
}
循环发送选项框槽函数,主要用于控制定时器的开关及定时时间
//循环发送按钮槽函数
void Widget::on_checkBoxSend_clicked(bool checked)
{
if(checked)
{
ui->spinBox->setEnabled(false);
//定时时间从spinBox内获取
buttonConTimer->start(ui->spinBox->text().toInt());
}
else
{
buttonConTimer->stop();
ui->spinBox->setEnabled(true);
}
}
若在循环发送的选项框槽函数内使用UI线程的延时函数,会导致UI线程卡死,并且无法实时发送数据,因此需要创建一个单独的定时发送数据的线程,使得UI线程能正常显示。实际应用中,响应事件若与UI相关则可用定时器解决,若需完全其他任务需有耗时操作,需要使用多线程,在其他线程中尽量不操作UI界面。
本次设计也是通过线程来模拟定时器,每隔1定的时间线程发送一个自定义信号,主函数接收到信号后启动定时器,否则终止线程,类似于实现一个软定时的效果
线程类的定义
class CustomThread : public QThread
{
Q_OBJECT
protected:
void run() override;
public:
CustomThread(QWidget * parent);
signals:
void threadTimeOut();
};
重写 run() 函数
void CustomThread::run()
{
//每隔1秒发送自定义信号
while(true)
{
msleep(1000);
emit threadTimeOut();
}
}
实例化线程并绑定槽函数
注意: 线程时间溢出槽函数与定时器时间溢出槽函数一样,为同一个函数
myThread = new CustomThread(this); //实例化循环发送线程对象
//循环发送线程 信号与槽
connect(myThread, &CustomThread::threadTimeOut, this, &Widget::btnCon_Handler);
当检测到循环发送按钮按下后,只需将原本的启动定时器改为启动线程即可
//循环发送按钮槽函数 void Widget::on_checkBoxSend_clicked(bool checked) { if(checked) { ui->spinBox->setEnabled(false); //buttonConTimer->start(ui->spinBox->text().toInt()); myThread->start(); //启动线程 } else { //buttonConTimer->stop(); myThread->terminate(); //终止线程 //循环发送 ui->spinBox->setEnabled(true); } }
重置功能即清空右侧文本框和选择框的数据
//重置按钮槽函数 void Widget::on_btnInit_clicked() { //消息提示框提示 QMessageBox msgBox; msgBox.setWindowTitle("提示"); msgBox.setText("重置列表不可逆,确认是否重置?"); msgBox.setIcon(QMessageBox::Question); msgBox.addButton("是", QMessageBox::YesRole); msgBox.addButton("否", QMessageBox::NoRole); int ret = msgBox.exec(); //qDebug() << "ret = " << ret; switch (ret) { case 0: //qDebug() << "yes button"; //遍历QLineEdit QPushButton QCheckBox 并清空数据 for(int i = 0; i < lineEdits.size(); i++) lineEdits[i]->clear(); for(int i = 0; i < checkBoxs.size(); i++) checkBoxs[i]->setChecked(false); break; case 1: //qDebug() << "no button"; break; default: qDebug() << "ret default"; break; } }
保存功能即将文本框和选择框的数据保存至txt文件中,方便实现下次读取后直接载入即可
//保存按钮槽函数 void Widget::on_btnSave_clicked() { //创建文本对话框 QString fileName = QFileDialog::getSaveFileName(this, tr("保存文件"), "E:/qtProject/00_QT_CLC/03_USARTSerial/SendList.txt", tr("文本类型 files (*.txt)")); //打开文件 QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; //将文本框内数据保存至文件 QTextStream out(&file); for(int i = 0; i< checkBoxs.size(); i++) { out << checkBoxs[i]->isChecked() << ","; out << lineEdits[i]->text() << endl; } file.close(); }
读取txt文本数据,根据数据直接设置选择框的状态和行编辑器内容
//输入按钮槽函数 void Widget::on_btnLoad_clicked() { int i = 0; //创建文本对话框 QString fileName = QFileDialog::getOpenFileName(this, tr("打开文件"), "E:/qtProject/00_QT_CLC/03_USARTSerial/SendList.txt", tr("文本类型 files (*.txt)")); if(fileName != NULL) { //以只读方式打开文件 QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return; //读取数据 QTextStream in(&file); if(in.atEnd() && i <= 9) { //读取每一行的数 QString line = in.readLine(); //以 , 分割字符串存入parts列表 形如 0,AT+RST QStringList parts = line.split(","); if(parts.count() == 2) { checkBoxs[i]->setChecked(parts[0].toInt()); lineEdits[i]->setText(parts[1]); } i++; } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。