<QT学习>串口QSerialPort类同步与异步接收和发送数据

1.功能需求

  通过QT,编写一个库。库的作用是上层直接调用库的函数,并且传参。库函数根据下位机的通信协议,将数据进行封装。通过串口将数据发送给下位机。下位机获得数据后,会对数据进行解析,再通过串口应答一帧数据。库函数再对数据进行解析,提取上层需要的数据,以返回值的形式传递给上层。

 

2.实现步骤

  1.初始化并打开串口

  2.根据下位机的通信协议,编写相对应的函数对数据进行封装。

  3.库函数接收到一帧数据后,提取有效数据并返回给上层。

 

3.代码实现

3.1打开串口

/* 全局变量 */
QSerialPort *serial;

bool OpenCOM(const QString &name) { serial = new QSerialPort(); //port name serial->setPortName(name); //open serial->open(QIODevice::ReadWrite); if(serial->isOpen()) { serial->setBaudRate(115200); serial->setDataBits(QSerialPort::Data8); serial->setStopBits(QSerialPort::OneStop); serial->setFlowControl(QSerialPort::NoFlowControl); } else { return false; } return true; }

  以上的程序就是实例化一个QSerialPort类的对象。上位机根据实际串口是COM几,以传参的形式传递进来。要先打开串口再对串口进行配置。

  其中isOpen()用来检测设备是否打开。

  这里需要说明一下流控制。通讯的双方A和B,假如A给B发送数据时,B反应过慢,A不管不顾的不停发送数据,结果会导致数据丢失。为了防止这种情况发生,可使用流控制(也叫握手)。

  软件流控制(XON/XOFF):通讯的一方(B)如果不能及时处理串口数据,会给对方(A)发送XOFF字符,对方接收到这个字符后,会停止发送数据;B不再忙的时候,会给A发送XON字符,A接收到这个字符后,会接着发送数据。软件流控制最大的问题就是不能传输XON和XOFF。

  硬件流控制(RTS/CTS):硬件流控制需要按下图连接两个串口设备的RTS和CTS。

 

 

   通讯的一方(B)如果不能及时处理串口数据,会设置自己的RTS为低电平,B的RTS连着对方(A)的CTS,A发现自己的CTS为低电平,将停止发送数据;B不再忙的时候,会设置自己的RTS为高电平,A发现自己的CTS为高电平,将接着发送数据。

  上面的代码中,设置流控制为无,其含义为:不管对方是否能够反应过来,这边只管发送数据。

if(serial->open(QIODevice::ReadWrite))
{
    //成功打开串口
    serial->setRequestToSend(true);             //设置 RTS 为高电平
    serial->setDataTerminalReady(true);         //设置 DTR 为高电平

} 

  当流控制为硬件时,系统会自动管理RTS和DTR的状态。否则,应该设置RTS和DTR为高电平,通知对方可以发送串口数据了。

 

3.2 关闭串口

void CloseCom(void)
{
    serial->clear();
    serial->close();
    serial->deleteLater();  //因为之前new了serial这个对象,所以在关闭串口的时候要销毁这个对象。不然会造成内存泄露
}

  clear()用来清除输入输出缓冲区里面的数据。调用这个函数之前,串口必须已经被打开。

  close()用来关闭串口设备。跟open相对应。

  由于我们之前使用new创建了一个对象,会调用构造函数。就必须调用delete来销毁这个对象。这个是C++的规则。QT作为C++的库,也是一样的道理。但是QT可以不用delete,还可以使用deleteLater。从字面上的意思就是后面再删除。 (delete 和 new 必须 配对使用(一 一对应):delete少了,则内存泄露,多了麻烦更大。) 

  deleteLater()并没有将对象立即销毁,而是向主消息循环发送了一个event,下一次主消息循环收到这个event之后才会销毁对象。 这样做的好处是可以在这些延迟删除的时间内完成一些操作,坏处就是内存释放会不及时。

 

3.3数据封装

void Open_Door(int addr, int which_door)
{
    QByteArray tx_buf;

    tx_buf.append(0xAA);
    tx_buf.append(static_cast<char>(addr));
    tx_buf.append(0x01);
    tx_buf.append(static_cast<char>(which_door));
    tx_buf.append(zero);
    tx_buf.append(zero);
    tx_buf.append(zero);
    tx_buf.append(0xFF);

    SendCmd(tx_buf);

}

  

3.4 通过串口下发数据

QByteArray SendCmd(QByteArray cmd)
{
    serial->write(cmd);
    serial->waitForBytesWritten(50000);

    QByteArray data;

    while(serial->waitForReadyRead(5000))
   {

        data = serial->readAll(); //读取串口数据
        if(!data.isEmpty())
        {
            //读到数据了,退出循环
            return data;

        }

    }

}

  可以看到这边使用了waitForBytesWritten和waitForReadyRead函数。下面来解释一下这两个函数。

 

4. 串口发送接收的同步与异步

4.1 异步读取串口数据

  m_port->readAll(函数QIODevice::readAll)用来读取串口数据。不过,它是异步执行的。什么是异步呢?那就是即使对方还没有发送串口数据,m_port->readAll也会立即返回,而不是傻傻的等着对方发送数据过来后再返回。

  既然是异步的,那么何时读取串口数据就成为了关键。Qt提供的方案就是使用信号、槽。

connect(m_port,SIGNAL(readyRead()),this,SLOT(slotReadData()));

  当对方发送串口数据后,将触发m_port的信号QIODevice::readyRead。上面的代码将信号readyRead与槽函数slotReadData连接了起来,因此槽函数slotReadData将被调用,其代码如下:

void Widget::slotReadData()
{
  QByteArray data;
  const int nMax = 64 * 1024;

  for(;;)
  {
    data = m_port->readAll(); //读取串口数据
    if(data.isEmpty())
    {//没有读取到串口数据就退出循环
      break;
    }
    //读取到的串口数据,加入到QByteArray m_dataCom
    m_dataCom.append(data);
    if(m_dataCom.size() > nMax)
    {  
      //防止 m_dataCom 过长       m_dataCom = m_dataCom.right(nMax);     }   }   ui->txtRecv->setText(m_dataCom); //将 m_dataCom 显示到文本框   ui->txtRecv->moveCursor(QTextCursor::End); //移动文本框内的插入符 }

 

4.2 发送串口数据

  m_port->write(函数QIODevice::write)用来发送串口数据,不过它也是异步的。也就是说:代码m_port->write("123");会立即返回,至于数据"123"何时会发送给对方,那是操作系统的事情。操作系统不忙的时候,才会做此项工作。

参考如下代码:

char szData[1024];
memset(szData,'1',sizeof(szData));
szData[sizeof(szData)-1]='\0';
m_port->write(szData);
m_port->close();

  m_port->write(szData);会把1023字节的'1'发送出去。假如波特率为1200,则这些数据需要9秒才能发送完毕。因为m_port->write是异步执行的,所以m_port->write(szData)只是把数据提交给了操作系统就立即返回了。操作系统克隆了一份串口数据szData,在空闲的时候发送,还没发送完毕m_port->close()就被执行了。结果就是大部分的串口数据丢失。

  为了保证上述代码不丢失串口数据,需要将异步通讯更改为同步通讯:

char szData[1024];
memset(szData,'1',sizeof(szData));
szData[sizeof(szData)-1]='\0';
m_port->write(szData);
m_port->waitForBytesWritten(10000);
m_port->close();

  就增加了一行代码m_port->waitForBytesWritten(10000);其含义为:操作系统把串口数据发送出去后,m_port->waitForBytesWritten才会返回。不过,总不能无限制等下去吧?10000就是等待时间的最大值,其单位为毫秒,10000毫秒就是10秒。

 

4.3 同步读取串口数据

  异步通讯的效率比较高,但是代码结构比较复杂。有时,需要同步读取。如:给对方发送字符串 Volt,对方回应电压值 5。

代码如下:

m_port->write("Volt");
m_port->waitForBytesWritten(5000);
QByteArray data;

for(;;)
{
    data = m_port->readAll(); //读取串口数据
    if(!data.isEmpty())
    {
        //读到数据了,退出循环
        break;
    }
}

  通过一个无限循环,将异步读取变成了同步读取。不过,上述代码运行时,CPU占用率将会达到100%(单核CPU)。为此,需要改进代码:

m_port->write("Volt");
m_port->waitForBytesWritten(5000);
QByteArray data;

while(m_port->waitForReadyRead(3000))
{
    data = m_port->readAll(); //读取串口数据
    if(!data.isEmpty())
    {
        //读到数据了,退出循环
        break;
    }
}     

  修改了一行代码m_port->waitForReadyRead(3000),其含义为等待对方发送串口数据过来。如果对方发送串口数据过来了,它返回true,然后使用m_port->readAll读取串口数据;如果对方在3秒内都没有发送串口数据过来,它返回false,退出循环。

注意:

  如果使用waitForReadyRead这种同步的方式来读取串口数据,那么就不需要用connect来连接readyRead信号和槽函数。

  这种方式使用场景是串口发送数据后,数据最好是能立马返回或者是固定多少时间返回。如果串口返回数据的时间不确定,不要用这种方式。还是用connect的异步方式。

 

5. 知识延伸“波特率”

  在4.2中有谈到“把1023字节的'1'发送出去。假如波特率为1200,则这些数据需要9秒才能发送完毕”。

  为什么是9秒呢?

  首先需要明确几个概念:

波特率:

  在消息传输通道中,携带数据信息的信号单元叫码元,每秒通过信道传输的码元数称为码元传输速率,简称波特率。

  所以波特率传输的单位是码元,而码元不是bit。是可以通过不同调制方法在一个符号上负载多个bit信息。

数据帧:

  电脑串口以及一般使用的开发板串口都是默认8个数据bit,一个停止bit,(起始1bit是必须的)默认无奇偶校验位,无流控。

  那么实际上一帧数据其实是10bit,而不是8个bit。那么1200的波特率一秒就是能发送120帧数据,因为一帧里面只有1个字符。就是中间的8个有效数据(ASCII中可以转为为字符,8位就是char这种的数据类型)。

比特率:

  比特率是每秒传输多少bit。以9600bps为例,就是每秒传输9600bit。

  那么每个bit的时间就是1/9600秒=104.16666666666666666666666666666us,大约0.1ms。因此每个bit紧接着下个bit,不存在额外的间隔,不管是起始bit,数据bit,奇偶bit,停止bit。

  所以波特率和比特率的传输单位是不同的。前者是码元后者是bit。

 

posted @ 2020-04-28 17:37  一个不知道干嘛的小萌新  阅读(16545)  评论(4编辑  收藏  举报