目錄
1.4 setRequestToSend在Windows上的BUG 5
第1章 Qt 串行通訊
最近要在 Android 手機上開發串行通訊程序,為此學習了一下Qt的串行通訊。本文中,Qt的版本為 5.7.0。
1.1 配置.pro文件
使用 Qt 5.7.0 創建"Qt Widgets Application"類型的項目,然后修改.pro文件,如下圖所示:
圖1.1
給變量 QT 增加serialport,說明程序里將使用串行通訊相關的類。
1.2 查詢串口信息
本節將通過代碼查找系統里的串口,然后填入下圖所示的下拉列表框中。
圖1.2
函數 QSerialPortInfo::availablePorts 會返回系統所有的串口,它的使用請參考如下代碼:
#include <QSerialPortInfo> #include <map>
std::map<int,QString> mapPort; QString sPort; int nPort; foreach (const QSerialPortInfo &info,QSerialPortInfo::availablePorts()) {//foreach 遍歷QSerialPortInfo::availablePorts() 的返回值 sPort = info.portName(); //串口名稱,如:COM5 nPort = GetIntInStr(sPort); //根據串口名稱獲取串口號,如:5 if(nPort >= 0) { mapPort[nPort] = sPort; //根據串口號排序,加入 map } } |
函數GetIntInStr 根據串口名稱(如COM5)獲取串口號(如:5),其代碼如下:
/***************************************************************\ 從字符串里提取整數 s [in] 字符串 返回:提取出來的整數,-1 表示錯誤 \***************************************************************/ int GetIntInStr(const QString&s) { bool bOK = false; //是否發現了數字 int n = 0; int nLenS = s.length(); //字符串長度 ushort c = 0; for(int i = 0;i < nLenS;++i) { c = s[i].unicode(); if(c >= '0' && c <= '9') { bOK = true; n = n * 10 + (c - '0'); } } if(!bOK) { n = -1;//沒有數字,返回 -1 } return n; } |
根據 std::map<int,QString> mapPort填充下拉列表框QComboBox cboPort的代碼如下:
ui->cboPort->clear(); for(std::map<int,QString>::iterator it = mapPort.begin(); it != mapPort.end();++it) { ui->cboPort->addItem(it->second); } |
1.3 配置、打開串口
配置、打開串口的代碼如下:
#include <QSerialPort> m_port = new QSerialPort(); m_port->setPortName("COM1"); //打開 COM1 m_port->setBaudRate(9600); //波特率:9600 m_port->setParity(QSerialPort::NoParity); //校驗法:無 m_port->setDataBits(QSerialPort::Data8); //數據位:8 m_port->setStopBits(QSerialPort::OneStop); //停止位:1 m_port->setFlowControl(QSerialPort::NoFlowControl); //流控制:無 if(m_port->open(QIODevice::ReadWrite)) {//成功打開串口 m_port->setRequestToSend(true); //設置 RTS 為高電平 m_port->setDataTerminalReady(true); //設置 DTR 為高電平 } |
首先new一個QSerialPort對象,然后設置該對象的串行通訊參數,最后調用QSerialPort::open函數打開串口。
這里需要說明一下流控制。通訊的雙方A和B,假如A給B發送數據時,B反應過慢,A不管不顧的不停發送數據,結果會導致數據丟失。為了防止這種情況發生,可使用流控制(也叫握手)。
軟件流控制(XON/XOFF):通訊的一方(B)如果不能及時處理串口數據,會給對方(A)發送XOFF字符,對方接收到這個字符后,會停止發送數據;B不再忙的時候,會給A發送XON字符,A接收到這個字符后,會接着發送數據。軟件流控制最大的問題就是不能傳輸XON和XOFF。
硬件流控制(RTS/CTS):硬件流控制需要按下圖連接兩個串口設備的RTS和CTS。
圖1.3
通訊的一方(B)如果不能及時處理串口數據,會設置自己的RTS為低電平,B的RTS連着對方(A)的CTS,A發現自己的CTS為低電平,將停止發送數據;B不再忙的時候,會設置自己的RTS為高電平,A發現自己的CTS為高電平,將接着發送數據。
上面的代碼中,設置流控制為無,其含義為:不管對方是否能夠反應過來,這邊只管發送數據。
當流控制為硬件時,系統會自動管理RTS和DTR的狀態。否則,應該設置RTS和DTR為高電平,通知對方可以發送串口數據了。
1.4 setRequestToSend在Windows上的BUG
經測試,流控制為無時,調用m_port->setRequestToSend(true);是沒有任何效果的。
下面的Qt源代碼節選自文件C:\Qt\Qt5.7.0\5.7\Src\qtserialport\src\serialport\qserialport_win.cpp(C:\Qt\Qt5.7.0是Qt的安裝目錄)
bool QSerialPortPrivate::setRequestToSend(bool set) { if (!::EscapeCommFunction(handle, set ? SETRTS : CLRRTS)) { setError(getSystemError()); return false; } return true; } |
上面的代碼調用EscapeCommFunction(handle,SETRTS)似乎沒什么問題,但是請看下面的代碼:
bool QSerialPortPrivate::setFlowControl(QSerialPort::FlowControl flowControl) { ... ... ... dcb.fRtsControl = RTS_CONTROL_DISABLE; switch (flowControl) { case QSerialPort::NoFlowControl: break; case QSerialPort::SoftwareControl: break; case QSerialPort::HardwareControl: dcb.fRtsControl = RTS_CONTROL_HANDSHAKE; break; ... ... ... |
硬件流控制時dcb.fRtsControl為RTS_CONTROL_HANDSHAKE,這個沒問題。問題出在dcb.fRtsControl = RTS_CONTROL_DISABLE上,它直接禁用了RTS,所以EscapeCommFunction(handle,SETRTS)並不能設置RTS為高電平。
那么m_port->setDataTerminalReady(true)為什么又是正常的呢?看代碼:
bool QSerialPortPrivate::setDataTerminalReady(bool set) { ... ... ... dcb.fDtrControl = set ? DTR_CONTROL_ENABLE : DTR_CONTROL_DISABLE; ... ... ... } |
set 為 true 時,dcb.fDtrControl 為 DTR_CONTROL_ENABLE,所以可以設置DTR為高電平。
1.5 讀取串口數據
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); //移動文本框內的插入符 } |
1.6 發送串口數據
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秒。
1.7 同步讀取
異步通訊的效率比較高,但是代碼結構比較復雜。有時,需要同步讀取。如:給對方發送字符串 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,退出循環。
1.8 本文示例代碼
本文示例代碼已上傳至 git 服務器,具體如下:
https://github.com/hanford77/SerialPort
https://git.oschina.net/hanford/SerialPort
為了便於測試,可使用Virtual Serial Port Driver 7.1創建一個串口對COM100、COM200,如下圖所示:
圖1.4
上圖的COM100、COM200是虛擬出來的。COM100發送的數據將會被COM200接收到,反之亦然。下圖是測試界面:
圖1.5
本文示例代碼打開了COM100,另一個串行通訊程序打開了COM200。兩者可以相互發送數據。
1.9 Qt 示例代碼
安裝完 Qt 5.7.0 后,Qt 串行通訊的示例代碼也被安裝了,如下圖所示:
圖1.6
C:\Qt\Qt5.7.0是筆者安裝Qt 5.7.0時的安裝目錄。
需要進一步了解Qt串行通訊的可查看這些示例。如:creaderasync 表示異步讀取;creadersync 表示同步讀取……更多信息查看Qt幫助。