• <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]='';
    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]='';
    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。

      如果还有人认为那最后都是按照bit传输的啊,还是都一样啊。那我个人理解就是:你跟猪是一样的,毕竟都是细胞组成的。

  • 相关阅读:
    ASP.NET 取得 Uri 各项属性值
    js获取当前时间显示在页面上
    脚步提示及跳转
    整体刷新和局部刷新frameset窗口
    asp.net 字符串过滤
    .net 获取当前网页的的url
    优酷去广告最新的关于如何屏蔽优酷广告的方法
    bat命令集合
    修复IE
    网易见外工作台(AI),语音转文字,快速制作字幕,中英翻译,在线修改字幕
  • 原文地址:https://www.cnblogs.com/zhuangquan/p/12795729.html
Copyright © 2020-2023  润新知