以前寫了一系列嵌入式 Linux + Qt 的應用開發文章,裡面包括了一些應用開發的基本知識點,如新建Qt工程,點燈,調節背光,串口通信,多線程,TCP/IP,UDP,等等。
嵌入式Linux + Qt應用開發的遠遠不止這些,但由於工作原因,我就再沒有更新過嵌入式Linux + Qt的相關內容了。
現在整理出來,全文共3.2萬字,123張圖,預計閱讀時間81分鐘,各位老鐵,先點個收藏,然後快速拉到文末,一鍵四連吧~
PDF 文檔也可以從 gitee 下載:
https://gitee.com/embediot/technical_album
(1)第一個嵌入式QT應用程式在成功安裝Qt Creator開發環境後,我們通過一個簡單的嵌入式Qt應用程式,來說明一下如何構建和編譯一個Qt界面應用程式。
目標:了解Qt Creator如何構建和編譯工程,如何把應用程式放到開發板運行。
功能:通過點擊觸控螢幕上的按鈕,實現不同的顯示效果。
我們把第一個Qt應用程式放在ubuntu的/opt/work/qt-application/first_app目錄下。
1、打開Qt Creator開發環境,點擊「New Project」,在彈出的對話框中,選擇「Application」->「Qt Widgets Application」,點擊「Choose...」。
2、點擊「Choose...」後,在彈出的對話框中,設置項目名稱和項目的保存路徑,筆者把項目保存在 /opt/work/qt-application/first_app目錄下,然後點擊「下一步」,如下圖所示:
3、在彈出的對話框中,選擇構建套件,工具鏈選擇「imx6ul-toolchain」,我們目前在debug環境下調試,因此,去除「Release」和「Profile」選項,再點擊「下一步」。
4、在彈出的對話框中,我們選擇構建界面的基類,我們選擇「QWidget」作為基類,類名,頭文件,源文件,界面文件這些屬性,可以重命名,筆者選擇默認。然後點擊「下一步」。(關於「QWidget」和「QMainWindow」的區別,可自行上網查詢,這裡不作詳細描述。)
5、在彈出的對話框中,選擇版本控制軟體,目前筆者沒有進行版本控制,選擇「None」,最後點擊「完成」。Qt工程構建完畢。
6、工程創建完成後,開發界面如下圖所示。關於Qt的具體編程細節,本手冊不作描述。本手冊的所有源碼均公開,源碼含有適當的注釋以輔助閱讀,開發者可作學習參考。
7、雙擊打開widget.ui文件,設計一個簡單的Qt界面,含有一個文本顯示控制項和三個按鈕,點擊不同的按鈕,會在文本顯示框內顯示不同內容。
8、雙擊打開widget.cpp文件,完成代碼編輯,如下圖所示。
9、點擊左下角的構建按鈕,完成工程構建。工程構建完成後,生成的可執行文件存放在ubuntu系統的/opt/work/qt-application/first_app/build-first_app-imx6ul_toolchain-Debug目錄。執行以下命令,把可執行文件複製到ubuntu系統與開發板的共享目錄。
cp build-first_app-imx6ul_toolchain-Debug/first_app /opt/work/qt-images/ -a
10、參考本手冊第6.3節內容「開發板如何通過NFS訪問ubuntu指定目錄?」,在開發板的命令行終端,執行以下命令,把可執行文件複製到開發板的 /opt/qt-application 目錄。
cp /mnt/ubuntu-nfs/qt-images/first_app /opt/qt-application -a
執行以下命令,運行可執行程序
cd /opt/qt-application
./first_app -qws
11、可執行程序在開發板裡運行的界面,如下圖所示,點擊不同的按鈕,文本顯示窗口有不同的顯示內容。
(2)NXP i.MX6UL GPIO控制程序作者使用的i.MX6UL開發板,帶有兩個LED,可以用來進行GPIO的輸出實驗。
目標:了解Qt應用程式如何控制GPIO,實現通用的輸出控制。
功能:通過點擊觸控螢幕上的按鈕,實現LED不同的效果(開/關、心跳燈、硬碟燈)。
應用程式放在ubuntu系統/opt/work/qt-application/002_gpio_ctrl目錄下。
i.MX6UL開發板的板載LED的硬體原理圖如下所示:
從原理圖可以看出,TQ-i.MX6UL使用了GPIO5_IO2和GPIO5_IO7這兩個IO口進行LED的驅動,LED為高電平點亮。
一般情況下,如果使用通用的方法,進行嵌入式Linux的GPIO控制,可以通過訪問/sys/class/gpio路徑下的文件,控制GPIO的方向(輸入還是輸出),狀態(高電平還是低電平)。
以控制GPIO5_IO2為例:
1. 計算對應sys/class/gpio的值GPIOn_IOx = (n-1)*32 + x
GPIO5_IO2=(5 -1)*32 + 2 = 130
2. 將GPIO5_IO2設置為輸出。
#通知系統導出控制的GPIO引腳編號
echo 130 > /sys/class/gpio/export
#控制為輸出
echo "out" > /sys/class/gpio/gpio130/direction
#輸出為高電平
echo "1" > /sys/class/gpio/gpio130/value
#輸出為低電平
echo "0" > /sys/class/gpio/gpio130/value
#通知系統取消導出
echo 130 > /sys/class/gpio/unexport
3. 將GPIO5_IO2設置為輸入。
#通知系統導出控制的GPIO引腳編號
echo 130 > /sys/class/gpio/export
#控制為輸入
echo "in" > /sys/class/gpio/gpio130/direction
這時給該引腳接高電平,輸入即為高電平,反之為低電平
#通知系統取消導出
echo 130 > /sys/class/gpio/unexport
我們不使用以上的方式進行控制,因為內核驅動裡面,已經把這兩個GPIO的資源用作LED控制,如果強行使用以上方式進行GPIO控制,則系統會提示Device or resource busy,如下圖所示:
對於TQ-i.MX6UL平臺,由於內核已經封裝好LED的驅動程序,並且向應用層提供了LED的控制接口(在 /sys/devices/platform/leds/leds/ 目錄下的LED1和LED2),因此,應用程式可以基於這個接口,對相應的LED進行控制。
下面以LED1為例:
/sys/devices/platform/leds/leds/LED1 目錄下的文件節點如下圖所示:
這裡關注兩個節點:brightness 和 trigger 。
brightness節點用來控制LED的亮滅,通過對brightness節點寫入0或1,可以控制LED的亮滅狀態。由於TQ-i.MX6UL的LED接口不是使用PWM方式驅動,因此,brightness節點不支持亮度調節,只支持亮滅控制。
trigger節點用來控制LED不同的觸發方式,通過對trigger節點寫入不同的狀態值,LED可以在不同的狀態下進行亮滅顯示。
trigger支持的狀態值有:
rc-feedback, nand-disk, mmc0, timer, oneshot, heartbeat, backlight, gpio
以下是GPIO應用程式的開發過程
1、先用Qt Creator構建一個工程,命名為:002_gpio_ctrl,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
3、關於LED的具體操作,在編寫代碼的時候,使用面向對象的思想,我們使用一個類進行封裝,這個類封裝了各種關於LED控制的方法。(具體細節請下載源碼),如下圖所示:
4、在Linux系統中一切皆文件,所以,我們可以用普通訪問文件的方法來進行LED的控制,在Qt中,可以使用QFile類進行文件訪問控制。
5、代碼編寫完成後,編譯,並下載到開發板運行。
(3)NXP i.MX6UL LCD背光調節程序目標:了解i.MX6UL如何調節顯示屏的背光亮度。
功能:點擊觸控螢幕對應的亮度按鈕,實現不同的亮度設置,同時獲取和顯示當前的亮度值。
在進行應用軟體開發前,先看一下開發板的LCD硬體接口原理圖,如下所示:
從硬體原理圖可以看出開發板的LCD支持背光調節,通過核心板的GPIO1_IO08引腳,連接到LCD的Backlight接口。應用程式可以通過系統提供的接口,對LCD的背光亮度進行調節。TQ-i.MX6UL支持8級背光亮度調節。
對於TQ-i.MX6UL平臺,內核已經封裝好背光系統的驅動,並對外提供了系統接口,關於背光系統提供給應用程式的接口,在開發板的以下目錄:
/sys/devices/platform/backlight/backlight/backlight
這裡,我們關注三個節點:actual_brightness, brightness, max_brightness
actual_brightness:這個節點只讀,可以通過讀取這個節點,獲取LCD實際的亮度值。
brightness:這個節點可讀可寫,向這個節點寫入不同值(0-7),可調節LCD亮度。
max_brightness:這個節點只讀,通過讀取此節點,獲取可以設置的最大亮度級別。
對於合法的亮度設置值,可以查看驅動的設備樹文件:
arch/arm/boot/dts/tq-imx6ul.dts
由上圖可知,背光碟機動支持8級調節,因此,三個節點actual_brightness, brightness, max_brightness 對應的可調值範圍為 0 - 7。
以下是應用程式的開發過程
1、 先用Qt Creator構建一個工程,命名為:003_backlight_pwm,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、 雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
3、 對於設置背光值和獲取背光值,可以封裝一個類Backlight_Ctrl,這個類包含了設置背光和獲取背光這兩個方法,類的具體內容如下圖所示。
4、 set_brightness() 和 get_brightness() 這兩個方法的具體實現如下圖所示:
5、 在構建Widget對象的時候,我們可以同步構建一個backlight_ctrl對象,這樣,就可以通過這個對象,調用裡面的方法進行背光設置和獲取,如下圖所示。
6、需要注意的是,由於我們設置了背光亮度為0後,顯示屏背光關閉,導致看不到顯示的按鈕。因此,我們通過一個定時器,3秒後恢復到指定的亮度。定時器設置為單次觸發的方式,3秒後觸發。如下圖所示。
7、 至此,所有代碼編寫完成,下載到開發板,運行應用程式。
(4)NXP i.MX6UL RS232串口通信程序目標:了解i.MX6UL如何使用串口進行數據通信。
功能:使用串口進行自定義的數據收發,並把收發數據實時在顯示屏上顯示,實現一個嵌入式上運行的,簡單的串口調試助手。
RS232是工業控制上用得比較多的一種通信方式,TQ-i.MX6UL開發板引出了8個串口(含命令調試口),各個串口的硬體電路圖,請查看官方開發資料。以下是各個串口的描述。
UART1:調試串口,Debug口,三線(RX, TX, GND),RS232電平。
UART2:與RS485復用,默認RS485通信,用作串口時,4線(5V, TXD, RXD, GND)TTL電平。
UART3:無復用,可選3線(RX, TX, GND)RS232電平或4線(5V, TXD, RXD, GND)TTL電平。
UART4:無復用,可選3線(RX, TX, GND)RS232電平或4線(5V, TXD, RXD, GND)TTL電平。
UART5:無復用,可選3線(RX, TX, GND)RS232電平或4線(5V, TXD, RXD, GND)TTL電平。
UART6:無復用,僅支持4線(5V, TXD, RXD, GND)TTL電平。
UART7:與網口2復用,默認為網口2,用作串口時,4線(5V, TXD, RXD, GND)TTL電平。
UART8:與網口2復用,默認為網口2,用作串口時,4線(5V, TXD, RXD, GND)TTL電平。
由此可見,TQ-i.MX6UL某些串口與其他外設接口進行了復用設計,除了調試串口(UART1)外,其他所有串口均支持TTL電平輸出(需要更改某些電阻)。我們選擇無復用功能的RS232串口(UART3, UART4, UART5)進行實驗。
由於 i.MX6UL開發板運行的是QT4.8,不支持QT自帶的串口類庫,QT5以上才支持自帶串口類。因此,開發QT5以下的串口應用時,需要藉助第三方的串口類。
第三方串口類的下載連結如下:
https://sourceforge.net/projects/qextserialport/files/
最新的版本為:qextserialport-1.2win-alpha.zip
在Linux下進行串口應用開發,需要用到以下6個文件:
如果在Windows下使用這個第三方串口類,只需將posix_qextserialport.cpp/posix_qextserialport.h
換為
win_qextserialport.cpp/win_qextserialport.h
以下是應用程式的開發過程
1、 先用Qt Creator構建一個工程,命名為:004_uart_test,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、 雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
界面描述:
[PORT]:指定需要打開的串口,目前提供 ttySAC1 - ttySAC5。表示UART2 - UART6
[BAUDRATE]:提供 2400/4800/9600/19200/38400/115200 這幾種波特率。程序默認8位數據位,1位停止位,無校驗位,無流控的設置方式(可通過代碼修改)。
[OPEN]:設置好串口的工作參數後,點擊「OPEN」打開串口。
[TX_CLEAR]按鈕和[RX_CLEAR]按鈕:清空接收和發送的顯示區域。
[send_data]按鈕:點擊一次,則通過串口發送一次固定數據。
3、 為了方便配置和操作串口讀寫,我們可以把串口相關的操作(配置,讀/寫串口緩衝區)封裝成一個類:Uart_Test,這個類包含了打開和關閉串口的方法,讀/寫串口緩衝區的方法,類的具體內容如下圖所示。
4、 串口類中的bool open_serial_port(QString port,QString baud),具體實現如下:
重點:由於第三方的串口類庫qextserialport在Linux環境下,不支持以事件方式(EventDriven)去讀取串口數據(windows下則同時支持EventDriven和Polling)。所以,Linux環境下,串口需要配置為Polling的工作方式,並且開啟一個周期定時器,去讀取串口數據。
最新的QT5版本添加了串口的操作類QSerialPort,支持事件觸發。但目前TQ-i.MX6UL僅支持QT4.8,後續待開發板的QT版本更新後,會同步更新串口通信程序。
5、 串口數據的讀函數void slot_read_serial_port() 和 寫函數void write_serial_port(QByteArray arr)的具體實現如下所示:
重點:由於串口數據是通過定時器查詢的方式讀取,並且一次性讀取所有數據。因此,每次串口有數據到達,可能會出現數據粘包或分包的情況。對於此類情況,建議使用自定義報文的方式,定義數據報文的幀頭和幀尾,並使用環形隊列處理方式。每次保證接收到完整的數據報文後,再進行數據處理。實驗中為了簡化工程量,所以並沒有採用以上方式。
6、 在Widget類的構造函數中,我們定義一個類對象uart_test。並且通過connect函數把對象uart_test裡面的信號void read_serial_signals(QByteArray arr) 與串口數據處理的槽函數void slot_serialport_data_process(QByteArray arr)進行綁定。當串口有數據到達時,可以通過該槽函數進行處理。
7、 在槽函數void slot_serialport_data_process(QByteArray arr)裡,我們把串口接收的數據顯示出來,當然,也可以處理其他事務。
8、 點擊send_data按鈕,則通過串口發送固定數據,send_data按鈕的具體實現如下所示:
9、 編譯代碼,下載到開發板運行應用程式。我們使用UART3(ttySAC2)與電腦進行串口數據收發,其他串口操作類似。
(5)嵌入式QT多線程的簡單實現(方法一)本文的內容是拜讀完以下文章後的總結,喝水不忘挖井人,感謝前輩的肩膀,讓我們這些晚輩少走彎路,走得更遠。如果已經理解了原作者的文章,則可完全忽略本文,感謝閱讀。
https://blog.csdn.net/czyt1988/article/details/64441443
在嵌入式Linux應用程式的開發過程中,多線程永遠是一個不可逃避的話題。多線程的出現,可以使一些任務處理更加方便快捷。使用多線程,可以把原來複雜龐大的系統任務,分解成多個獨立的任務模塊,方便開發人員管理,使程序架構更加緊湊,更加穩定。
關於線程的簡單通俗理解,請參考以下文章:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
在QT開發過程中,使用多線程,有兩種方法:
方法一:繼承QThread的 run() 函數,把複雜的循環邏輯放在run() 函數中執行。方法二:把一個繼承於QObject的類,使用moveToThread() 方法,轉移到一個QThread的類對象中。
方法一:繼承QThread類,重載run() 方法。
使用此方法進行QT多線程,有一條很重要!很重要!很重要!的規則需要記住:繼承QThread類,創建線程對象後,只有run()方法運行在新的線程裡,類對象裡面的其他方法都在創建QThread類的線程裡運行。
簡單地舉一個例子:
如果在QT界面的ui線程裡,使用繼承了QThread的類去定義一個對象qthread,並且重載了run()函數,這個類還有其他函數。那麼,調用對象qthread裡面的非run()函數,這些函數就會在ui線程中執行,並不會產生新的線程ID。因此,如果要執行耗時的任務,最好把任務邏輯都寫在run()函數中,否則,耗時的任務會把ui阻塞,導致ui出現卡死現象。還有一點要注意,如果要在非run()函數和run()函數裡面,進行qthread對象裡面的某一個變量修改,要注意進行加鎖操作。因為,run()函數與非run()函數運行於不同的線程。
繼承QThread類,重載run()方法,這樣開啟一個線程比較簡單,但在開發過程中,我們更關注以下問題:
1、在ui線程中,調用了繼承QThread類裡面的方法,會不會造成ui卡頓。
2、在ui線程中,調用了QThread::quit() / QThread::exit()/ QThread::terminate() 會不會停止線程。
3、如何安全地退出一個線程?
4、如何正確地啟動一個線程?
> 如何正確地啟動一個全局線程?
> 如何正確地啟動一個局部線程?
目標:了解QT如何分別使用兩種方法,實現多線程編程。
功能:在i.MX6UL開發板上運行多線程,並把實驗現象在顯示屏上進行顯示。
以下是應用程式的開發過程
1、為了驗證線程的相關問題,我們先編寫一段簡單的代碼,使用QThread類進行線程創建。先用Qt Creator構建一個工程,命名為:005_qthread_test,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
界面描述:
QThread run:點擊此按鈕,開始運行使用QThread類進行創建的線程,即運行run()函數。
QThread quit:點擊此按鈕,執行Qthread::quit()函數。
QThread Terminate:點擊此按鈕,以安全的方法退出線程。
QThread exit:點擊此按鈕,執行QThread::exit()函數。
QThread run local:點擊此按鈕,開始運行一個局部線程。
Clear Browser:清空顯示區域的內容。
get something:點擊此按鈕,在ui線程中調用QThread類裡面的函數,觀察其線程id。
set something:點擊此按鈕,在ui線程中調用QThread類裡面的函數,觀察其線程id。
do something:點擊此按鈕,在ui線程中調用QThread類裡面的函數,觀察其線程id。
heartbeat進度條:在ui線程中運行,觀察ui線程是否有卡死現象。
thread進度條:在QThread線程中運行,顯示線程運行的百分比。
信息窗口:顯示程序運行時,各個線程的列印信息。
3、針對以上提出的問題,首先,我們先把已經編譯下載好的程序,在開發板中運行起來,看一下實驗現象。
實驗現象說明:
a. 點擊 [QThread run] 按鈕,ThreadFormQThread類(繼承於QThread類)裡面的run() 函數開始運行。run()函數首先列印線程啟動信息,列印當前的線程ID,每隔1秒鐘,更新一次thread進度條,列印已運行的次數,列印線程的具體信息。
b. 點擊 [QThread quit] 和 [QThread exit],調用QThread::quit() 和 QThread::exit() 函數,線程並沒有停止運行,因此,以上兩個函數並不能結束線程的運行。
c. 點擊 [get something] [set something] [do something] 按鈕,列印出調用以上三個函數的線程ID,是ui線程ID。這就說明了,在ui線程裡調用ThreadFormQThread類對象裡面的函數,函數也是運行在ui線程,而非新創建的線程,只有ThreadFormQThread類對象裡面的run()函數才以新的線程來運行。
d. 點擊 [QThread Terminate] 按鈕,安全退出線程,即安全退出run()函數。
e. 點擊 [Clear Browser] 按鈕,清除顯示框的內容。
f. 點擊 [QThread run local] 按鈕,啟動一個新的局部線程,這個線程的父線程不是ui線程,而且,這個線程執行完後,會自動銷毀線程運行時的所有資源。
4、新建一個 ThreadFormQThread.h 文件,創建一個繼承QThread的類。
5、新建一個 ThreadFormQThread.cpp文件,編寫類方法的具體實現(詳細內容請參見源碼),重載run()函數。以下是run()函數的具體實現。
重載run()函數的實現內容,當調用QThread::start()後,這個run()函數就開始進行在新的線程裡被調用,剛進入函數時,列印出當前調用run()函數的線程ID,可以看出,跟ui線程是不一樣的ID。把m_isCanRun變量設置為true,這個變量用來安全退出線程的,這個變量只能在m_lock這個互斥鎖裡面被修改。
6、開始回答以上提出的問題,在ui線程中,調用了繼承QThread類裡面的方法,會不會造成ui卡頓?
如代碼所示,在ui線程中,點擊按鈕,分別通過m_thread對象(注意:m_thread對象是在ui線程中生成的)直接調用裡面的函數,裡面的函數也是歸屬於ui線程的,從實驗現象可以看出,heartbeat進度條一直在更新,驗證了在ui線程中,調用了繼承QThread類裡面的方法,並不會造成ui卡頓。
7、在ui線程中,調用了QThread::quit() / QThread::exit()/ QThread::terminate() 會不會停止線程?
如代碼所示,分別調用QThread::quit() / QThread::exit() / QThread::terminate() 進行退出線程。從實驗現象可以得出,QThread::quit() 和QThread::exit() 這兩個函數,並不會讓線程退出,因為這兩個函數隻對QThread::exec()有效。QThread::terminate()則會強制退出線程,不管線程的運行情況(不建議使用這種方法)。應該使用stopImmediately()函數,安全退出線程。stopImmediately()函數的內容如下:
可以看出,在m_lock互斥鎖的保護下,把m_isCanRun變量置為false,當run()函數的while循環遇到這個變量為false,則break當前運行的循環,結束線程。
8、如何安全地退出一個線程?
如第7點描述所示,要安全地退出一個線程,可以在外部使用stopImmediately()函數。因為是在ui主線程中調用這個函數的,並使用了互斥鎖進行保護,因此,當這個函數被調用時,會馬上把m_isCanRun變量置為false,這樣,即可安全地退出run()函數的while循環,run()函數在返回的時候,即被視為線程結束,會發射finish()信號,槽函數onQThreadFinished()即會被調用。
9、如何正確地啟動一個線程?(全局線程和局部線程)
線程的啟動有多種方法,這幾種方法都涉及到線程由誰(父線程)去生成,以及線程如何安全地退出。關於線程的生成和退出,首先需要搞清楚的是線程的生命周期,這個線程的生命周期是否跟ui線程一致(全局線程),還是線程只是臨時生成,完成任務後就進行銷毀(局部線程)。
全局線程,在創建時,把ui線程作為自己的父對象,當ui線程析構時,全局線程也會進行銷毀。但此時,應該關注一個問題:當ui線程結束(窗體關閉)時,全局線程還沒有結束,應當如何處理?如果沒有處理好這種情況,在ui線程析構時,強行退出全局線程,會導致程序崩潰。往往這種線程的生命周期是伴隨著ui線程一起開始與結束的。
局部線程,也叫臨時線程,這種線程一般是要進行一些耗時任務,為了防止ui線程卡死而存在的。同樣地,我們更關注以下問題:在局部線程運行期間,如果因為某些因素要停止線程,該如何安全地退出局部線程?例如,在圖片打開期間(還沒有完全打開),要切換圖片,該如何處理。在音樂播放期間,要切換下一首音樂,應如何處理。
如何正確地啟動一個全局線程?
由於是全局線程,因此,在ui窗體構建的時候,線程隨即被構建,並且把ui窗體設置為線程的父對象。此時,需要注意的是,不能隨便delete線程指針!!!因為這個線程是伴隨著ui線程構建的,存在於QT的循環事件隊列中,如果手動delete了線程指針,程序會很容易崩潰。正確的退出方法,可以使用 void QObject::deleteLater() [SLOT] 這個槽函數。全局線程的創建代碼如下圖所示:
在ui窗體構建時,創建一個全局線程對象,並關聯槽函數,此時,線程對象已經構建,但線程還沒有運行,run()函數還沒有執行。注意,這裡沒有使用void QObject::deleteLater() [SLOT] 這個槽函數,而是使用了另一種方法來進行線程結束。void QObject::deleteLater() [SLOT] 這個槽函數會在局部線程那裡進行使用。
10、要啟動線程,點擊界面上的 [QThread run] 按鈕,調用onButtonQThreadClicked() 槽函數,這個函數裡面,判斷全局線程是否已經運行,如果沒有運行,則調用QThread::start()函數,啟動線程(即run()函數開始運行)。
如果在線程運行期間,重複調用QThread::start(),其實是不會進行任何處理的。在按鈕的槽函數中,也進行了適當的判斷。
11、啟動運行一個全局線程,是很簡單的,但我們更應該關注如何安全退出一個全局線程,因為這個全局線程是在ui線程中進行生成的,因此,在ui窗口析構時,應該需要判斷線程是否已經運行結束(或者主動安全結束線程),才能進行 delete ui 操作。
在ui線程析構時,調用 stopImmediately() 安全退出線程,然後調用 QThread::wait() 等待線程結束,QThread::wait()會一直阻塞,這樣才不會導致線程還沒有結束就 delete ui,造成程序崩潰。
如何正確地啟動一個局部線程?
12、啟動一個局部線程(運行完自動銷毀資源的線程),操作方法跟啟動一個全局線程差不多,主要是需要多關聯一個槽函數:void QObject::deleteLater() [SLOT],這個函數是局部線程安全退出的關鍵函數。點擊 [QThread run local] 按鈕,調用onButtonQThreadRunLoaclClicked()函數,啟動一個局部線程。
與全局線程不同的是,局部線程在new ThreadFromQThread(NULL)時,並沒有給它指定父對象,deleteLater()槽函數與線程的finish()信號進行綁定,線程結束時,自動銷毀線程創建時分配的內存資源。
對於局部線程,還需要注意重複調用線程的情況。對於比較常見的需求,是在局部線程還沒有執行完的時候,需要重新啟動下一個線程。這時,就需要安全結束本次局部線程,再重新創建一個新的局部線程。例如:在一張圖片還沒有加載完成的時候,切換到下一張圖片;在一首歌曲還沒有播放完成的時候,切換到下一首歌曲。針對這種情況,我們使用了一個成員變量m_currentRunLoaclThread來記錄當前局部線程的運行情況,當m_currentRunLoaclThread變量存在時,先結束線程,然後再生成新的局部線程。
13、除了使用成員變量來記錄當前運行的局部線程,還需要關聯destroy(QObject*)信號,這個信號用於當前局部線程銷毀時,重新把m_currentRunLoaclThread變量置為NULL。
也可以在這個onLocalThreadDestroy(QObject *obj)的槽函數中,進行局部線程的資源回收工作。
14、至此,使用繼承QThread類,重載run()方法來創建線程,已經介紹完畢,以下是這種方法的簡單總結。
(1)繼承QThread類,只有run()方法是運行在新的線程裡,其他方法是運行在父線程裡。
(2)執行QThread::start()後,再次執行該函數,不會再重新啟動線程。
(3)在線程run()函數運行期間,執行QThread::quit()和QThread::exit(),不會導致線程退出。
(4)使用成員變量和互斥鎖,可以進行線程的安全退出。
(5)對於全局線程,不應該delete線程指針,在ui窗體析構時,應使用QThread::wait()等待全局線程執行完畢,再進行delete ui操作。
(6)對於局部線程,要善於使用QObject::deleteLater()和QObject::destroy()來銷毀線程。
(6)嵌入式QT多線程的簡單實現(方法二)本文的內容是拜讀完以下文章後的總結,喝水不忘挖井人,感謝前輩的肩膀,讓我們這些晚輩少走彎路,走得更遠。如果已經理解了原作者的文章,則可完全忽略本文,感謝閱讀。
https://blog.csdn.net/czyt1988/article/details/71194457
上一篇文章介紹了使用繼承QThread類,重載run()函數的方法來實現多線程,這種方法是QT實現多線程編程的傳統方法,但從QT4.8以後的版本開始,QT官方並不是很推薦使用這種方法,而是極力推薦使用繼承QObject類的方法來實現多線程,因為QObject類比QThread類更加靈活。當然,使用哪種方法實現多線程,需要根據具體的實際情況而定。
QObject類是QT框架裡面一個很重要的基本類,除了QT的關鍵技術信號與槽,QObject還提供了事件系統和線程操作接口的支持。對QObject類,可以使用QObject類裡面的方法moveToThread(QThread *targetThread),把一個沒有父級的繼承於QObject的類轉移到指定的線程中運行。
使用繼承QObject來實現多線程,默認支持事件循環(QT裡面的QTimer、QTcpSocket等等,均支持事件循環)。而如果使用QThread類來實現多線程,則需要調用QThread::exec()來支持事件循環,否則,那些需要事件循環支持的類都無法實現信號的發送。因此,如果要在多線程中使用信號和槽,那就直接使用QObject來實現多線程。
不管使用方法一還是方法二,在QT中創建多線程是比較簡單的,難點在於如何安全地退出線程和釋放線程資源。在創建完線程之後,使用方法二(繼承QObject)來創建的線程,不能在ui線程(主線程)中直接銷毀,而是需要通過deleteLater() 進行安全銷毀。
先來總結一下,如何使用繼承QObject的方法來創建多線程:
(1)寫一個繼承QObject的類,並把複雜耗時的操作聲明為槽函數。
(2)在主線程(ui線程)中new一個繼承Object類的對象,不設置父類。
(3)聲明並new一個QThread對象。(如果QThread對象沒有new出來,則需要在QObject類析構的時候使用QThread::wait()等待線程完成。如果是通過堆分配(new方式),則可以通過deleteLater來進行釋放)
(4)使用QObject::moveToThread(QThread*)方法,把QObject對象轉移到新的線程中。
(5)把線程的finished()信號與QObject的deleteLater()類連接,這個是安全釋放線程的關鍵,不連接的話,會導致內存洩漏。
(6)連接其他信號槽,如果是在連接信號槽之前調用moveToThread,不需要處理connect函數的第五個參數,這個參數是表示信號槽的連接方式;否則,如果在連接所有信號槽之後再調用moveToThread,則需要顯式聲明Qt::QueuedConnection來進行信號槽連接。
(7)完成一系列初始化後,調用QThread::start()來啟動線程。
(8)在完成一系列業務邏輯後,調用QThread::quit()來退出線程的事件循環。
使用繼承QObject的方法來創建和啟動線程,這種方法比使用QThread來創建線程要靈活得多。使用這種方法創建線程,整個線程對象都在新的線程中運行,而不像QThread那樣,只有run()函數在新的線程中運行。QObject自身的信號槽和事件處理機制,可以靈活地應用到業務邏輯中。下面,我們基於方法一(繼承QThread)的例程,使用繼承QObject的方法來創建線程。
以下是應用程式的開發過程
1、先用Qt Creator構建一個工程,命名為:006_qthread_object,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
界面描述:
[moveToThread run 1]:發送信號啟動耗時任務1
[moveToThread run 2]:發送信號啟動耗時任務2
[stop thread run]:修改變量,退出耗時任務的運行
[Clear Browser]:清空信息顯示窗口
3、創建一個ThreadObject.h頭文件,在頭文件定義一個繼承於QOBject的類ThreadObject,類的具體成員變量和成員函數,如下所示:
4、創建一個ThreadObject.cpp文件,ThreadObject類裡面的成員函數,均在這個文件裡面實現。代碼如下所示:
線程的耗時操作都在這兩個函數中進行,這兩個函數都是通過ui線程中的信號進行觸發的。同時,為了對線程進行停止,這裡使用了一個m_isStop的成員變量。通過在ui線程中,調用stop()函數修改m_isStop變量,達到退出線程的目的。注意,修改m_isStop變量時,要進行互斥鎖操作。
5、在widget.cpp文件中,ui窗口進行構造時,先設置進度條的初始狀態,並關聯定時器的超時槽函數,這個定時器用來監控ui線程是否卡死。代碼如下所示:
6、點擊 [moveToThread run 1] 或 [moveToThread run 2] 按鈕,即可調用startObjThread()啟動線程,然後通過發送信號,喚起線程耗時操作的槽函數。startObjThread()函數的具體實現如下所示:
這裡需要注意,QThread對象是通過new方法在堆內創建的,線程finished()退出的時候,需要關聯deleteLater槽函數進行線程的安全退出和線程資源回收。否則,線程所佔用的內存資源就不會進行釋放,長時間下去,會造成內存資源洩漏。
7、至此,使用繼承QObject這種方法來實現多線程已經介紹完畢,這種方法比使用繼承QThread更加方便靈活,使用這種方法的總結如下:
(1)如果線程裡面使用到消息循環,信號槽,事件隊列等等操作,建議使用QObject來創建線程。
(2)繼承QObject的類對象,都不能指定其父對象。
(3)新線程裡面耗時操作,都定義為槽函數,方便通過信號啟動線程。
(4)操作線程裡面的成員變量時,為了安全,需要加鎖後再進行操作。
(5)互斥鎖QMutex會帶來一點性能損耗。
8、把程序編譯完後,下載到開發板運行,運行現象如下圖所示:
實驗現象說明:
(1)程序開始運行時,先列印出ui的線程ID。
(2)點擊[moveToThread run 1]按鈕,耗時任務work1開始運行,並列印出運行的線程ID。
(3)點擊[moveToThread run 2]按鈕,耗時任務work2開始運行,並列印出運行的線程ID。
(4)點擊[stop thread run]按鈕,耗時任務停止運行,並列印出stop()函數所在的線程ID。
(5)在work1運行期間,如果開啟work2任務,則work1任務會中斷,反之亦然。
(6)任務work1和任務work2均由同一個線程管理並運行。stop()函數與新建線程不在同一個線程內,stop()函數是歸屬於ui線程的。
(7)基於TCP/IP的網絡通信應用程式(TCP-Client)不管是嵌入式Linux應用程式,還是物聯網IoT應用開發,網絡通信一定是一個不可或缺的重要環節。可以說,沒有網絡支持,整個物聯網應用體系將產生不了社會價值,沒有網絡,很多應用程式都會受到限制。
作為全世界最優秀的開源作業系統,Linux內部已經集成了強大的網絡協議棧,並向應用層提供豐富的系統調用,開發者可以基於通用的系統調用接口,使用Linux內核提供的網絡功能。
如果要分析Linux內部的網絡通信機制以及實現原理,相信不是一時半刻或片文隻字能描述清楚,一般的應用開發者可以通過網上搜索資料去了解一下,但在初學階段,不建議去深入研究。因此,本章節只是站在應用開發的角度,描述如何開發嵌入式QT的網絡應用程式。
TCP/IP協議模型(Transmission Control Protocol/Internet Protocol),包含了一系列構成網際網路基礎的網絡協議,是Internet的核心協議。它是一種面向連接的、可靠的、基於字節流的傳輸協議。關於TCP/IP協議的具體實現原理,本文不進行描述(關於TCP/IP的實現原理描述,其複雜度已經可以用一本書來具體闡述了)。本文著重描述在嵌入式QT環境下,如何使用TCP/IP進行數據通信。
對於TCP/IP的客戶端(TCP-Client)角色,在進行數據通信之前,一般會經歷以下過程:
(1)調用socket套接字函數,創建套接字的文件描述符。
(2)配置連接參數(需要連接的伺服器IP,埠,連接協議,等等)。
(3)基於套接字的文件描述符,調用connect函數,連接指定的伺服器。
(4)處理connect過程中可能出現的情況。(如連接成功,連接出錯,找不到伺服器等)
(5)connect成功後,調用數據發送接口,進行數據發送。
(6)調用數據接收接口,並處理接收到的網絡數據。(Linux C語言開發,可以阻塞接收或使用select/poll機制接收。QT開發,可以使用信號槽機制進行接收。)
使用嵌入式QT進行TCP/IP的網絡通信應用程式開發,對於TCP客戶端,只需要關注QT提供的QTcpSocket類,這個類繼承於QAbstractSocket類,在QAbstractSocket類裡面提供了一系列的網絡操作接口函數,如:連接伺服器connectToHost()、斷開與伺服器的連接disconnectFromHost(),等等。提供各種信號(connected()、disconnected()、stateChanged())與槽函數,方便應用開發者調用。具體可以參閱 QtNetwork/qabstractsocket.h 文件的內容。
目標:
使用QT提供的TCP/IP網絡通信類,實現一個簡單的TCP客戶端(TCP-Client)
功能:
(1)開發板界面顯示開發板的網絡IP位址。
(2)可手動輸入需要連接的伺服器IP和埠。
(3)界面顯示TCP客戶端的連接狀態。(連接成功,斷開連接,連接出錯)
(4)界面顯示TCP客戶端的收發數據,並提供清屏按鈕。
(5)提供手動發送按鈕和自動發送按鈕。
實驗現象初覽:
伺服器端的界面(在windows7運行的網絡通信工具):
服務端界面描述:
(1)輸入本機的服務端IP位址和埠,並開始偵聽。筆者服務端是192.168.1.59:4418
(2)界面顯示已經連接上的客戶端IP位址,可以看出,跟imx6ul本機的IP一致。
(3)數據發送區1裡面是服務端自動回復給客戶端的內容,表示伺服器端收到客戶端的數據後,馬上自動回復」reply_from_pc_to_imx6ul」。
(4)最底下顯示接收到的客戶端數據,從上圖可以看出,客戶端分別以自動方式和手動方式發送了數據。
客戶端的界面(在imx6ul上運行):
客戶端界面描述:
(1)程序啟動時,在界面右上角顯示imx6ul本機的IP位址。
(2)填寫客戶端需要連接的伺服器IP和埠。
(3)點擊 [CONNECT] 按鈕後,會在按鈕後面顯示連接的狀態。
(4)點擊 [START_AUTO_SEND] 後,客戶端會自動以1秒的頻率向伺服器發送數據。
(5)每點擊一次 [tcp_send_data] 按鈕,客戶端都會向伺服器發送一次數據。
(6)在數據顯示框顯示客戶端發送/接收到的數據內容。
(7)點擊 [TX_CLEAR] 或 [RX_CLEAR] 進行清屏操作。
以下是應用程式的開發過程
1、先用Qt Creator構建一個工程,命名為:007_tcp_client,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、創建工程後,修改007_tcp_client.pro裡面的內容,添加QT裡面的網絡通信模塊network,使工程支持QT網絡類的調用,如下圖所示。
3、雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
客戶端界面描述如上面內容所示,這裡不作重複。
4、創建一個tcp_client.h文件,編寫一個TCP_Client類繼承於QTcpSocket,這個類提供了一些客戶端經常用的操作函數,如連接伺服器,獲取本機IP,處理連接狀態,等等。類的具體實現如下圖所示:
5、創建一個tcp_client.cpp文件,這個文件內包含了TCP_Client類裡面的所有函數實現。有關tcp_client.cpp的具體內容,請參閱源碼。
6、Qt應用程式啟動時,跟C語言一樣,都是以main函數作為入口。(當然了,真正的程序啟動入口並不是main函數,這裡忽略了main函數之前的一系列複雜過程,應用程式呈現給開發者的,一般都是以main函數作為入口)。以下是Qt應用程式的main函數入口。
這個main函數比較簡單,裡面定義了一個QApplication和Widget對象,通過Widget::show()函數顯示窗體,然後執行QApplication::exec()函數把整個應用程式加入Qt的事件隊列,不斷循環。
7、在Qt的界面應用程式中,對於Widget類型的窗體,其ui都是通過QWidget類進行構建,在widget.cpp文件中,針對本工程,Widget類的構造函數如下圖所示:
在Widget類的構造函數裡面,先對ui裡面的某些控制項進行配置,比如:在連接按鈕點擊之前,手動和自動發送數據的按鈕不可用,伺服器IP和埠可被編輯。然後創建一個TCP_Client類對象,TCP-Client的一系列操作,如連接伺服器,收發數據等等,都是基於這個類對象進行。程序開始運行的時候,在界面上顯示本機的IP位址,並連接tcp-client對象提供的信號槽。最後,構建一個定時器對象用於數據的自動發送,定時器對象在構建時,並沒有啟動計時。
8、先在windows7上運行伺服器端的程序,設置好監聽的IP和埠。imx6ul板卡上的客戶端應用程式啟動後,設置好需要連接的伺服器參數,點擊 [CONNECT] 按鈕,程序就會調用該按鈕的槽函數void Widget::on_pushButton_connect_clicked(),函數的實現內容如下圖所示:
9、點擊按鈕後,分兩種情況,需要判斷當前的客戶端網絡狀態是連接還是斷開。如果客戶端處於連接狀態,則點擊按鈕後,斷開客戶端與伺服器的連接。如果客戶端處於斷開狀態,則點擊按鈕後,先檢查輸入的參數是否有效,如果參數有效,則啟動客戶端與伺服器的連接。客戶端與伺服器的連接函數connect_server(),如下圖所示:
10、客戶端與伺服器連接過程中,由於在TCP-Client類裡面綁定了連接狀態的信號槽,因此,連接狀態改變後,系統會發送相應的信號,然後調用對應的槽函數進行處理。因為需要把連接狀態顯示在界面上,因此,需要在Widget類中,編寫一個處理連接狀態的槽函數,這個槽函數處理了CONNECTED,DISCONNECTED和CONNECT_ERROR這三種狀態。如下圖所示:
11、每點擊一次手動發送按鈕 [tcp_send_data],客戶端將會發送一包數據到服務端。點擊自動發送按鈕 [START_AUTO_SEND] 按鈕,客戶端將啟動定時器,並以1秒的間隔向服務端發送數據。手動發送函數和自動發送函數,如下圖所示:
12、客戶端的發送數據函數tcp_client_send_data(QString),是直接調用了QIODevice::write()進行發送的。我們用面向對象多態的思維,編寫兩個發送函數,可以分別以QByteArry或QString類型的參數進行數據發送,如下圖所示:
13、客戶端的接收數據函數,是通過信號槽機制來實現的,當Widget類對象接收到TCP-Client類發送的信號signal_tcp_client_recv_data(QByteArray),就調用槽函數處理接收到的數據,如下圖所示:
14、在TCP-Client類對象中,客戶端是通過槽函數slot_tcp_client_recv_data()進行網絡數據接收的,這個槽函數綁定了QIODevice::readyRead()信號,一旦底層的IO有網絡數據接收,則會調用該槽函數處理,然後這個槽函數會把數據從底層驅動的緩衝區中把數據全部讀出,再發送signal_tcp_client_recv_data(QByteArray),通知Widget類進行處理。在這裡,可能大家會有一個疑惑,為什麼不在這個槽函數直接處理數據呢?那是因為考慮到了軟體的封裝性。TCP-Client類只負責接收數據,然後再把接收到的數據通過信號槽傳遞出去,至於數據怎樣處理,則應該在其他的數據handle類裡面進行。比如,這個客戶端只進行數據顯示,則可以在Widget類裡面處理數據,只需要把Widget類裡面的數據處理槽函數與TCP-Client類裡面的信號綁定就行了。TCP-Client類裡面的槽函數slot_tcp_client_recv_data()如下圖所示:
15、至此,整個TCP-Client連接伺服器以及數據收發過程已經描述完成。這個工程只是簡單地描述了TCP-Client建立通信和數據收發的簡單過程。在真正的TCP-Client網絡應用程式中,還需要處理很多突發的網絡情況。如:連接過程中的錯誤處理,心跳包機制,服務端強行斷開連接後客戶端的處理,數據粘包與斷包,數據接收隊列,等等。開發者應在工程開發中不斷積累經驗,才能開發出穩定可靠的網絡應用程式。
題外知識:
很多初學者可能會對伺服器和客戶端沒有什麼概念,不知道怎樣理解服務端/客戶端這兩個角色,只要記住關鍵的一點:客戶端是請求服務的,伺服器是提供服務的。
舉一個簡單而通俗的例子:汽車去加油站加油。
把汽車比喻為TCP-Client客戶端,把加油站的加油機比喻為TCP-Server伺服器。當汽車(TCP-Client)需要向加油機(TCP-Server)請求加油服務時,則需要先知道加油站在哪(伺服器的IP位址),在哪臺加油機(伺服器埠)上加油,當指定好加油站和具體的加油機後,就可以向加油機請求連接(向汽車插上加油槍)了。But,總會有特殊情況的時候,比如,當該加油機沒有油而導致加不了油(連接不上)了,那麼,加油機(伺服器)就會通知請求加油的汽車(客戶端)進行處理,(這裡就涉及到連接不上的情況了)。當汽車加完油之後,就像是服務端已經提供完服務,那麼,汽車(客戶端)就可以主動端開與加油機(服務端)的連接了。
(8)基於TCP/IP的網絡通信應用程式(TCP-Server)
上一篇文章講述了在i.MX6UL開發板中,以客戶端的角色,使用TCP/IP協議進行網絡通信。本章節,將以服務端的角色進行講解,如何開發一個TCP服務端(TCP-Server)。
目標:
使用QT提供的TCP/IP網絡通信類,實現一個簡單的TCP服務端(TCP-Server)
功能:
(1)開發板界面顯示開發板服務端的網絡IP位址。
(2)可手動輸入需要監聽的網絡埠。
(3)提供按鈕,可手動開啟/停止服務端監聽。
(4)界面顯示TCP客戶端的收發數據,並提供清屏按鈕。
(5)提供服務端手動發送按鈕和自動發送按鈕。
開發板運行TCP服務端(TCP-Server)後,界面如下圖所示:
服務端界面描述:
(1)服務端程序啟動後,先獲取本機IP位址作為伺服器的IP位址。程序默認監聽4418埠,用戶可自定義修改需要監聽的埠。
(2)點擊[LISTEN]按鈕,開始服務端監聽,等待客戶端連接。
(3)客戶端連接成功後,會在數據顯示窗口提示客戶端上線,並在客戶端列表顯示每個客戶端的IP位址和連接埠。
(4)用戶點擊[START_AUTO_SEND]按鈕,服務端以1秒的頻率,自動對所有客戶端發送固定數據。再次點擊該按鈕,停止自動發送數據。
(5)用戶每點擊一次[tcp_send_data]按鈕,服務端對指定的客戶端發送一幀固定數據。
(6)用戶點擊[CLEAR]按鈕,清空數據顯示窗口的內容。
對於TCP/IP的服務端(TCP-Server)角色,在進行數據通信之前,一般會經歷以下過程:
(1)調用socket套接字函數,創建套接字的文件描述符(這個套接字是用來監聽客戶端連接請求的)。
(2)調用bind()綁定函數,將創建成功的套接字與需要監聽的IP位址和Port綁定。
(3)綁定成功後,就可以調用listen()函數進行監聽,等待客戶端的連接請求。(服務端需要成功調用listen()函數,客戶端才可以發起連接請求,否則,客戶端的連接會出錯)
(4)調用listen()函數成功後,若此時有客戶端申請建立連接,服務端則調用accept()函數,接收客戶端的連接,並自動產生用於網絡I/O通信的套接字,作為accept()函數的返回值。(accept()函數自動產生的套接字是用來進行網絡I/O數據收發的,與socket()套接字不同)
(5)當客戶端連接成功後,服務端就可以基於accept()函數返回的套接字,使用系統調用的讀寫函數read()/write()進行數據收發了。
使用嵌入式QT進行TCP/IP的網絡通信應用程式開發,對於TCP服務端,QT的network類庫提供的QTcpServer類,這個類繼提供了一系列的服務端網絡操作接口函數,如:監聽函數listen(),阻塞等待客戶端連接waitForNewConnection(),虛函數incomingConnection()用來處理客戶端的連接請求。更多接口函數,具體可以參閱 QtNetwork/qtcpserver.h 文件的內容。在服務端應用程式裡面,我們通常建立一個繼承於QTcpSocket的類,來描述每一個連接成功的客戶端,每一個客戶端的具體信息,可以通過這個類來獲取。關於這個類的使用方法,請參考上一章節的內容。
以下是應用程式的開發過程
1、 先用Qt Creator構建一個工程,命名為:008_tcp_server,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、 創建工程後,修改008_tcp_server.pro裡面的內容,添加QT裡面的網絡通信模塊network,使工程支持QT網絡類的調用,如下圖所示。
3、 雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
服務端界面描述如上面內容所示,這裡不作重複。
4、 對於一個合格的TCP服務端,由於要給多個TCP客戶端提供服務,因此,難以避免處理多個客戶端連接,以及對指定的客戶端進行數據收發。因此,TCP服務端需要一個TcpClient類來描述每個客戶端的信息,這個TcpClient類繼承於QTcpSocket,注意,這個TcpClient類與上一章節的TcpClient類有些差異。它是用來描述客戶端,而非創建一個客戶端,如下圖所示:
5、 創建好TcpClient類之後,對於服務端,我們還需要創建一個TcpServer類,用來描述整個服務端的操作,這個TcpServer類包含了服務端的數據收發函數,連接成功的客戶端列表QList<TcpClient *> tcp_clients,啟動和停止服務端的監聽,重載void incomingConnection(int handle);虛函數,等等。TcpServer類的具體定義,如下圖所示:
6、 以上的兩個類創建完成後,我們創建一個tcp_server.cpp文件,這個文件主要是用來編寫Tcp-Client類和Tcp-Server類裡面各個函數的具體實現,關於整個tcp_server.cpp文件的具體內容,請參閱源碼,裡面有詳細的注釋。以下列出關鍵的函數進行講解。
7、 Tcp-Client類是用來描述每個連接服務端成功的客戶端的,這個類的構造函數,如下圖所示:
構造函數主要綁定了錯誤相關的槽函數,通信斷開槽函數,以及數據接收的槽函數。當服務端底層I/O接收到客戶端發送上來的數據的時候,就會調用slot_read_data()進行處理。
8、 對於每個連接服務端成功的客戶端,服務端可以使用以下的函數,對其進行數據的接收和發送,函數實現如下圖所示:
對於數據發送函數void TcpClient::slot_send_data(const QString &data),主要是把需要發送的數據轉為QByteArray,然後調用QIODevice::write()函數進行發送,發送完成後,調用信號函數signals_send_data(client_ip, client_port, data); 通知widget類,是向哪個客戶端發送了哪些數據。
對於數據接收函數void TcpClient::slot_read_data(),主要是跟底層I/O的讀數據信號readyRead()進行綁定,當接收到服務端的數據時,會調用QIODevice::readAll()函數,把緩衝區的數據全部讀出,然後通過signals_receive_data(client_ip, client_port, buffer);信號,把客戶端的IP位址,埠,數據內容都發送出去。
9、 Tcp-Server類主要用來描述Tcp服務端的,裡面實現了服務端的開始監聽與停止監聽的函數,如下圖所示:
主要調用listen函數,啟動伺服器監聽所有地址的客戶端連接。對於停止伺服器監聽,則先斷開所有的客戶端連接後,調用close()函數關閉服務端監聽。
10、 伺服器的監聽函數成功啟動後,對於客戶端發起的連接請求,服務端會調用一個虛函數void TcpServer::incomingConnection(int handle)進行處理,這個虛函數的具體實現,如下圖所示:
當有客戶端的連接請求,這個函數就會被調用,在這個函數裡面,保存客戶端的設備描述符,這個描述符是服務端和客戶端通信的基礎。然後綁定各個槽函數,分別是斷開連接槽函數,數據收發槽函數,然後把連接上來的客戶端IP和埠保存下來。在發送信號通知ui界面,有客戶端上線了。最後,把這個客戶端對象保存在服務端的tcp_clients列表中。
11、 對於客戶端主動斷開連接,調用以下函數進行處理:
這個槽函數是在void TcpServer::incomingConnection(int handle)裡面,與TcpClient類的disconnected()進行綁定的,當客戶端連接斷開後,函數會發送信號給ui界面,告知客戶端下線,然後把客戶端從tcp_clients列表中移除。
12、 服務端可以對指定的客戶端發送數據,也可以對所有的客戶端發送數據,具體的函數實現,如下圖所示:
對指定的客戶端發送數據,則先從客戶端列表中獲取匹配的IP位址和埠,獲取成功後,調用TcpClient::slot_send_data()函數發送數據。對所有的客戶端發送數據,則不用匹配IP位址和埠,直接對列表中所有的客戶端發送數據。
13、 TcpClient類和TcpServer類的具體實現已經介紹完畢,在這裡,我們就可以在界面的構造類Class Widget裡面,基於TcpServer構建服務端應用。Widget類是ui的界面類,關於ui界面的操作,都在該類實現,Widget類的構造函數,如下圖所示:
在這個構造函數裡面,主要定義了一個tcp_server對象,然後把這個tcp_server對象裡面的各種信號與相關的槽函數進行綁定,對界面的按鈕控制項進行初始化,定義一個定時器,用來自動發送數據,最後,在界面顯示本機的IP位址。
14、 widget.cpp還實現了按鈕控制項和顯示控制項的相關功能,具體的實現函數,請參考widget.cpp的源碼文件,部分源碼如下圖所示:
15、 至此,整個TCP服務端已經開發完畢,編譯成功後,下載到開發板運行,應用程式運行後如下圖所示。
16、 以上只是實現了一個簡單的TCP服務端應用程式,並在單個線程裡面處理了少量的TCP客戶端連接,對於大規模的TCP服務端應用程式,還需要考慮高並發,數據低延遲,如何管理大規模的客戶端數量,保證服務端7*24小時運行不宕機,等等,這些大規模的服務端程序,都是運行在性能較高的硬體上。在硬體性能滿足的前提下,如果嵌入式設備需要管理比較多的客戶端連接,建議採用線程池的管理方式,對客戶端分批採用線程池管理,即可達到多客戶端管理,更多的網絡服務應用技術,需要開發者不斷在工程應用中不斷優化,不斷積累經驗,才能不斷進步。
(9)基於UDP協議的網絡通信應用程式(UDP-Socket)前兩篇文章介紹了基於TCP/IP協議的網絡通信應用程式。相比起TCP/IP協議的可靠,面向連接,基於字節流通信這些特性,UDP協議是一種輕量級,不可靠,基於數據報的傳輸協議。
很多人會問,為什麼UDP協議傳輸不可靠還要繼續使用它?那是因為UDP協議使用起來很方便,不需要建立連接,資源消耗少,通信效率高,在線播放音頻或者視頻的時候,使用UDP協議比使用TCP/IP協議有更高的傳輸效率,因為在這種使用場景下,即使丟失一兩個數據包,對結果都影響不大。在網絡質量不佳的情況下,使用UDP協議傳輸可能丟包嚴重,開發者應該注意根據不同的場合選擇合適的協議。
對於UDP傳輸協議,數據報有以下三種傳輸方式:
單播:UDP單播傳輸,即發送方指定接收方的IP位址和埠號,把數據直接發送到對方。這種屬於一對一的數據傳輸方式。
組播:接收方需要加入組播地址和埠號,發送方往組播的IP位址發送數據,只要是加入了組播地址的設備,都能收到數據,屬於一對多的數據傳輸方式。
廣播:接收方不需要加入廣播地址,只指定接收的埠號,發送方往廣播的IP位址發送數據,則區域網內的所有設備都能收到數據,屬於一對多的數據傳輸方式。
總的來說,不管是單播,組播,還是廣播,對於發送方,每一次數據傳輸都需要指定接收方的IP位址和埠。而對於接收方,單播和廣播的接收方式是一樣的,都是監聽區域網內所有的IP位址,組播則多了一個步驟,需要加入指定的組播地址才能接收組播數據。
使用嵌入式QT進行UDP協議的網絡通信應用程式開發,QT的network類庫提供了QUdpSocket類,這個類繼承於QAbstractSocket類,並向應用層提供了UDP通信的基本操作方法,如綁定函數bind(),加入組播joinMulticastGroup(),離開組播leaveMulticastGroup(),數據報的讀寫操作函數,等等。具體可以參閱 QtNetwork/qudpsocket.h 文件的內容。
目標:
使用QT提供的UDP網絡通信類,實現一個簡單的Udp_Socket收發工具。
功能:
(1)界面顯示開發板本機IP位址。
(2)可手動輸入接收端的IP位址和埠。
(3)可選擇UDP數據報的發送方式:組播,單播,廣播。
(4)界面顯示UDP協議的網絡收發數據,並提供清屏按鈕。
(5)提供手動發送按鈕和自動發送按鈕。
開發板運行Udp_Socket收發工具後,界面如下圖所示:
界面描述:
(1)Udp_Socket收發工具運行後,在界面顯示開發板的IP位址。
(2)可輸入接收端的IP位址和埠。
(3)選擇UDP的發送方式,單播,組播或廣播。
(4)點擊 [tcp_send_data] ,往指定的IP位址和埠發送數據。
(5)點擊 [START_AUTO_SEND] ,以一秒的頻率間隔發送數據。
以下是應用程式的開發過程。
1、 先用Qt Creator構建一個工程,命名為:009_udp_socket,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。
2、 創建工程後,修改009_udp_socket.pro裡面的內容,添加QT裡面的網絡通信模塊network,使工程支持QT網絡類的調用,如下圖所示。
3、 雙擊打開「widget.ui」文件,構建界面,構建後的界面如下圖所示:
應用程式界面描述如上面內容所示,這裡不作重複。
4、 對於Udp_Socket收發工具,我們使用一個類UdpSocket對其進行封裝,這個類定義了UDP數據的收發函數,以及獲取自身IP位址的函數,還定義了一個信號,用來通知ui界面接收到數據並顯示。UdpSocket類的具體內容如下圖所示:
5、 對於UdpSocket類,在其構造函數裡,綁定監聽的IP位址和埠號,並禁止在組播發送時自己接收數據。綁定QUdpSocket()類的readyRead()信號,一旦發現數據即調用槽函數進行處理。UdpSocket類的構造函數如下圖所示。
6、 當應用程式接收到UDP的數據報時,底層系統就會觸發readRead()信號,進而調用數據接收的槽函數void UdpSocket::slot_udp_read_data()進行處理。數據接收槽函數如下圖所示:
這個函數不斷讀取緩衝區的數據,讀取完成後,發送信號通知ui界面進行數據顯示。
7、 應用程式進行UDP數據發送時,可以調用QUdpSocket::writeDatagram()進行數據發送,對於單播,組播,廣播這三種發送方式,需要對應不同的IP位址,UDP數據發送函數的具體實現,如下圖所示:
8、 以上就是UdpSocket類的主要函數實現,接下來,分析一下窗體構造Widget類的具體實現過程。
9、 在Widget類的構造函數裡,我們使用UdpSocket類定義一個udp_socket對象,並且綁定數據處理的槽函數。構建一個定時器,用於自動發送數據。顯示本機的IP位址。構建一個QButtonGroup類對象,並使界面上的radioButton實現互斥。如下圖所示:
10、 應用程式可以自由選擇(單播,組播,廣播)這三種發送方式的其中一種,可以通過使用radioButton來實現互斥,當發送方式發生改變時,調用以下槽函數進行處理。
11、 當應用程式接收到UDP數據時,會調用void Widget::slot_recv_udp_data()函數進行處理,在這個函數裡面,把接收到的數據以及數據來自哪個IP位址,都顯示出來。
12、 以上就是整個Udp_Socket應用程式的開發過程,相比起TCP/IP,使用UDP協議進行數據傳輸是比較方便快捷的,開發者應該根據不同的應用場景選擇不同的數據傳輸協議,每種協議都各有利弊,合適的就是最好的。
13、 把應用程式下載到開發板上運行,如下圖所示:
(10)NXP i.MX6UL基於嵌入式QT實現電容屏多點觸控基於i.MX6UL平臺,使用嵌入式QT實現電容屏的多點觸控,前提是開發板的電容觸控螢幕驅動已經支持多點觸控,並且驅動程序能通過事件方式向應用程式上報觸控數據。關於電容觸控螢幕多點觸控驅動程序的介紹,不在本章節描述之列。本章節重在實現多點觸控的QT應用程式。
嵌入式QT電容屏多點觸控應用程式,是基於qTUIO庫和mtdev庫來實現的。qTUIO是國外一位開發者編寫的第三方庫,這個第三方庫在本地UDP套接字上實現了一個基於TUIO協議的監聽器,然後把監聽到的事件轉發到QT的內部事件系統。mtdev是一個獨立的庫,它可以將來自內核的MT事件轉換為時隙類型的B協議,這些MT事件可能來自內核中所有的MT設備。
總的來說,mtdev庫可以把內核上報的多點觸摸事件,通過mtdev2tuio後臺應用程式(後面會講到這個應用程式的移植),轉換為TUIO協議,然後QT應用程式可以使用qTUIO庫對內核上報的觸摸事件進行處理。
以下是多點觸控應用程式(塗鴉畫板)運行時的界面:
界面描述:
(1)手寫板界面,可以用多個手指同時塗鴉,最多支持5點同時觸摸。
(2)Options裡面有clear screen按鈕,可以進行清屏。
本章節主要分以下兩個步驟進行講述:
(1) 在i.MX6UL開發板上移植qTUIO庫,使其支持嵌入式QT多點觸摸應用程式開發。
(2) 基於qTUIO庫,編寫一個QT多點觸控應用程式。(手寫板應用程式)。
一、在i.MX6UL開發板上移植qTUIO庫。
在i.MX6UL開發板上移植qTUIO庫,需要依賴以下的第三方庫:liblo,mtdev,mtdev2tuio,qTUIO。通過以下網址,下載以上的第三方依賴庫:
https://github.com/x29a/qTUIO
https://github.com/olivopaolo/mtdev2tuio
http://bitmath.org/code/mtdev/
http://liblo.sourceforge.net/
如果不能訪問以上網址,也可以使用作者下載好的源文件:
連結:https://pan.baidu.com/s/17l6LonZuh8U7axVRQ4TV-w
提取碼:gvo7
1、 下載完成後,把下載好的文件,通過FileZilla工具上傳到ubuntu系統,作者上傳到ubuntu系統的/opt/work/tools/qt_multitouch/目錄,以上依賴庫的交叉編譯也是基於該目錄進行,把下載好的文件進行解壓,並創建好各個依賴庫的安裝目錄,如下圖所示。
以上依賴庫的交叉編譯順序是:liblo --> mtdev --> mtdev2tuio --> qTUIO
交叉編譯完的文件,統一存放在 imx6ul-rootfs目錄,方便移植到開發板。
2、 在ubuntu系統的命令行終端,執行以下命令,配置交叉編譯環境。
#export CC=/opt/EmbedSky/gcc-linaro-5.3.1-2016.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc
#export CXX=/opt/EmbedSky/gcc-linaro-5.3.1-2016.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++
3、 先交叉編譯liblo庫,執行以下命令進行交叉編譯:
#cd /opt/work/tools/qt_multitouch/liblo-0.26
#export SKIP_RMDIR_CHECK=yes
#./configure --prefix=/opt/work/tools/qt_multitouch/liblo_install --host=arm
#make clean
#make
#make install
交叉編譯成功後會在liblo_install目錄下生成bin、include、lib目錄,如下圖所示:
執行以下命令,把生成的三個目錄都複製到imx6ul-rootfs文件夾:
#cp /opt/work/tools/qt_multitouch/liblo_install/* /opt/work/tools/qt_multitouch/imx6ul-rootfs/ -a
4、 然後,再交叉編譯mtdev庫,執行以下命令進行交叉編譯:
#cd /opt/work/tools/qt_multitouch/mtdev-1.1.3
#./configure --prefix=/opt/work/tools/qt_multitouch/mtdev_install --host=arm
#make clean
#make
#make install
交叉編譯成功後會在mtdev_install目錄下生成bin、include、lib目錄,如下圖所示:
執行以下命令,把生成的三個目錄都複製到imx6ul-rootfs文件夾:
#cp /opt/work/tools/qt_multitouch/mtdev_install/* /opt/work/tools/qt_multitouch/imx6ul-rootfs/ -a
5、 接著,再交叉編譯mtdev2tuio-master後臺程序,mtdev2tuio後臺程序是連接mtdev與qTUIO的橋梁,在運行基於qTUIO庫編寫的應用程式前,需要先運行mtdev2tuio,與以上兩個依賴庫不同,mtdev2tuio-master源碼裡面已經有Makefile文件,修改/opt/work/tools/qt_multitouch/mtdev2tuio-master/Makefile內容,交叉編譯過程可以基於此Makefile進行,如下圖所示:
修改完Makefile文件後,執行以下命令,進行交叉編譯:
#cd /opt/work/tools/qt_multitouch/mtdev2tuio-master
#make clean
#make
交叉編譯完成後,會在源碼目錄下生成mtdev2tuio應用程式,如下圖所示:
執行以下命令,把mtdev2tuio應用程式複製到 imx6ul-rootfs的bin目錄:
#cd /opt/work/tools/qt_multitouch/mtdev2tuio-master/
#cp ./mtdev2tuio /opt/work/tools/qt_multitouch/imx6ul-rootfs/bin -a
6、 最後,交叉編譯qTUIO庫。qTUIO庫是一個QT工程,其工程的.pro文件以及源碼目錄是:/opt/work/tools/qt_multitouch/qTUIO-master/src,可以使用qmake工具進行生成Makefile文件,再交叉編譯。執行以下命令,進行交叉編譯。
#cd /opt/work/tools/qt_multitouch/qTUIO-master
#rm -rf /opt/work/tools/qt_multitouch/qTUIO-master/lib/*
#cd src/
#/opt/EmbedSky/TQIMX6UL/TQ_COREB/imx6ul_rootfs/opt/qt-4.8.7/bin/qmake
#make clean
#make
交叉編譯成功後,會在/opt/work/tools/qt_multitouch/qTUIO-master/lib文件夾下生成qTUIO的動態連結庫,如下圖所示:
執行以下命令,把這些動態連結庫,複製到imx6ul-rootfs的lib目錄:
#cd /opt/work/tools/qt_multitouch/qTUIO-master/
#cp ./lib/* /opt/work/tools/qt_multitouch/imx6ul-rootfs/lib -a
7、 完成以上工作後,imx6ul-rootfs目錄下各個文件夾裡面的內容分別如下圖所示:
imx6ul-rootfs/bin目錄的內容
imx6ul-rootfs/include目錄的內容
imx6ul-rootfs/lib目錄的內容
8、 最後,把imx6ul-rootfs/bin目錄的文件複製到i.MX6UL開發板的/bin目錄,把imx6ul-rootfs/include目錄的文件複製到i.MX6UL開發板的/include目錄,把imx6ul-rootfs/lib目錄的文件複製到i.MX6UL開發板的/usr/lib目錄,複製完成後,如下圖所示:
9、 至此,qTUIO庫已經全部移植到i.MX6UL開發板,開發板已經支持運行基於qTUIO庫編寫的多點觸摸應用程式,為了方便應用程式運行前不用手動運行mtdev2tuio後臺程序,我們把該後臺程序設置為開機自啟動,修改 /etc/embedsky_env文件,如下圖所示:
/bin/mtdev2tuio /dev/input/cap_ts & 表示監控 /dev/input/cap_ts節點,該節點是電容觸控螢幕的設備節點,內核裡面的的電容觸摸事件都是通過該節點進行上報,應用程式通過該節點捕獲電容觸摸事件,然後進行處理。
二、基於qTUIO庫編寫QT多點觸控應用程式
1、 先用Qt Creator構建一個工程,命名為:010_multi_touch,關於如何構建工程,請參考「第一個嵌入式QT應用程式」的具體內容。與以往的程序不同,此次構建的工程,基類選擇QMainWindow類。
2、 在編寫代碼前,我們需要把上一步驟移植好的qTUIO動態庫和頭文件複製到工程中,複製完成後,如下圖所示:
其中,3rdparty目錄直接從 /opt/work/tools/qt_multitouch/qTUIO-master/src 複製,並刪除目錄裡面所有的非 .h 文件,只保留 .h 頭文件。
3、 在工程目錄上,右鍵 --> 添加庫,如下圖所示:
4、 選擇添加「外部庫」,點擊下一步,在彈出的對話框中,平臺選擇Linux,庫文件選擇剛剛添加進工程的libqTUIO.so,然後點擊下一步,如下圖所示:
5、 動態連結庫添加完成後,010_multi_touch.pro文件如下圖所示:
6、 為了方便在界面上進行塗鴉繪畫,我們創建一個ScribbleArea類,這個類繼承於QWidget類,包含了清屏函數,以及重構的繪畫事件函數,窗口大小調整函數,系統事件捕獲函數,等等。類的具體內容如下圖所示:
7、 在ScribbleArea類的構造函數中,我們設置該類對象可以捕獲觸摸事件,並初始化設置畫筆的顏色,該類的構造函數如下圖所示:
8、 當系統有觸摸事件上報時,應用程式會調用bool ScribbleArea::event(QEvent *event)函數,實際上,這個被重構的event函數不僅僅只在觸摸事件發生時被調用,對於一般的系統事件,這個函數也會被調用,在這個event函數裡面,我們只處理QEvent::TouchBegin,QEvent::TouchUpdate,QEvent::TouchEnd事件,其他時間忽略不進行處理。event函數的內容如下圖所示:
在該函數裡面,只處理三種觸摸事件,先獲取觸摸事件裡面的所有觸摸點,然後根據該觸摸點的坐標和矩形大小,繪製橢圓點,然後調用update函數更新繪製的區域,update函數被調用的時候,void ScribbleArea::paintEvent(QPaintEvent *event)事件函數就會被觸發。
9、 圖形會在void ScribbleArea::paintEvent(QPaintEvent *event)事件函數裡面繪製,函數的內容如下圖所示:
10、 在MainWindow的窗體構造函數裡實例化一個ScribbleArea對象,並把該對象設置為MainWindow的中心窗體,然後繪製一個菜單欄,並重置窗體的大小。代碼如下圖所示:
11、 在應用程式的入口int main(int argc, char *argv[])函數裡面,使用QTuio類實例化一個對象qTUIO,該對象引用了MainWindow類對象進行實例化,然後運行qTUIO.run()函數,這樣,qTUIO就可以監聽系統的多點觸摸事件,當系統有多點觸摸事件上報時,qTUIO會把這些事件轉換為Qt內部可以處理的系統事件,方便Qt應用程式進行處理,main函數的內容如下圖所示:
12、 至此,整個多點觸摸應用程式已經開發完成,編譯下載到開發板,應用程式運行的現象如下圖所示。
關於qTUIO庫的實現機制和實現原理,作者沒有進行太多深入的研究和分析,網上對於該庫的講解資料也比較少。根據移植過程的分析,大概可以推斷系統的MultiTouch事件會被mtdev庫捕獲,然後這些事件會通過mtdev2tuio通過TUIO協議轉發到qTUIO庫,最後qTUIO庫把這些多點觸摸事件轉換為Qt系統內部可處理的事件。qTUIO庫的原始碼裡提供了這個庫的使用例程,例程源碼在 qTUIO-master/examples/ 目錄裡面,感興趣的開發者可以進行移植測試。如對qTUIO庫有比較深入的研究,或者發現以上的移植過程有紕漏,歡迎聯繫作者並指正,謝謝。
-- END --
專輯 | 【開源】嵌入式物聯網應用開發
專輯 | 嵌入式Linux應用程式開發
專輯 | 嵌入式Linux開發環境搭建
專輯 | 物聯網BLE裸機程序開發
專輯 | 物聯網BLE應用程式開發
專輯 | RT-Thread學習筆記
專輯 | RT-Thread & Bear-Pi 開發筆記系列
關注並回復【技術文檔】,可下載所有專輯的PDF文檔