Qt 串口連接


Qt 串口連接

使用 Qt 開發上位機程序時,經常需要用到串口,在 Qt 中訪問串口比較簡單,因為 Qt 已經提供了 QSerialPort 和 QSerialPortInfo 這兩個類用於訪問串口。

使用 QSerialPort

Qt 提供的 QSerialPort 類繼承於 QIODevice,也就是說,除了少數幾個串口特有的屬性需要單獨設置外,可以像一般的 IO 設備(最常見的是文件)一樣訪問串口。

在項目中加入對串口的支持,先在 .pro 項目工程文件中加入

QT += serialport

然后在程序中包含 QSerialPort 的頭文件,即可使用該串口類:

// file name: comm.cpp  
// class: Comm
#include "comm.h"
#include <QSerialPort>

...

QSerialPort port;
connect( port, &QIODevice::readyRead, this, &Comm::onRead );   // 異步方法,連接 readyRead 信號和數據響應處理槽函數

port.setPortName( "COM1" );   // 設置串口
port.open( QIODevice::ReadWrite );  //讀寫方式打開

成功打開串口后,當上位機從串口接收到數據時就會發出 readyRead 信號,由 Qt 的事件派遣機制調用響應的 onRead 槽函數,在 onRead 函數中對接收到的數據進行處理即可。

出現問題

Qt 中使用串口比較簡單,上下位機通訊是采用的 RS485 協議,當下位機發送數據到上位機時,上位機能夠正常接收並處理。

但是在實際使用過程中卻發現一些問題:
但如果此時對上位機程序進行操作,如向下位機發送一些指令,將會導致 RS485 線上的電平信號出現異常,導致上位機無法正常收到一些數據,而下位機也沒有正確接收到上位機的指令,因此不會回復上位機。

問題的原因在於 RS485 串行協議是半雙工協議,即通訊雙方無法同時發送數據,若同時使用數據通道,將會導致通道上的電平異常,進而導致數據異常(亂碼)。

半雙工協議不支持通訊雙方同時發送數據,那么只要在發送數據前檢測當前串口線路是否被另一方占用,等待串口線路空閑時再發送數據就不會發生沖突。

檢測線路是否被占用,在上位機這邊處理要方便些,具體做法是實時檢測串口,當串口中接收數據時,認為此時不能發送數據;而超過若干 ms 后仍未接收到下位機的數據時,認為此時可以發送數據。

解決方法是,在上述代碼中添加一個計時器,超過 5ms (波特率為9600時,5ms 大約可以發送 4 個字節)未接收到數據時認為 RS485 線路空閑。
然而添加計時器后仍然無法獲取准確的串口線路狀態。並且在示波器上觀察的結果顯示,上位機發送下位機指令的時機有時會在下位機停止發送數據后 3ms 開始發送數據,有時會在下位機停止發送數據后十多毫秒內開始發送數據,總之上位機發送數據的時機是不受控的。

最開始猜測可能是 Qt 的事件循環機制對於事件處理不及時導致無法實時檢測串口線路狀態,查閱 QSerialPort (QIODevice)的 API 后,發現接收數據有阻塞的 API waitForReadyRead,因此嘗試使用多線程+阻塞式方式檢測串口狀態。

多線程與阻塞式

Qt 的多線程較其他的編程語言有些不同,用起來其實非常方便,用法如下:

// file name: commmgr.cpp
// class CommMgr
#include <QObject>
#include <QThread>

...

// obj 必須是 QObject 或其子類的實例指針,並且不能傳入 parent 參數,Comm 是
// QObject  的子類並使用了 Q_OBJECT 宏
Comm *obj = new Comm;

// 實例化 thread 時可以傳入 parent 參數。
QThread *thread = new QThread( this );
obj->moveToThread( thread )     

// 不能直接在主線程中調用 obj 的方法,若直接調用,則該方法將會在主線程中執行
// obj->init();

// 要使 init 方法在子線程 thread 中執行,可以通過 Qt 的元對象提供的 invokeMethod 調用
// 該方法,或者連接某個信號到 obj 的 "init" 槽中,通過發射信號調用 init 槽函數。
QMetaObject::invokeMethod( obj, "init" );
// connect( this, &CommMgr::init, obj, &Comm::init );

