实现串口调试助手的环境为 win7 32位,先来看下最终的实现效果吧!
下面逐步介绍串口调试助手的各部分代码实现及部分说明:
新建一个基于QMainWindow的类,这里没有应用Qt的设计师,而是用代码的方式实现界面(本人新手,为熟悉下Qt,达到练手的目的),本身由于Qt对串口的支持,这里代码并不多,全部放到了一个类中完成(高手见笑了),类如下:
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
// 界面布局实现控件
private:
QStatusBar *statusBar; // 状态栏
QVBoxLayout *mainLayout; // 整体布局 上下两部分
// 上半部分左部分 参数设置,收发接收状态。
QHBoxLayout * upLayout; // 上半部分布局
QVBoxLayout * upLeftLayout; // 上部分的 左部分布局
QGroupBox *leftUpGroupBox; // 参数设置项
QGridLayout *leftUpLayout;
QLabel *port; // 端口
QComboBox *portBox;
QLabel *baudRate; // 波特率
QComboBox *baudRateBox;
QLabel *dataBits; // 数据位
QComboBox *dataBitsBox;
QLabel *parity; // 校验位
QComboBox *parityBox;
QLabel *stopBits; // 停止位
QComboBox *stopBitsBox;
QPushButton *openSerial; // 打开串口
QPushButton *closeSerial; // 关闭串口
QGridLayout *leftBottomLayout;
QPushButton *saveFile; // 保存数据
QPushButton *stopSave; // 保存数据
QLineEdit *savePath;
QPushButton *clearShow; // 清空接收区
QLabel *receive;
QLabel *receiveValue;
QLabel *send;
QLabel *sendValue;
// 上半部分 右半部分的设置。
QTextEdit * textShow;
// 下半部分 发送状态1
QVBoxLayout *bottomlayout;
QHBoxLayout *send1Layout;
QPushButton *clearSend1;
QPushButton *send1;
QTextEdit *sendText1;
// 下半部分 发送状态2
QHBoxLayout *send2Layout;
QPushButton *clearSend2;
QPushButton *send2;
QTextEdit *sendText2;
// ///////////////////////////////////////
private:
QString saveFileName; // 文件保存名字。
bool bSaveFile = false; // 是否启用保存标志
void SaveToTxt(QString string);
QSerialPort *mySerialPort;
QByteArray rcvData;
QByteArray sndData;
int recvNum;
int sendNum;
QTimer *timer;
// ///////////////////////////////////////
protected slots:
void OnOpenSerialClicked(); //
void OnCloseSerialClicked();
void OnSaveFileClicked();
void OnStopSaveClicked();
void OnClearShowClicked();
void OnClearSend1Clicked();
void OnSend1Clicked();
void OnClearSend2Clicked();
void OnSend2Clicked();
void OnSerialRcvUpdate();
};
上面的代码主要分为三部分,各部分之间用“//////////////”这样的一行隔开。
第一部分是界面实现需要的各种控件,包括按钮、布局、显示和输入控件。
第二部分是运行串口需要的一些变量
第三部分是运行中按钮的一些槽
需要说明的是,所实现的串口的接收发送记录的显示区域使用的控件为QTextEdit,将其属性设置为只读的形式即可,另外串口的两个发送区域没有用QLineEdit,而是用QTextEdit来实现的,主要是考虑到输入的发送内容可能会很多,另外也是为了和显示区域保持一致,只用一种控件,简单些。
下面看下构造函数的实现:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QStringList list;
// 左上半部分,参数设置部分
port = new QLabel(tr("端 口:"));
portBox = new QComboBox;
list = AvailablePorts();
portBox->addItems(list);
baudRate = new QLabel(tr("波特率:"));
baudRateBox = new QComboBox;
list.clear();
list <<"1200" <<"2400" <<"4800" <<"9600" <<"19200" <<"38400" <<"57600" <<"115200";
baudRateBox->addItems(list);
baudRateBox->setCurrentText("9600");
dataBits = new QLabel(tr("数据位:"));
dataBitsBox = new QComboBox;
list.clear();
list <<"5" <<"6" << "7" <<"8";
dataBitsBox->addItems(list);
dataBitsBox->setCurrentText("8");
parity = new QLabel(tr("校验位:"));
parityBox = new QComboBox;
list.clear();
list <<"No" <<"Even" <<"Odd";
parityBox->addItems(list);
parityBox->setCurrentText("Even");
stopBits = new QLabel(tr("停止位:"));
stopBitsBox = new QComboBox;
list.clear();
list<<"1" <<"1.5" <<"2";
stopBitsBox->addItems(list);
stopBitsBox->setCurrentText("1");
openSerial = new QPushButton(tr("打开串口"));
connect(openSerial, SIGNAL(clicked()), this, SLOT(OnOpenSerialClicked()));
openSerial->setToolTip(tr("打开串口"));
openSerial->setToolTipDuration(2000);
openSerial->setDefault(true);
openSerial->setEnabled(true);
openSerial->setStatusTip(tr("打开串口"));
closeSerial = new QPushButton(tr("关闭串口"));
connect(closeSerial, SIGNAL(clicked()), this, SLOT(OnCloseSerialClicked()));
closeSerial->setToolTip(tr("关闭串口"));
closeSerial->setToolTipDuration(2000);
closeSerial->setDisabled(true);
closeSerial->setStatusTip(tr("关闭串口"));
leftUpLayout = new QGridLayout;
leftUpLayout->addWidget(port, 0, 0);
leftUpLayout->addWidget(portBox, 0, 1);
leftUpLayout->addWidget(baudRate, 1, 0);
leftUpLayout->addWidget(baudRateBox, 1, 1);
leftUpLayout->addWidget(dataBits, 2, 0);
leftUpLayout->addWidget(dataBitsBox, 2, 1);
leftUpLayout->addWidget(parity, 3, 0);
leftUpLayout->addWidget(parityBox, 3, 1);
leftUpLayout->addWidget(stopBits, 4, 0);
leftUpLayout->addWidget(stopBitsBox, 4, 1);
leftUpLayout->addWidget(openSerial, 5, 0);
leftUpLayout->addWidget(closeSerial, 5, 1);
leftUpLayout->setColumnStretch(0, 1); // 设置列的比例为1:1
leftUpLayout->setColumnStretch(1, 1);
leftUpGroupBox = new QGroupBox(tr("参数设置"));
leftUpGroupBox->setLayout(leftUpLayout);
leftUpGroupBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
//leftUpGroupBox->setFlat(true);
// 上 左半部分下部,接收参数设置
saveFile = new QPushButton(tr("保存数据"));
saveFile->setStatusTip(tr("保存数据到TXT文档"));
connect(saveFile, SIGNAL(clicked()), this, SLOT(OnSaveFileClicked()));
stopSave = new QPushButton(tr("停止保存"));
stopSave->setDisabled(true);
stopSave->setStatusTip(tr("停止保存数据"));
connect(stopSave, SIGNAL(clicked()), this, SLOT(OnStopSaveClicked()));
savePath = new QLineEdit;
savePath->setText("data.txt");
clearShow = new QPushButton(tr("清楚接收区"));
connect(clearShow, SIGNAL(clicked()), this, SLOT(OnClearShowClicked()));
receive = new QLabel(tr("接收数目:"));
receiveValue = new QLabel(tr("0"));
send = new QLabel(tr("发送数目:"));
sendValue = new QLabel(tr("0"));
leftBottomLayout = new QGridLayout;
leftBottomLayout->addWidget(saveFile, 0, 0);
leftBottomLayout->addWidget(stopSave, 0, 1);
leftBottomLayout->addWidget(savePath, 1, 0, 1, 2);
leftBottomLayout->addWidget(clearShow, 2, 0, 1, 2);
leftBottomLayout->addWidget(receive, 3, 0);
leftBottomLayout->addWidget(receiveValue, 3, 1);
leftBottomLayout->addWidget(send, 4, 0);
leftBottomLayout->addWidget(sendValue, 4, 1);
leftBottomLayout->setVerticalSpacing(10);
leftBottomLayout->setColumnStretch(0, 1); // 设置列的比例为1:1
leftBottomLayout->setColumnStretch(1, 1);
leftBottomLayout->setRowStretch(0, 1); // 设置行的比例1:1:1:1:1
leftBottomLayout->setRowStretch(1, 1);
leftBottomLayout->setRowStretch(2, 1);
leftBottomLayout->setRowStretch(3, 1);
leftBottomLayout->setRowStretch(4, 1);
// 左半部分布局
upLeftLayout = new QVBoxLayout;
upLeftLayout->addWidget(leftUpGroupBox, 1);
upLeftLayout->addSpacing(10);
upLeftLayout->addLayout(leftBottomLayout, 1);
//upLeftLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
upLeftLayout->setSpacing(10);
upLeftLayout->addStretch(1);
// 上部分 右半部分显示区域
textShow = new QTextEdit();
textShow->setReadOnly(true);
textShow->setTextColor(QColor(Qt::blue));
textShow->setAcceptRichText(false);
textShow->setUndoRedoEnabled(true);
textShow->setTextInteractionFlags(Qt::TextBrowserInteraction);
textShow->setFocusPolicy(Qt::WheelFocus);
// 若使用setPlaintText();后续插入的字符换都是在这句上面。
textShow->insertPlainText(tr("串口收发记录显示......") + "\n");
// 上部分布局
upLayout = new QHBoxLayout;
upLayout->addLayout(upLeftLayout, 1);
upLayout->addWidget(textShow, 3);
// 下部分
clearSend1 = new QPushButton(tr("清空1"));
connect(clearSend1, SIGNAL(clicked()), this, SLOT(OnClearSend1Clicked()));
send1 = new QPushButton(tr("发送1"));
connect(send1, SIGNAL(clicked()), this, SLOT(OnSend1Clicked()));
send1->setEnabled(false);
sendText1 = new QTextEdit;
sendText1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
sendText1->setFixedHeight(45);
sendText1->setTextColor(QColor(Qt::darkBlue));
//sendText1->setPalette(QPalette(Qt::lightGray));
//sendText1->setTextBackgroundColor(QColor(Qt::darkGray));
sendText1->setAcceptRichText(false);
sendText1->setStatusTip(tr("请以十六进制形式输入......"));
send1Layout = new QHBoxLayout;
send1Layout->addWidget(clearSend1, 1);
send1Layout->addWidget(send1, 1);
send1Layout->addWidget(sendText1, 6);
clearSend2 = new QPushButton(tr("清空2"));
connect(clearSend2, SIGNAL(clicked()), this, SLOT(OnClearSend2Clicked()));
send2 = new QPushButton(tr("发送2"));
connect(send2, SIGNAL(clicked()), this, SLOT(OnSend2Clicked()));
send2->setEnabled(false);
sendText2 = new QTextEdit;
//sendText2->setMaximumHeight((savePath->height())*2 + 2);
sendText2->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
sendText2->setFixedHeight(45);
sendText2->setTextColor(QColor(Qt::magenta));
//sendText2->setTextBackgroundColor(QColor(Qt::darkGray));
//sendText2->setPalette(QPalette(Qt::darkGray));
sendText2->setStatusTip(tr("请以十六进制形式输入......"));
send2Layout = new QHBoxLayout;
send2Layout->addWidget(clearSend2, 1);
send2Layout->addWidget(send2, 1);
send2Layout->addWidget(sendText2, 6);
bottomlayout = new QVBoxLayout;
bottomlayout->addLayout(send1Layout);
bottomlayout->addLayout(send2Layout);
// 总体布局,上下布局
mainLayout = new QVBoxLayout;
mainLayout->addLayout(upLayout, 8);
mainLayout->addLayout(bottomlayout, 1);
mainLayout->setSpacing(10);
//mainLayout->addStretch(1);
// 状态条
statusBar = new QStatusBar();
statusBar->showMessage(tr("提示信息..."));
this->setStatusBar(statusBar);
// 总体布局
QWidget *mainWidget = new QWidget();
mainWidget->setLayout(mainLayout);
this->setCentralWidget(mainWidget);
this->setMinimumSize(800, 500);
this->setWindowTitle("串口调试助手");
}
对于上面的界面的实现,用Qt的设计师实现会更加简单快捷。上面的代码实现界面基本是按照从左到右,从上到下的顺序实现的,最后是总体的布局和状态栏。
对于串口端口的实现利用了AvailablePorts()函数,来实现查询现有可以使用的串口的名称,并将串口端口名称以列表的形式返回,如没有可用串口时,只返回Com1,代码如下:
QStringList AvailablePorts()
{
QStringList list;
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
{
//qDebug() << "Name : " << info.portName();
list << info.portName();
}
if (list.empty() == true)
{
list << "Com1";
}
return list;
}
如利用虚拟串口生成两个串口Com1,Com5,利用上述函数得到的串口列表如下图:
对于串口的其他属性,如波特率、数据位、校验位和停止位的实现使用直接定义好的列表内容实现。
这里要实现的串口功能包括串口的打卡、关闭、接收数据及处理和发送数据处理等。
串口的打开是在“打开按钮”的槽中实现的,实现判断串口是否存在或被占用,如串口空闲就打开串口,否则发出错误警告。
这里设置打开串口后才能使用串口的发送和关闭操作,串口的打开和关闭操作会处理相关的按钮的是能与否。
void MainWindow::OnOpenSerialClicked()
{
mySerialPort = new QSerialPort();
mySerialPort->setPortName(portBox->currentText());
if(mySerialPort->open(QIODevice::ReadWrite)) //设置为打开的状态
{
mySerialPort->setBaudRate(BaudRate(baudRateBox->currentIndex())); //波特率
mySerialPort->setDataBits(DataBits(dataBitsBox->currentIndex()));
mySerialPort->setParity(Parity(parityBox->currentIndex()));
mySerialPort->setStopBits(StopBits(stopBitsBox->currentIndex()));
mySerialPort->setFlowControl(QSerialPort::NoFlowControl);
connect(mySerialPort, SIGNAL(readyRead()), this, SLOT(OnSerialRcvUpdate()));
portBox->setDisabled(true);
baudRateBox->setDisabled(true);
dataBitsBox->setDisabled(true);
parityBox->setDisabled(true);
stopBitsBox->setDisabled(true);
closeSerial->setEnabled(true);
openSerial->setDisabled(true);
send1->setEnabled(true);
send2->setEnabled(true);
}
else
{
QMessageBox::warning(NULL, tr("警告"), tr("串口被占用"), QMessageBox::Retry);
}
}
对于串口波特率等参数的设置,考虑对应控件QComboBox的内容不一定是Qt5串口的参数,如停止位为1.5位时对应的Qt5的串口停止位参数设置值为3,因此这里在QComboBox的选项到串口参数之间转换由准们的函数来实现,停止位的转换函数StopBits()的实现如下:
QSerialPort::StopBits StopBits(int index)
{
QSerialPort::StopBits stopBits = QSerialPort::UnknownStopBits;
Q_ASSERT(index >= 0 && index < 3);
switch (index) {
case 0:
stopBits = QSerialPort::OneStop;
break;
case 1:
stopBits = QSerialPort::OneAndHalfStop;
break;
case 2:
stopBits = QSerialPort::TwoStop;
break;
default:
stopBits = QSerialPort::UnknownStopBits;
break;
}
return stopBits;
}
其他几个参数,如波特率、检验位、数据位长度的实现和上面类似,由输入QComboBox的索引值,输出对应的Qt5串口参数。
串口的关闭比较简单,调用close函数即可,另外是一些控件的使能操作。
void MainWindow::OnCloseSerialClicked()
{
mySerialPort->close(); // 关闭并移除串口
delete mySerialPort;
portBox->setEnabled(true);
baudRateBox->setEnabled(true);
dataBitsBox->setEnabled(true);
parityBox->setEnabled(true);
stopBitsBox->setEnabled(true);
closeSerial->setDisabled(true);
openSerial->setEnabled(true);
send1->setEnabled(false);
send2->setEnabled(false);
}
这里的实现是在发送按钮的槽中,首先读取发送输入内容,检查合法性并转换成十六进制的形式,这里要求输入形式应是16进制的形式。
void MainWindow::OnSend1Clicked()
{
bool checkFlag = true;
QString sendData = sendText1->toPlainText();
sndData = QString2Hex(sendData, checkFlag);
if (checkFlag == true)
{
mySerialPort->write(sndData);
sendNum += sndData.length();
sendValue->setNum(sendNum);
textShow->setTextColor(QColor(Qt::red));
//textShow->insertPlainText("[send]" + sendData + "\n");
textShow->append(tr("发送") + ":" + sendData);
SaveToTxt(tr("发送") + ":" + sendData);
}
else
{
QMessageBox::warning(NULL, tr("输入警告"), tr("请以十六进制格式输入"), QMessageBox::Ok);
}
sndData.clear();
}
void MainWindow::OnClearSend2Clicked()
{
sendText2->clear();
sendNum = 0;
sendValue->setNum(sendNum);
}
这里由QByteArray QString2Hex(QString str, bool &flag)函数将读取的16进制输入的内容转换成字节数组的形式,flag来传递输入是否合法。如输入合法则进行发送,否则发出警告。
在发送之后将发送的内容显示到收发记录显示处,这里对QTextEdit的写入要注意insertPlainText方法、append方法及setPlaintText方法的区别。
QString2Hex的代码实现如下:
//将1-9 a-f字符转化为对应的整数
char ConvertHexChar(char ch)
{
if ((ch >= '0') && (ch <= '9'))
{
return ch-0x30;
}
else if ((ch >= 'A') && (ch <= 'F'))
{
return ch-'A'+10;
}
else if ((ch >= 'a') && (ch <= 'f'))
{
return ch-'a'+10;
}
else
{
return (-1);
}
}
//将字符型进制转化为16进制的字节数组
QByteArray QString2Hex(QString str, bool &flag)
{
QByteArray senddata;
int hexdata,lowhexdata;
int hexdatalen = 0;
int len = str.length();
char lstr,hstr;
senddata.resize(len/2);
for(int i=0; istr[i].toLatin1(); //字符型
if(hstr == ' ')
{
continue;
}
i++;
lstr = str[i].toLatin1();
if(lstr == ' ' || i >= len) // 保证单字符或最后一个是单字符的情况下发送正确。
{
hexdata = 0;
lowhexdata = ConvertHexChar(hstr);
}
else
{
hexdata = ConvertHexChar(hstr);
lowhexdata = ConvertHexChar(lstr);
}
if((hexdata == -1) || (lowhexdata == -1)) // 输入不合法
{
flag = false;
senddata.resize(hexdatalen);
return senddata;
}
else
{
hexdata = hexdata*16 + lowhexdata;
}
senddata[hexdatalen] = (char)hexdata;
hexdatalen++;
}
senddata.resize(hexdatalen);
return senddata;
}
这里的转换代码是网上下载的,并经过自己的修改而来的。
串口的接收是由信号readyRead()触发串口接收处理槽OnSerialRcvUpdate()而实现的,两则的连接在打开串口时确定。接收处理也很简单,调用readAll读取出接收到的数据即可。
void MainWindow::OnSerialRcvUpdate()
{
rcvData = mySerialPort->readAll(); //读取数据
QString rcvBuf;
recvNum += rcvData.length();
receiveValue->setNum(recvNum);
rcvBuf = ShowHex(rcvData);
textShow->setTextColor(QColor(Qt::blue));
//textShow->insertPlainText(rcvBuf + "\n");
textShow->append(tr("接收") + ":" + rcvBuf);
SaveToTxt(tr("接收") + ":" + rcvBuf);
rcvData.clear();
}
注意这里用到了QString ShowHex(QByteArray str)函数,实现如下:
//将接收的一串QByteArray类型的16进制,转化为对应的字符串16进制
QString ShowHex(QByteArray str)
{
QDataStream out(&str,QIODevice::ReadWrite); //将str的数据 读到out里面去
QString buf;
while(!out.atEnd())
{
qint8 outChar = 0;
out >> outChar; //每次一个字节的填充到 outchar
QString str = QString("%1").arg(outChar&0xFF,2,16,QLatin1Char('0')).toUpper() + QString(" "); //2 字符宽度
buf += str;
}
return buf;
}
这个函数也是网上下载的(见笑)。
到此为止,串口的基本功能都已实现,利用虚拟串口工具,新建两个串口,并将其绑定到一起,打开一个现有的串口工具,和自己设计的串口工具进行联调,这样就可以对设计的串口工具进行各方面的收发测试。
这里要实现的是要把串口接收到的课串口发送出去的数据都保存到一个TXT文档中。
可以在保存数据按钮下面的QLineEdit中输入文件名,可以只输入文件名,不带”.txt”,如输入内容为空,默认文件名为“data.txt”。为简便行事,默认将文件保存到运行文件同目录下。
首先看下保存数据按钮触发的槽,该槽中主要是确定文件名,并在文件中保存保存时间,置位保存标志位,这样以后串口收发的数据都会保存到文件中的。实现如下:
void MainWindow::OnSaveFileClicked()
{
saveFileName = savePath->text();
if (saveFileName.length() == 0)
{
saveFileName = "data.txt";
}
else if (saveFileName.length() > 4)
{
if (saveFileName.indexOf(".txt", (saveFileName.length() - 4)) == -1) // 以.txt结尾?
{
saveFileName = saveFileName + ".txt";
}
}
else
{
saveFileName = saveFileName + ".txt";
}
bSaveFile = true;
QDateTime localTime(QDateTime::currentDateTime());
SaveToTxt(localTime.toString("yy-MM-dd hh:mm:ss.zzz") + " Save:");
stopSave->setEnabled(true);
saveFile->setDisabled(true);
savePath->setDisabled(true);
}
void MainWindow::OnStopSaveClicked()
{
bSaveFile = false;
saveFile->setEnabled(true);
savePath->setEnabled(true);
stopSave->setDisabled(true);
}
注意到在串口收发数据处理及上面的保存文件保存时间的都用到了函数void SaveToTxt(QString string),她的作用就是将传递的字符串保存到文件中,当然首先判断是否需要保存。
void MainWindow::SaveToTxt(QString string)
{
if (bSaveFile == false)
{
return;
}
QFile data(saveFileName);
if (data.open(QFile::WriteOnly | QIODevice::Append))
{
QTextStream out(&data);
out<"\r\n";
}
data.close();
}
制作成可执行文件的方法步骤可以参考另一篇文章 ——Qt5生成可执行文件总结
实现串口调试助手的工作似乎没有太大的意义,毕竟网上有很多优秀的同类工具,当需要定制化的串口工具时,普通的工具就很难满足了,这时需要我们自己实现串口的收发机数据处理和界面。如嵌入式需要串口的情况,很多时候需要对特有的协议实现接收、回应、发送等操作。
至此,串口调试助手的工作基本完成,已经有了一个可以正常使用的串口工具了,当然可能有很多不完善的地方,欢迎批评指正。