1.准备工作
使用QT封装的串口通讯类,QSerialPort类中包含了对串口的相关操作,QSerialPortInfo类中包含了串口的相关信息,首先将这两个类包含到头文件中,另外需要在项目的.pro文件中添加serialport库,如下所示:
#include<QSerialPort>
#include<QSerialPortInfo>
QT +=serialport
2.使用UI设计界面设计窗口
设计好的界面如下所示:
通常我们需要:
一个Combox控件:用于选择串口号。
两个Button控件:一个控制打开串口的按钮,一个发送数据的按钮
两个TextEdit控件:一个显示接收数据(设置为只读属性),一个用于输入要发送的数据
3.相关操作
(1)声明一个QSerialPort类型的变量
QSerialPort MySerialPort; //串口类变量
(2)初始化Combox控件的内容
在窗口构造函数中,我们利用QSerialPortInfo::availablePorts()函数检测当前可用的串口,函数的返回值为QSerialPortInfo类型的变量,并将其串口名称添加到显示串口号的Combox控件中,代码如下:
ui->SerialPortComb->clear();
foreach(const QSerialPortInfo info,QSerialPortInfo::availablePorts())
{
ui->SerialPortComb->addItem(info.portName());
}
(3)打开串口
在UI设计时,添加打开串口按钮点击时响应的槽函数,在该函数中完成串口参数的设置,并且打开串口,相关的操作如下:
MySerialPort.setPortName(ui->SerialPortComb->currentText());
MySerialPort.setBaudRate(QSerialPort::Baud9600);
MySerialPort.setDataBits(QSerialPort::Data8);
MySerialPort.setStopBits(QSerialPort::OneStop);
MySerialPort.setParity(QSerialPort::NoParity);
MySerialPort.setFlowControl(QSerialPort::NoFlowControl);
-
设置串口号:从Combox下拉选择框中获取当前的串口号,使用setPortName()函数
-
设置波特率:使用setBaudRate()函数设置波特率为9600,输入参数为波特率大小,是一个枚举型变量
-
设置数据位:使用setDataBits()函数设置数据位有8位,输入参数为数据位大小,是一个枚举型变量
-
设置停止位:使用setStopBits()函数设置有1位停止位,输入参数为停止位大小,是一个枚举型变量
-
设置奇偶校验:使用setParity()函数设置奇偶校验为无校验,输入参数同样是一个枚举型变量
-
设置流控制:使用setFlowControl()设置为不使用流控制,输入参数同样是一个枚举型变量
相关枚举变量如下所示:
波特率参数有:
enum BaudRate {
Baud1200 = 1200,
Baud2400 = 2400,
Baud4800 = 4800,
Baud9600 = 9600,
Baud19200 = 19200,
Baud38400 = 38400,
Baud57600 = 57600,
Baud115200 = 115200,
UnknownBaud = -1
};
Q_ENUM(BaudRate)
数据位的参数有:
enum DataBits {
Data5 = 5,
Data6 = 6,
Data7 = 7,
Data8 = 8,
UnknownDataBits = -1
};
Q_ENUM(DataBits)
停止位的参数有:
enum StopBits {
OneStop = 1,
OneAndHalfStop = 3,
TwoStop = 2,
UnknownStopBits = -1
};
Q_ENUM(StopBits)
奇偶校验的参数有:
enum Parity {
NoParity = 0,
EvenParity = 2,
OddParity = 3,
SpaceParity = 4,
MarkParity = 5,
UnknownParity = -1
};
Q_ENUM(Parity)
流控制的参数有:
enum FlowControl {
NoFlowControl,
HardwareControl,
SoftwareControl,
UnknownFlowControl = -1
};
Q_ENUM(FlowControl)
接下来使用open()函数打开串口,设置通信模式为能读能写,并且判断是否成功打开串口,如果没有,则弹出错误对话框进行说明,代码如下:
if(!MySerialPort.open(QIODevice::ReadWrite))
{
QMessageBox::critical(NULL,QString("SerialPort Error"),QString("Can't Open Serial Port"));
return;
}
最后,QSerialPort::readyRead信号和ReadSlot()槽连接起来,如果串口的数据缓冲区一有数据(即收到新数据),就会发出readyRead()信号,我们在槽函数中对缓冲区中的数据进行读取和处理,代码如下:
QObject::connect(&MySerialPort,&QSerialPort::readyRead,this,&SerialDlg::ReadSlot);
(4)关闭串口
首先判断串口是否已经打开,如果打开了,则使用close()函数关闭,并且清空接收和发送数据的TexTEdit控件内容
if(MySerialPort.isOpen())
{
MySerialPort.close();
}
ui->ReceiveEdt->clear();
ui->SendEdit->clear();
(5)编写发送数据的函数
在发送数据按钮的点击信号槽函数中使用QSerialPort类的write()函数发送数据,输入参数为QByteArray类型的变量,因此我们首先获取到编辑框中的内容,获得的内容是QString变量,最后使用toUtf8()函数将QString类型转化为QByteArray类型发送即可,代码如下:
if(MySerialPort.isOpen())
{
QString Str=ui->SendEdit->toPlainText();
MySerialPort.write(Str.toUtf8());
}
else
QMessageBox(QMessageBox::Icon::Critical,QString("SerialPort Error"),QString("Serial Port isn't open"));
(6)编写读取数据的槽函数
使用QSerialPort类的readAll()函数可以读取到当前串口缓冲区中的所有数据,并返回一个QByteArray类型的变量,然后我们可以使用QString()函数将其转化为QString类型,最后显示在编辑框中,代码如下,其中Buffer为自己定义的一个QByteArray类型的变量。
Buffer.clear();
Buffer=MySerialPort.readAll();
QString Str=ui->ReceiveEdt->toPlainText();
Str+=QString(Buffer);
Str+=QString("\n");
ui->ReceiveEdt->clear();
ui->ReceiveEdt->append(Str);
(7)重写关闭窗口的事件函数
通过重写关闭窗口的事件函数,可以在关闭窗口前确认串口已经关闭,如果没有则关闭,如果有些自己定义的指针,也可以在此处进行delete。
if(MySerialPort.isOpen())
{
MySerialPort.close();
}
4 数据通信格式
QSerialPort类的读写函数输入的变量都是QByteArray类型的,所以说我们要发送int,float等类型的数据需要将其转化为QByteArray类型再进行收发,另外,我们在测控程序中,通常发送的数据时一帧一帧的,为了确保数据的准确性,每一帧数据包含帧头、帧尾、校验位等等,发送和接收这样的数据就需要为其设计解析的代码,在这里也进行举例说明。
假设我们定义的数据帧格式如下所示:
typedef
struct SerialPortDataStruct //串口通讯结构体
{
unsigned char iHeader; //帧头
unsigned char Length; //有效数据长度
int data; //数据
unsigned char checksum; //检验和
unsigned char iTailer; //帧尾
}SerialPortData;
设计到的宏定义如下所示:
#define DATASIZE 4 //有效数据长度
#define HEADER (char)0xAA
#define TAILER (char)0xEF
数据发送:
因为这里的数据只有一个int类型,所以数据长度始终定义为4个字节。
我们在发送一帧数据时,首先要定义一个结构体变量,并为其帧头、帧尾、 数据、有效数据长度赋值,使用for循环计算出校验和的大小,最后将其拷贝到一个QByteArray类型的变量中进行发送。代码如下:
SerialPortData SendData;
unsigned char checksum=0;
QByteArray tempdata(sizeof(SerialPortData),'\0');
SendData.iHeader=HEADER;
SendData.iTailer=TAILER;
SendData.Length=DATASIZE;
SendData.data=0xabcdef12;
SendData.checksum=0;
memcpy(tempdata.data(),&SendData,sizeof(SendData));
for (size_t i=4;i<8;i++) {
checksum+=tempdata.at(i);
}
SendData.checksum=checksum;
memcpy(tempdata.data(),&SendData,sizeof(SendData));
MySerialPort.write(tempdata);
这里使用内存拷贝函数memcpy函数直接将结构体变量SendData中的内容全部逐字节复制到QByteArray变量tempdata中,使用拷贝之前必须先初始化tempdata变量,变量的大小和结构体的字节数一样,初始值均为0,这里我们自定义的结构体大小是12个字节,多于实际上以为的8个字节,这是因为结构体的对齐原则导致的,我们将在后续说明。
数据接收:
我们在接收到这一帧数据以后,对其解析包括:首先判断收到的数据大小是否和结构体的大小一致,其次判断帧头和帧尾和数据大小是否一致,如果均一致,截取数据段,计算校验和是否与其一致,如果一致,提取出数据进行操作,这里的操作是将其显示出来,代码如下:
Buffer.clear();
Buffer=MySerialPort.readAll();
if (Buffer.size() == sizeof(SerialPortData))
{
if (Buffer.at(0) == HEADER && Buffer.at(1) == DATASIZE && Buffer.at(9) == TAILER)
{
//计算校验和
unsigned char sum = 0;
for (size_t i = 4; i < 8; i++)
{
sum += Buffer.at(i);
}
if (Buffer.at(8)== (char)sum)
{
//对数据进行操作,这里是显示在编辑框中
SerialPortData *recivedata=new SerialPortData;
memcpy(recivedata,Buffer,Buffer.size());
QString Str=ui->ReceiveEdt->toPlainText();
Str+=QString("成功收到数据:%1").arg(recivedata->data);
Str+=QString("\n");
ui->ReceiveEdt->clear();
ui->ReceiveEdt->append(Str);
delete recivedata;
}
}
}
Buffer.at(0) 中存放的是帧头,Buffer.at(1)存放的是数据长度, Buffer.at(9)存放的是帧尾,Buffer.at(4)~Buffer.at(8)存放的是int类型的数据,我们是如何判断出怎么存放的呢,比较简单的方法是我们自己写一小段程序,将每一个字节打印在控制台上观察出来,当然我们也可以根据结构体的对齐原则得到,结构体的对齐原则如下:
/原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。
从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,
因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)/
/原则二:结构体的总大小,也就是sizeof的结果,必须是内部最大成员的整数倍,不足的要补齐。/
/原则三:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素
大小的整数倍地址开始存储。(struct a 里有struct b里有char int double 等元素那b应该从8的整数
倍开始存储)/
对于SerialPortData结构体而言,unsigned char类型占一个字节,依据原则一,iHeader和Length依次存放在前两个字节,即及字节0和字节1处,而int变量存放的地址必须是他的宽度4的整数倍,因此,data变量会从第四个字节开始存放,并且占四个字节,因此,checksum变量会存放在第8个字节处,iTailer变量存放在第9个字节处,所有变量都已经存放完了,但是依据原则二,结构体的总大小必须是内部最大成员的整数倍,也就是4的整数倍,因此需要补两个字节,最终结构体的大小是12个字节,存放示意图如下所示:
5 关于串口通信方式的说明
上述的编程方法的通信方式是异步通信,当调用write()函数发送数据时,函数会立即返回,数据内容将在随后发送出去,读取数据时同理,当串口缓冲区有数据时,会发出readyRead,我们通过自定义的槽函数与其连接,就能够及时读取数据。
实际上,QSerialPort类为我们提供了waitForReadyRead()函数和waitForBytesWritten()函数,可以实现同步阻塞的方式,发送数据时先调用write()函数,然后接着使用while循环判断waitForBytesWritten()函数的返回值,只有该函数返回了,才会执行下一步操作,否则始终卡在while循环中等待数据发送完成。同步接收数据时,无需连接信号和槽函数,我们使用while循环判断waitForReadyRead()的返回值就可以一直等待串口收到的数据,直到收到数据,跳出while循环了,我们再在后续的代码中使用readAll()函数读取数据。
注意:同步阻塞的方式在等待过程中会导致用户界面卡死,不能响应鼠标和键盘等其他事件,如果非要使用,建议创建线程,在独立的线程中进行数据的接收和发送操作。