需要注意的是要置入子線程的對象 obj 不能設置 parent,否則在運行時就會出現錯誤提示,另外要注意的是 QIODevice 及其子類只能在例化它的線程中使用,如果 Comm 的構造函數中就例化了串口,那么在運行時也會得到錯誤提示。所以這里用到了一個 init 函數,在 obj 例化並移動至子線程之后,在子線程中執行 init 方法,這樣就能避免運行時出錯。

為了控制子線程,這里引入了一個線程管理器類 CommMgr,並在該管理器中增加與 Comm 相應的信號和槽,以便轉發其它組件的信號到 Comm 或從 Comm 中接收數據轉發給其它組件。

接下來修改 onRead 槽函數,使用阻塞式方法讀取串口數據:

// file name: comm.cpp 
// class: Comm

void Comm::onRead() {
    QByteArray data;
    while( m_serial->waitForReadyRead( m_waitTime ) ) {
        data = m_serial->readAll();
        emit receiveRawData( data );
        data.clear();
    }

    // 未接收到數據時,認為串口處於空閑狀態,可以向串口發送數據
    handleQuery();
    // 調用 QCoreApplication 的事件處理方法,用於分發其他線程發送的信號
    QCoreApplication::processEvents();
}

只要能夠從串口中接收到數據,就認為串口被占用,那么就會讀取串口中收到的所有數據並發送給其它組件。
只有當 waitFroReadyRead 方法超時后才可以執行查詢,向串口發送數據,即 handleQuery()方法。

onRead() 方法的調用在打開串口后,使用 while 循環進行重復調用,因此每次執行完一遍 onRead 方法后,需要調用 Qt 的事件處理方法,否則子線程可能就無法結束。

采用多線程+阻塞式方法對串口后,觀察單片機處的串口線路電平發現仍然會出現上下位機通訊時發生沖突的情況。

解決問題

半雙工通訊的模式失敗后,又采用了 Windows 提供的串口 API 隊串口進行監測,並在接受到數據時,打印出時間。調試后發現不論從串口中接收到多少個字節的數據,每一行數據都會相差 16ms 才會被上位機接收到。因此斷定,問題的原因既不在於 Windows 系統,也不在於 Qt 的事件循環機制。

由於采用的 USB-RS485 轉接線中是將 RS485 中的數據轉接到 USB,猜測可能是由於轉接線上有延遲導致無法對串口進行實時監測。

查看 Windows 設備管理器,發現 USB-RS485 轉接器采用的驅動是 FT232R 驅動,搜索后找到一篇關於該轉接器的介紹,notes-on-ftdi-latency-with-arduino 詳細的描述了這一問題並且給出了解決方法,其中一種是:進入設備管理器,找到 USB Serial Port 屬性 ==> 高級。

USB串口端口設置

USB串口的高級設置

將圖中紅色方框內的延遲計時器的值設為 1即可。
當把串口的延時計時器設為1ms時,實時檢測串口的類在發送數據時就不會與下位機沖突。

總結

Qt 的串口類使用起來很方便,一般不需要用到阻塞式的方法 waitForReadyRead,它已經提供了異步非阻塞的信號 readyRead,因此實際上采用 信號+定時器 方式對串口進行實時監測也是有可能的。

為了方便,將適用於全雙工模式的串口類和適用於半雙工模式的串口類進行抽象,提取了抽象的串口操作基類,最開始認為好像多做了很多事情,后來發現對代碼的整體結構和可擴展性都有很大的幫助。增加虛擬串口類用於模擬下位機發送數據時,只需要讓虛擬串口類繼承抽象通訊基類,實現基類中定義的純虛方法,在工廠模式下添加虛擬串口類的例化即可。增加網絡通訊類時,也可以方便的繼承抽象通訊基類(用到的 QTcpSocket 也是繼承自 QIODevice),並且不用修改現有代碼。

另外值得一提的是 Qt 的多線程機制,雖然和大多數編程語言的多線程有所區別,但是使用起來非常方便,需要注意的是 QThread 的功能更像是一個線程管理句柄,有這個句柄才能對其它線程作出調度。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM