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。
如果還有人認為那最后都是按照bit傳輸的啊,還是都一樣啊。那我個人理解就是:你跟豬是一樣的,畢竟都是細胞組成的。