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传输的啊,还是都一样啊。那我个人理解就是:你跟猪是一样的,毕竟都是细胞组成的。