前言
没用到线程,实现过程比较笨比,凑合看看得了。
正文
首先知道串口助手大概一般都会有三个功能:发送串口数据,接收串口数据,数据解析显示。
也许有的人觉得显示不算,但还是可以拿出来讲讲。
新建工程时脑海里会想到一个串口助手会有什么功能,每个功能需要怎么实现,然后再对应这些需求添加槽函数。
- 找串口
- 打开串口
- 关闭串口
- 收数据
- 发数据
- 延时
- 串口读数据
大概就是这样的。
先做个界面,创建几个label和几个comboBox,分别用来提示是什么端口、波特率… …并用下拉框给选择。
然后下拉框的选项添加可以双击这个组件然后添加(左下角加号)你要的选择项,完事就确定。
波特率这个选项可以自己设定波特率,选就行了,到时候读的时候可以直接读选框里的text内容;C++里有个SerialPort的库,里面的类方法都是写好的,等下看代码。
这几个按键分别对应代码里的:
ui.open_serial
ui.off_serial
ui.query_button
ui.clear_button
ui.push_button
然后根据上面列出来的功能创建几个按键,基本就是打开/关闭/发送串口这三个按键,创建完后按键基本就差不多了,然后可以放两个显示框,就用edittext。
关于这个发送框,记得在ui里头把readonly的勾选给去掉,我这边默认是勾选的,以至于一开始一直无法写入东西… …
到这里界面就布置得差不多了,想要连接按键、显示框这些控件和代码逻辑的关系,可以使用connect,我这里用的全都是connect。有了这些,就表示上位机界面的控件和想要的逻辑连接起来了,连接的信号就是clicked()
,this
表示触发的事件只在本界面发生。
比如:
connect(ui.refreshbtn, SIGNAL(clicked()), this, SLOT(find_port()));//刷新串口的按钮
connect(ui.open_serial, SIGNAL(clicked()), this, SLOT(on_open_port_clicked()));
connect(ui.off_serial, SIGNAL(clicked()), this, SLOT(on_close_port_clicked()));
connect(ui.query_button, SIGNAL(clicked()), this, SLOT(query_data()));//这行不用管,query_data函数里写的是板子数据上报要发送的指令
connect(ui.clear_button, SIGNAL(clicked()), this, SLOT(on_clear_button1_clicked()));
connect(ui.push_button, SIGNAL(clicked()), this, SLOT(on_send_button_clicked()));
比较关键的就是接收串口然后显示到接收框的部分了,因为收到串口数据之后要及时显示到接收框,所以需要定义一定时器连接到读数据的槽函数中。
//数据接收模式
timerserial = new QTimer();
QObject::connect(serialport, &QSerialPort::readyRead, this, &Serial2418::serial_timerstart);
QObject::connect(timerserial, SIGNAL(timeout()), this, SLOT(Read_Data()));
接下来是开始找串口,打开串口和关闭串口这三个功能,首先定义这三个功能的槽函数。
接着完善这三个功能的逻辑,这三个功能可以在网上找到很多参考,像打开串口serialport->*
这里可以根据自己的需要来添加,有代码补全的话会方便很多,总的来说就是多看官网文档,它的这些功能都是封装好的,看懂了直接用就好了。
void Serial2418::find_port()
{
foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts())
{
QSerialPort serial;
serial.setPort(info);
if (serial.open(QIODevice::ReadWrite))
{
ui.com_2->clear();
const auto COMinfos = QSerialPortInfo::availablePorts(); //用auto自动获取变量类型
for (const QSerialPortInfo& COMInfo : COMinfos) //构造一个串口信息,cpp11范围遍历
{
QStringList COMInfo_Str_Analysis;
COMInfo_Str_Analysis << COMInfo.portName()
<< COMInfo.description()
<< COMInfo.manufacturer()
<< COMInfo.serialNumber()
<< COMInfo.systemLocation();
ui.com_2->addItem(COMInfo_Str_Analysis.at(0), COMInfo_Str_Analysis); //添加可用端口号,把包含串口字符信息的list传过去
}
serial.close();
}
}
}
serial.open(QIODevice::ReadWrite)
表示用读写模式去打开IO设备。
这里有一个串口信息对象列表,包括了portName、description、manufacturer等内容,往ui.com_2->addItem
添加列表中可用的端口号。
使用静态函数生成QSerialPortInfo对象列表。列表中的每个QSerialPortInfo对象表示一个串行端口,可以查询端口名称、系统位置、描述和制造商。QSerialPortInfo类还可以用作QSerialPort类的setPort()方法的输入参数。
上面的寻找串口函数也可以用,在实际情况中要调用delete释放端口信息,如下。
Serial2418::~Serial2418()
{
if (serialport->isOpen())
{
serialport->close();
}
delete serialport;
}
如上,关闭上位机的时候记得关闭删除串口。
通过和同事交流发现寻找串口其实只需要创建一个串口列表并把可用串口的信息放进去,然后再遍历端口号就行,就是每次寻找串口之前都要先清空一遍下拉框里的串口名称,不然每次刷新串口或者新加入串口就会添加重复端口名称,并且拔掉串口模块之后刷新串口也不会清除已经下线的串口号。
void Serial2418::find_port()
{
ui.com_2->clear();
QList serialinfo = QSerialPortInfo::availablePorts();
for (int i = 0; i < serialinfo.length(); i++)
{
QString comx = serialinfo[i].portName();
ui.com_2->addItem(comx);
}
}
找到串口之后然后就是打开/关闭串口了。
在这里再利用读写模式打开串口就好了,然后就是设置串口常用的波特率、数据位数、奇偶校验等状态。
//打开串口
void Serial2418::on_open_port_clicked()
{
update();
sleep(100);
//初始化串口
serialport->setPortName(ui.com_2->currentText()); //设置串口名
if (serialport->open(QIODevice::ReadWrite))
{
serialport->setBaudRate(ui.baud_2->currentText().toInt()); //设置波特率
switch (ui.bit->currentIndex()) //设置数据位数
{
case 8:serialport->setDataBits(QSerialPort::Data8); break;
default: break;
}
switch (ui.jiaoyan->currentIndex()) //设置奇偶校验
{
case 0: serialport->setParity(QSerialPort::NoParity); break;
case 1: serialport->setParity(QSerialPort::OddParity); break;
case 2: serialport->setParity(QSerialPort::EvenParity); break;
default: break;
}
switch (ui.stopbit->currentIndex()) //设置停止位
{
case 1: serialport->setStopBits(QSerialPort::OneStop); break;
case 2: serialport->setStopBits(QSerialPort::TwoStop); break;
default: break;
}
serialport->setFlowControl(QSerialPort::NoFlowControl);
// 设置控件可否使用
ui.clear_button->setEnabled(true);
ui.off_serial->setEnabled(true);
ui.query_button->setEnabled(true);
ui.open_serial->setEnabled(false);
ui.push_button->setEnabled(true);
//QMessageBox::information(this, tr("Success"), tr("Open successfully."));
}
else //打开失败提示
{
sleep(100);
QMessageBox::information(this, tr("Error"), tr("Open the failure."));
find_port();//打开失败了要刷新一下串口,有可能串口信息没更新
}
}
//关闭串口
void Serial2418::on_close_port_clicked()
{
Data.START = 0x00;
serialport->clear(); //清空缓存区
serialport->close(); //关闭串口
find_port();//更新串口列表
ui.query_button->setEnabled(false);
ui.open_serial->setEnabled(true);
ui.off_serial->setEnabled(false);
ui.clear_button->setEnabled(true);
ui.push_button->setEnabled(false);
}
上面的关闭串口后本人设定了许多按键都false,直接禁止它们的操作可以防止上位机可能会出现各种奇怪的bug… …
清除数据很简单,只要把相应的几个数据框里的内容用clear掉就好了。
void Serial2418::on_clear_button1_clicked()
{
ui.send_text_window->clear();
ui.receive_text_window->clear();
ui.distedit->clear();
ui.eneredit->clear();
ui.addredit->clear();
ui.maxedit->clear();
ui.minedit->clear();
ui.displayedit->clear();
}
串口设置函数部分已经完成了,接下来就是发送和接收串口传来的数据。写代码的时候发现过一个问题,就是接收数据的时候是一个字节一个字节接收并处理的,比如说我这里的指令是16进制的,发送框发送的是68 41 AA AA AA AA AA AA AA 26 00 00 75 16,没有on_send_button_clicked里的那层转换时,串口接收到的数据就是
36‘6’
38‘8’
34‘4’
31‘1’… …
要把6和8连起来就需要先把他们从ascii变为hex,再定义一个数组,把这些分开的数字分为高位和低位加起来。
//发送数据:hex
void Serial2418::on_send_button_clicked()
{
uint8_t utmp[1024];
uint8_t utmp2[50];
memset(utmp, 0, sizeof(utmp));
memset(utmp2, 0, sizeof(utmp2));
QString sen = ui.send_text_window->toPlainText();
int len = ui.send_text_window->document()->characterCount();
int i, j, k;
double tlen=0;
if (!sen.isEmpty())
{
//if (ui.checkhex->isChecked() == false)//ascii,这一段if也可以用如果想要让串口发送中文的话可以把这段if的注释消去。
//{
// sendBuf = sen.toLocal8Bit();
//}
//else if (ui.checkhex->isChecked() == true)//hex
//{
//if (sen.contains(" ")) sen.remove(QRegularExpression("\\s"));
sen = sen.toLocal8Bit();
j = 0;
tlen = 0;
for (i = 0; i < len-1; i++)//这一段目的是为了把用户输入指令里的空格去掉
{
if (sen[j] != ' ')
{
utmp[i] = (uint8_t)(sen[j].toLatin1());
}
else if (sen[j] == ' ')//判断是否有空格,如果发送的是“68 41 21 43... ...”这样的指令就会把中间的空格忽略掉,如果发送的是“68412143... ...”就不会执行这一段代码
{
j++;
i--;//哈哈
continue;
}
j++;
if (j >= len-1)
{
tlen = i;
break;
}
}
for (i = 0; i < len - 1; i++)
{
utmp[i] = ascii2hex(utmp[i]);//先把他们从ascii变为hex
}
k = 0;
for(i = 0; i < (tlen/2+1); i++)
{
utmp2[k] = (utmp[i * 2 + 0] << 4) | (utmp[i * 2 + 1]);//再定义一个数组,把这些分开的数字分为高位和低位加起来
k++;
}
//sendBuf = sen.toLocal8Bit().toHex().toUpper();
//}
}
QByteArray sentmp((char*)utmp2, (tlen/2+1));
serialport->write(sentmp);
}
int Serial2418::ascii2hex(char ch)//ascii to hex
{
int hex = 0;
if ((ch >= '0') && (ch <= '9'))
{
hex = ch - '0';
}
else if ((ch >= 'A') && (ch <= 'F'))
{
hex = ch - 'A' + 10;
}
else if ((ch >= 'a') && (ch <= 'f'))
{
hex = ch - 'a' + 10;
}
else
{
hex = 0;
}
return hex;
}
void Serial2418::Read_Data()
{
ui.displayedit->clear();
ui.receive_text_window->clear();//这里习惯读数据之前先清空接收框里的上一次数据,按需添加
uint8_t len = 0;
timerserial->stop();//停止定时器
qDebug() << buffer;
//将接收到的数据保存到strbuf这个全局变量里
memcpy(&strbuf,buffer, buffer.length());
//然后用放到shuju_chuli函数里去处理串口接收到的数据
if (buffer.length() != 0)
{
shuju_chuli(strbuf);
}
updateDate();//输出本地时间函数(电脑系统时间)
ui.receive_text_window->append(QString::fromStdString(buffer.toHex().toUpper().constData()));//接收框里输出串口接收到的数据
buffer.clear();//清空串口接收到数据的buffer
}
//串口读数据的周期,单位:秒
void Serial2418::serial_timerstart()
{
//串口读数据周期时间(s),t=单个串口最大字节数/波特率=2062/115200=17.9ms
//本来给的18,现在直接给100
timerserial->start(100);
buffer.append(serialport->readAll());//读串口的所有数据并且把读到的数据累加到buffer上
}
这里的获取系统时间也是抄的CSDN上的代码,个人觉得还挺好懂的,可以自己看看,了解一下然后把他放到自己需要的地方就好了。
void Serial2418::updateDate()
{
// 获取当前系统时间
QDateTime currentTime = QDateTime::currentDateTime();
QString currentTimeString = currentTime.toString(Qt::ISODate);
QString addrtmp = (char*)set_addr;
QString disttmp = QString::number(dist,'f',6);
QString enertmp = QString::number(ener,'f',6);
// 将数据保存到数据结构中
QString dataString = QString("%1,%2,%3,%4") //字符串处理函数 .arg对应前方的%1等占位符位置
.arg(currentTimeString)
.arg(addrtmp)
.arg(disttmp)
.arg(enertmp);
//qDebug() << dataString;
m_data.append(dataString);
if (m_data.size() > 5000) //缓存区最大保存5000组数据,超出5000后会自动停止接收数据
{
serialport->clear(); //清空缓存区
serialport->close(); //关闭串口
QMessageBox::information(this, "Info", "The data cache is too large. Please save the data and reopen the serial port");
//SaveRecvDataFile();
}
ui.receive_text_window->setText(currentTimeString);
std::cout<< m_data.size()<< std::endl;
}
定义串口读数据周期时间
void Serial2418::serial_timerstart()
{
//串口读数据周期时间(s),t=单个串口最大字节数/波特率=2062/115200=17.9ms
//本来给的18,现在直接给100
timerserial->start(100);
buffer.append(serialport->readAll());
}
效果显示
发广播命令、复位、自检等等这些按钮不用管啦… …
第一次写上位机,垃圾得很。
没想到以后只能在这个文章里看到这个布局了,后面修改的时候我搞了个栅格布局… …完了之后界面变得超级难看,这里建议大家搞界面还是简洁一些,如果不是有需求就别设那么多按钮,less is more… …
后记
本文只写了发送串口、串口接收数据并显示到界面上这部分的内容,数据处理这部分的内容并没有详细描述。数据处理这部分内容后续可能会写,可能不会。
参考文章
QT串口调试助手开发教程:上位机接收数据解析数据帧+多通道波形显示+数据保存
qt 串口收发(完整版)
QT实现串口打开和关闭
Qt 串口通信 waitForReadyRead函数与waitForBytesWritten函数导致的内存增长问题记录