基于QT5的串口通讯编程学习

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()函数读取数据。
注意:同步阻塞的方式在等待过程中会导致用户界面卡死,不能响应鼠标和键盘等其他事件,如果非要使用,建议创建线程,在独立的线程中进行数据的接收和发送操作。

猜你喜欢

转载自blog.csdn.net/weixin_42411702/article/details/123487383