基於RT-Thread操作系統的 基礎四輪組智能車設計與實踐

卓晴 2021-08-15 12:52:00 阅读数:261

本文一共[544]字,预计阅读时长:1分钟~
rt-thread rt thread 操作 智能

學 校: 同濟大學
隊伍名稱: 智行·龍卷風
參賽隊員: 楊怡,韋炳宇,許澤華
帶隊教師: 張志明,餘有靈

 

§01


1.1全國大學生智能車競賽介紹

全國大學生智能汽車競賽是以智能汽車為研究對象的創意性科技競賽,是面向全國大學生的一種具有探索性工程實踐活動,是教育部倡導的大學生科技競賽之一。該競賽以“立足培養,重在參與,鼓勵探索,追求卓越”為指導思想,旨在促進高等學校素質教育,培養大學生的綜合知識運用能力、基本工程實踐能力和創新意識,激發大學生從事科學研究與探索的興趣和潜能,倡導理論聯系實際、求真務實的學風和團隊協作的人文精神,為優秀人才的脫穎而出創造條件。

該競賽過程包括理論設計、實際制作、整車調試、現場比賽等環節,要求學生組成團隊,協同工作,初步體會一個工程性的研究開發項目從設計到實現的全過程。競賽融科學性、趣味性和觀賞性為一體,是以迅猛發展、前景廣闊的汽車電子為背景,涵蓋自動控制、模式識別、傳感技術、電子、電氣、計算機、機械與汽車等多學科專業的創意性比賽。

1.2全國大學生智能車競賽基礎四輪組介紹

基礎四輪組比賽是在PVC賽道上進行,官方比賽賽道示意圖如圖1.1和圖1.2中所示,賽道采用黑色邊線和電磁進行導引。

▲ 圖1.1 第十六届全國大學生智能車競賽基礎四輪組

▲ 圖1.1 第十六届全國大學生智能車競賽基礎四輪組

▲ 全國大學生智能車競賽基礎四輪組賽場

▲ 全國大學生智能車競賽基礎四輪組賽場

選手制作的車模完成從車庫觸發沿著賽道運行兩周,然後在返回車庫。賽道元素包含多個環島、岔路、坡道、斑馬線、車庫等。車模需要連續運行兩圈,每次需要分別通過三岔路口的兩條岔路。比賽時間從車模駛出車庫到重新回到車庫為止。如果車模沒有能够停止在車庫內停車區內,比賽時間加罰五秒鐘。為此參賽車模需要無誤地判斷出賽道元素並順利通過,以最短的時間完成比賽。

1.3 RT-Thread操作系統介紹

1.3.1 嵌入式實時操作系統

嵌入式實時操作系統(Embedded Real-time Operation System,一般稱作RTOS),是指當外界事件或數據產生時,能够接受並以足够快的速度予以處理,其處理的結果又能在規定的時間之內來控制生產過程或對處理系統作出快速響應,並控制所有實時任務協調一致運行的嵌入式操作系統 。其主要特點是提供及時響應和高可靠性,核心是任務調度,任務調度的核心是調度算法。主流的RTOS國外的有μClinux、μC/OS-II、eCos、FreeRTOS等,國內的有RT-Thread、Huawei LiteOS等。

1.3.2 RT-Thread 概述

RT-Thread,全稱是 Real Time-Thread,是一款我國具有完全自主知識產權的開源嵌入式實時多線程操作系統,3.1.0 以後的版本遵循 Apache License 2.0 開源許可協議。它的基本屬性之一是支持多任務,事實上,多個任務同時執行只是一種錯覺,一個處理器核心在某一時刻只能運行一個任務。由於每次對一個任務的執行時間很短、任務與任務之間通過任務調度器進行非常快速地切換(調度器根據優先級决定此刻該執行的任務),就造成了這樣一種錯覺。在 RT-Thread 系統中,任務是通過線程實現的,RT-Thread 中的線程調度器也就是以上提到的任務調度器。經過了15年的迭代和豐富,伴隨著物聯網的興起,現已經成為市面上裝機量最大(超6億臺)、開發者數量最多、軟硬件生態最好的物聯網操作系統之一。

1.3.3 RT-Thread 架構

RT-Thread主要采用C語言編寫,容易理解,移植方便,同時具有良好的可剪裁性,其架構圖如圖1.3中所示。Nano 版本(極簡內核)僅需要 3KB Flash、1.2KB RAM 內存資源,適用於資源受限的微控制器(MCU)系統。完整版可以實現直觀快速的模塊化裁剪,無縫地導入豐富的軟件功能包,實現類似 Android 的圖形界面及觸摸滑動效果、智能語音交互效果等複雜功能,適用於資源豐富的物聯網設備。相較於 Linux 操作系統,RT-Thread 體積小,成本低,功耗低、啟動快速。除此以外 RT-Thread 還具有實時性高、占用資源小等特點,非常適用於各種資源受限(如成本、功耗限制等)的場合。

▲ 圖1.3 RT-Thread系統的架構

▲ 圖1.3 RT-Thread系統的架構

其中RT-Thread 內核,是 RT-Thread 的核心部分,包括了多線程及其調度、信號量、郵箱、消息隊列、內存管理、定時器等。

1.3.4 RT-Thread與智能車競賽

對於第十六届智能車競賽來說,MM32SPIN27、CH32V103由於內存較小,因此主要適配RT-Thread Nano版本的,這樣可以减少RAM的開銷。RT1064、RT1021、MM32F3277在智能車系統開發過程中使用RT-Thread的好處在於一方面可以充分發揮不同芯片的性能,讓智能車跑的更加順暢,另一方面提供了更高程度的抽象,屏蔽了不同單片機底層硬件細節,使得代碼邏輯更為清晰,編寫調試效率更高,移植性也更好。

1.4 智能車制作情况與本報告框架

經過近一年的准備,我們在小車機械結構設計、硬件電路設計、軟件算法設計等方面都有收獲和進展,靈活應用了RT-Thread系統的多線程並發、軟定時器、線程間同步——信號量、線程間通信——郵箱、時間片輪轉等特性,制作出了結構合理、系統穩定、可以順利完成比賽任務的基礎四輪小車。憑借著穩定的發揮,我們一路沖出校賽,在華東分賽區的預賽與决賽階段都成功完成任務,取得預賽第12、决賽第7的較好成績,最終排名為華東賽區基礎四輪組第7名,决賽成績103.007s。

本技術報告的框架大致如圖1.4所示。

▲ 圖1.4 技術報告框架結構

▲ 圖1.4 技術報告框架結構

在第一章對全國大學生智能車競賽、基礎四輪組別、RT-Thread系統進行介紹,簡單說明智能車制作的情况和報告框架。第二章介紹了智能車機械結構和基本硬件電路設計,第三章說明基礎四輪車軟件算法開發的基本流程,將傳統大while()+中斷模式與基於RT-Thread系統模式開發進行比較,得出後者具有巨大優勢的結論,並詳細介紹如何利用RT-Thread的多線程管理、定時器、郵箱、信號量等內核特性去更好的完成小車任務,總結部分進一步深入,講述如何利用模塊化思維、系統級的通信方式進行嵌入式任務的開發。第四章介紹RT-Thread操作系統的學習和遷移過程,其中4.3.4小節重點介紹了FinSH的詳細移植過程。第五章對本次智能車制作和比賽過程進行總結與反思。

 

§02 能車硬件


智能車的機械結構和控制電路對賽車的性能影響巨大。具備一個好的機械結構平臺,車身轉向的靈敏性、直道行駛的穩定性、較高速度的抓地性才能得到很好地實現。因此,我們在不違反比賽規則的情况下對小車的結構進行設計和改進以使其具有良好的機械性能。同時,我們在整個系統設計過程中嚴格按照競賽規範進行,本著可靠、高效的原則,在滿足各個要求的情况下,盡量使設計的電路簡單,PCB的效果簡潔。

2.1 小車機械結構總體布局

第十六届全國大學生智能車競賽基礎四輪組指定采用新B車模,車架長28.5cm,寬16.5cm,高6.0cm; 底盤采用高强度玻纖板,具有較强的彈性和剛性;前輪調整方式簡單,全車滾珠軸承、前後輪軸高度可調。驅動電機為RS540,伺服電機為SD-5舵機;輪胎經過軟化劑處理,增强其耐磨性和摩擦力,車模整體質量較輕,車模照片分別如圖2.1、圖2.2、圖2.3所示:

▲ 圖2.1 四輪車模俯視圖

▲ 圖2.1 四輪車模俯視圖

▲ 圖2.2 車模側視圖與前視圖

▲ 圖2.2 車模側視圖與前視圖

車模機械結構具有如下特點:

  • 舵機采用豎直姿態,方便控制;
  • 更換新的舵機連接杆,提高舵機控制的靈敏性;
  • 元件及電池布局在車身中心部分,提高車子的對稱性;
  • 對前輪進行調整,在保證直行穩定性的同時具有較好的過彎能力;
  • 采用雙電感安裝板雙前瞻設計,一前一後,提高小車的前瞻性;
  • 安裝支架高度盡量降低,最大程度降低車身重心。

2.2小車組件的安裝

2.2.1電路板及電池的安裝

車模的驅動板安裝在車身的後側,主板安裝在車身中心偏後方,為使得整個車身的重心盡量落在車身中心以减少因速度快而產生甩尾和側滑的現象,將兩個電磁信號板安裝與主板左右對稱處,電池安裝於主板下方偏前處,無線串口和電磁信號板均采用直插式封裝,方便拆裝。具體如圖2.4所示:

▲ 圖2.4 四輪組車模電路板及電池安裝比特置

▲ 圖2.4 四輪組車模電路板及電池安裝比特置

2.2.2 轉向舵機的安裝

舵機采用豎式安裝的方法,便於調節舵機的比特置和控制轉向,安裝時,為了提高舵機控制的靈敏性,减少其延遲的時間,我們特地更換了新的舵機安裝支架,使得舵機的安裝比特置更加契合要求,同時調整轉向連杆的長度等因素,使得轉動時的扭矩能够達到最佳的轉向效果。

2.2.3 前輪的調節

為了進一步提高車的性能,在保證車身在直行穩定的情况下能够更加輕便的過彎,能够有較大的過彎角度,經過查閱資料和實踐的檢驗,我們最終采用前輪外傾,主銷後傾,前輪前束的機械結構 .

(1) 前輪外傾: 是指前輪安裝後,其上端向外傾斜,於是前輪的旋轉平面與縱向垂直平面間形成一個夾角,稱之為前輪外傾角。通過前輪外傾的調節,使得前輪外傾和主銷內傾相配合,可以减小主銷偏距,使轉向輕便。

▲ 圖2.5 起來跑外傾示意圖

▲ 圖2.5 起來跑外傾示意圖

(2) 主銷後傾: 在車身縱向平面內,主銷軸線上端略向後傾斜,這種現象稱為主銷後傾。在縱向垂直平面內,主銷軸線與垂線之間的夾角叫主銷後傾角。通過設置主銷後傾,可以保持小車直線行駛時的穩定性,並使小車轉彎後能自動回正。一般來說,後傾角越大,車速越高,車輪的穩定性越强。但是後傾角過大會造成轉向沉重,所以主銷後傾角不宜過大,通過實際的實踐體驗,我們最終將主銷後傾角設置為2°~3°。

▲ 圖2.6 主銷後傾示意圖

▲ 圖2.6 主銷後傾示意圖

(3) 前輪前束: 是指前輪前端面與後端面在汽車橫向方向的距離差,也可指車身前進方向與前輪平面之間的夾角,此時也稱前束角。通過選擇適當的前束角,可使前束引起的側向力與車輪外傾引起的側傾推力相互抵消,從而避免了額外的輪胎磨耗和動力的消耗,同時前輪前束還可以保證小車穩定的直線行駛,使轉向輪具有自動回正的效果。經過多次嘗試,智能車前輪前束的調整如圖2.7所示:

▲ 圖2.7 前輪前束示意圖

▲ 圖2.7 前輪前束示意圖

▲ 圖2.8 調整後的四輪組車模前輪前束

▲ 圖2.8 調整後的四輪組車模前輪前束

2.2.4 電磁電感安裝板的安裝與固定

為了使車具有較好的前瞻性,電磁電感安裝板應盡量前置,但若距離過長,在車身行駛的過程中便極容易丟線,為了在不丟線的同時盡量提高其前瞻性,我們采用的是長短前瞻結合的方式,當長前瞻丟線時,切換到短前瞻進行控制,這樣的好處的前瞻長,留給車的控制時間長,有更長的時間做出反應,提高了賽車的速度上限,同時短前瞻的應用又保證了車在過急彎的時候仍能够檢測賽道並繼續正常行駛。在具體安裝時,我們采用碳素杆進行固定,减少支架的重量對於車身對稱性和平衡性的影響。支架後半部分用兩個支架安裝板固定比特置,前半部分從車前再引出兩個固定支架,構成三角形結構,提高其穩定性,减少車在行進過程中支架的顛簸,進而提高電感測量數據的穩定性。具體安裝分別如圖2.9、圖2.10所示:

▲ 圖2.9 電磁從夢境安裝板是示意圖

▲ 圖2.9 電磁從夢境安裝板是示意圖

▲ 圖1.10 前側采用三角形結構固定

▲ 圖1.10 前側采用三角形結構固定

2.3硬件電路設計

硬件電路系統是智能車運動控制系統的核心組件,為保證智能車穩定運行的基礎,需要一個良好、穩定的硬件環境才能使得小車能平穩快速的行駛在比賽賽道上。

2.3.1 單片機系統

核心單片機子系統采用英飛淩半導體公司設計生產的TC264D芯片。該芯片采用雙核TriCore架構,最高主頻為200MHz,高達2.5MB的閃存與240KB的RAM,完全滿足智能車控制的算力需求。為方便使用與後續更換,我們使用了逐飛科技公司生產的TC264單片機系統板,原理圖如圖 2.11 所示:

2.3.2 電源模塊設計

硬件電路的電源由18650鋰電池提供(額定電壓7.4V,容量2000mAh)。由於不同電路模塊中所需要的工作電壓和電流量各不相同,所以我們采用了三個穩壓電路將電源電壓轉換成各模塊需要的電壓。

  1. 使用MIC29302WU芯片將電源電壓轉換成6V電壓,用於智能車舵機供電。輸出電壓計算公式為:
Vout=1.240*(R3/R5+1) (1)
電路如圖2.12所示:

▲ 圖2.12 直流6V穩壓電路原理圖

▲ 圖2.12 直流6V穩壓電路原理圖

  1. 使用LM1085輸出5V電壓,用於蜂鳴器,通信串口供電。LM1085芯片的輸入電壓為5.5V到10V時,輸出電壓的典型值為5V。電路如圖2.13所示:

▲ 圖2.13 直流5V穩壓電路原理圖

▲ 圖2.13 直流5V穩壓電路原理圖

  1. 使用TPS76833將5V電壓轉換成3.3V電壓,用於單片機、信號放大電路、顯示屏等模塊的供電。TPS76833芯片的輸入電壓為2.7V到10V時,輸出電壓為3.3V。電路如圖2.14所示:

▲ 圖2.14 直流3.3V穩壓電路原理圖

▲ 圖2.14 直流3.3V穩壓電路原理圖

2.3.3 信號采集及放大電路模塊的設計

對賽道上電磁信號的采集和處理是智能車最重要的模塊之一,根據變化的磁場信號做出靈敏的檢測對控制智能車在賽道上穩定運行起著至關重要的作用。

智能車比賽賽道鋪設有中心電磁引導線,其中通有20kHz,100mA的交變電流。根據麥克斯韋電磁場理論,交變電流會在周圍產生交變的電磁場。因此我們采用10mH的工字電感和6.8uF的小溫差電容組成串聯諧振電路,來實現對20kHz信號的選頻和將賽道的電磁信號轉換成電壓,從而完成對信號的采集。接下來就是對收集到的電壓信號進行濾波、放大、整流用於單片機ADC模塊轉換成數字量。放大電路和電感排布方案如圖2.15中所示。

▲ 圖2.15 電磁信號放大電路和電感排布方案

▲ 圖2.15 電磁信號放大電路和電感排布方案

在上圖的電感排布方案中,水平電感1、7主要用於檢測彎道,在小車進入彎道過程中,可以根據兩端感應電動勢值判斷智能車與賽道中心線的偏離方向及偏差量。電感4用於檢測賽道中心的電磁線,當智能車偏離賽道中心線不多時,
該電感的變化程度較小,當偏離程度較大時,該電感的感應電動勢值突然下降很快,因此可以根據該電感的變化情况更加精確地得出智能車與賽道中心線的偏離程度。電感3、4用於檢測岔路。電感2、6主要用於引導智能車入環島,在環島路段,靠近環島的那個豎直電感的感應電動勢會比遠離環島的豎直電感的感應電動勢大許多,可以使用這兩個電感引導智能車進入環島。

2.3.4 電機驅動電路

電機驅動電路采用雙極性PWM全橋電路,可實現電機正反轉以及可調占空比控制電機轉速。該電路能控制電機正反轉運行,具有啟動快、調速精度高、動態性能好、調速靜差小、調速範圍大;能加速、减速、刹車、倒轉;能在負載超過設定速度時,提供反向力矩、能克服電機軸承的靜態摩擦力,產生非常低的轉速等多方面優點。電路原理圖如圖2.16所示:

▲ 圖2.16 雙極性PWM全橋電機驅動電路

▲ 圖2.16 雙極性PWM全橋電機驅動電路

2.3.5 無線串口模塊

采用NRF24L01 2.4GHz無線串口模塊,用於主機與單片機之間的實時數據通信,實現實時觀察車模的運行情况與無線調參等操作,節約調試時間,提高調試效率。

2.3.6編碼器測速模塊

我們的智能車使用龍邱智能科技的512 線 mini 型編碼器,使用减速齒輪和聯軸器加載到小車的動力輪上,進行小車的測速,工作電壓範圍 3.3V-5V。單片機通過讀取編碼器脈沖數來實現對智能車速度的測量。

2.3.7 PCB設計與實物圖

設計各功能電路的PCB,打樣後的PCB設計圖和實物照片分別如下圖中所示,包括:電路底板(圖2.17),電磁信號放大板(圖2.18),電機驅動板(圖2.19),電磁信號放大板(圖2.20)。

▲ 圖2.17 電路板的PCB與實物圖

▲ 圖2.17 電路板的PCB與實物圖

▲ 圖2.18 電磁信號放大版PCB與實物圖

▲ 圖2.18 電磁信號放大版PCB與實物圖

▲ 圖2.19 電機驅動板的PCB與實物圖

▲ 圖2.19 電機驅動板的PCB與實物圖

▲ 圖2.20 電感安裝板的PCB與實物

▲ 圖2.20 電感安裝板的PCB與實物

 

§03 RT-Thread軟件


本章介紹基於RT-Thread的四輪組車模軟件設計。

本章首先基於四輪車任務背景介紹軟件算法,接下來分析裸機大while()+中斷型開發模式的架構,並引出為什麼要使用RT-Thread操作系統,後續部分從PID算法著手,相對全面地介紹基於RT-Thread的四輪車軟件算法設計的各個方面。

3.1 軟件算法背景介紹

四輪車組別的主要任務有數據采集類、信號處理算法類、人機交互類、控制類等,其中:

  • 數據采集類任務包括攝像頭圖像采集、電磁信號采集、小車編碼器數據采集等。
  • 信號處理算法類任務主要包括圖像處理(灰度、二值化),電磁信號處理(均值濾波、卡爾曼濾波、歸一化)等。
  • 人機交互類任務主要包括圖像顯示、按鍵、撥碼開關、LED、蜂鳴器、串口通訊等。
  • 控制類任務主要包括舵機與電機的控制,通過PID進行閉環計算,輸出PWM波進行打角和轉速的控制。
  • 其他的組別還會有更多的任務,如直立車車身姿態的控制、AI機器視覺等,此處和四輪車組任務無關,不再贅述。

這樣一來,一輛四輪車就具備了巡線的基本功能,再加上對特殊賽道元素的正確判斷,就能初步具備完賽的能力。

▲ 圖3.1 四輪車組別主要任務

▲ 圖3.1 四輪車組別主要任務

3.2 裸機大while()+中斷型與RT-Thread系統開發對比分析

3.2.1 裸機大while()+中斷模式介紹

傳統的裸機大while()+中斷模式又稱為前後臺系統,前臺指中斷,後臺指main()函數裏的主循環while(1)。初學編程的時候老師會强調,循環一定要有退出的條件,不可以死循環。但是在嵌入式開發不用操作系統的情况下,一般都是用main函數裏while(1)無限循環的方式去編程的。中斷可以打斷main()函數,保證一定的實時性,而中斷也可以被優先級更高的中斷打斷。

▲ 圖3.2 裸機大while() + 中斷模式

▲ 圖3.2 裸機大while() + 中斷模式

3.2.2 裸機大while()+中斷模式優劣勢分析

在嵌入式編程發展的早期,是用裸機大while()+中斷模式編程的。優點是上手容易,處理簡單任務綽綽有餘。
而隨著計算性能的提高,嵌入式編程的發展是從簡單到複雜、從單任務到多任務,加上物聯網的興起,要處理的任務也是越來越複雜。這種模式漸漸顯露出弊端。

  1. 函數可能變得非常複雜,並且需要很長執行時間。且中斷嵌套可能產生不可預測的執行時間和堆棧需求。
  2. 超級循環和 ISR 之間的數據交換是通過全局共享變量進行的,應用程序的程序員必須確保數據一致性。
  3. 超級循環可以與系統計時器(硬件計時器)輕松同步,但如果系統需要多種不同的周期時間,則會很難實現。
  4. 超過超級循環周期的耗時函數需要做拆分,增加軟件開銷,應用程序難以理解,超級循環使得應用程序變得非常複雜,因此難以擴展。

舉個例子,逐飛科技做過這樣一個實驗。while(1)函數裏放兩個任務,一個是流水燈,一個是顯示屏顯示攝像頭圖像。兩個任務分別運行時都非常流暢。但是同時運行時,圖像刷新就變得十分卡頓。這是因為流水燈的delay()時間比較長,這段時間內cpu什麼也不做,所以圖像的刷新頻率就顯而易見的降了下來。這是一個典型的例子。隨著小車工作的不斷推進,while(1)裏要放的任務可能越來越多,後面的任務不可避免會對前面的任務造成影響。只能按順序執行,而不能進行有效的優先級區分。如果用中斷,首先中斷的數量可能會不足(比如tc264D只有4個pit中斷),其次中斷的嵌套也會使程序變得更加複雜,增加維護的難度。最重要的是,有很多的delay(),在這期間,cpu是什麼也不做的,這是巨大的資源浪費,而且也會明顯影響一些任務的執行頻率。

當然,說了這麼多,只用裸機大while()+中斷模式能不能把車做好?答案是肯定的,曆史上的許多神車都是用這種模式做出來的。但是,對於大多數同學,如果有一種效率更高,優勢巨大的方法擺在面前,要不要用?答案也是肯定的。下面探討一下相對於裸機大while()+中斷模式,RT-Thread系統的巨大優勢。

3.2.3 RT-Thread系統的優劣勢分析

RT-Thread系統在1.3節已有詳細介紹。這裏著重對比傳統裸機大while()+中斷模式分析其優劣勢。

優勢有很多,正好克服了裸機大while()+中斷模式的劣勢。

更靈活的任務處理和更好的實時性。線程數量不受限制,優先級最大256個。首先RT-Thread系統先天就有著處理複雜任務、多任務並發的屬性。可以把不同的任務拆分成不同的線程,根據優先級讓系統自動調度,更好地可以對多任務進行區別對待。如果優先級配置得當,不同任務之間相互的影響可以降到最低。顯著的優勢在於,delay()時會將線程掛起,把cpu使用權交出去,這時候cpu可以處理其他任務,顯著提高cpu的使用率。

更方便的模塊化開發和團隊合作。如果是團隊協作開發,那麼可以各自寫各自的線程,最後匯總、配置優先級啟動即可。模塊化開發也是用了面向對象的觀點,屏蔽了一些底層的實現細節,可以更專注於所要解决的任務上,代碼邏輯更加清晰,後續的拓展和維護也相對省力。

可重用性。這個是比較顯著的優勢。不同的平臺編程邏輯可能有很大不同,就智能車而言,不同的組別平臺就各有不同,同一個組別每一届的平臺也可能會有變化。所以對於許多打算做兩年或想換組別的同學來說,就免去了痛苦的從頭開始的過程,直接一鍵無痛移植。對於其他的比賽或項目而言,如果RT-Thread系統對該平臺有適配,則熟悉的編程邏輯和風格可以讓同學更加遊刃有餘。

豐富的軟件生態。這一優點可能在智能車競賽中不那麼突出,但是如果做物聯網的一些比賽,豐富的第三方庫會讓人拍手稱快。也許目前智能車面對的任務還不够複雜,但任務越來越複雜是大趨勢,一些複雜的項目也是用RT-Thread系統處理的。通過做智能車熟悉了RT-Thread操作系統,也有利於未來自身嵌入式編程的發展。

劣勢則是使用有一定門檻,上手不如大while()+中斷模式那麼容易。不過花點時間了解一下嵌入式實時操作系統的一些特性,學習一下RT-Thread的內核使用,這個時間的投入還是非常劃算的。且由於RT-Thread系統本身比較淺顯易懂,再加上逐飛有比較詳細的講解和demo,所以學習和移植過程還是比較順利的。我從剛開始接觸RT-Thread到移植成功,只花了大半天時間,後續的調試又用了一個下午。對於有一點嵌入式開發經驗的同學來說,可以預留出一天半時間來初步學習和移植。當然要想精進使用是需要不斷學習的。所以同學不要覺得操作系統好像很高大上的樣子,不明覺厲。當你開始閱讀開發者文檔,動手操作以後,可能就會覺得還是比較容易的。

3.3 RT-Thread系統在四輪車的部署方案

將3.1軟件算法進一步抽象,總體可以概括為:“一核心,兩關鍵,多輔助”。“一核心”指運動控制器,“兩關鍵”指賽道環境的采集(電磁采集和攝像頭圖像采集、編碼器采集)和控制量的輸出(舵機和電機的PWM輸出),“多輔助”指的是通過按鍵、撥碼開關、led燈、顯示屏、蜂鳴器、串口等一系列手段增强調試效率。使用RT-Thread的線程調度、時鐘管理、線程間同步、線程間通信等內核特性,有效解决了裸機大while()加中斷形式帶來的管理單一、無法處理複雜多任務的執行、調試不方便等痛點,在抽象層面更高的平臺上,使得多任務運行更加得心應手,編程邏輯更加清晰,調試手段更加多樣靈活。

由於小車速度很快,且感知、决策、控制存在必然的順序,所以必須保證三者的周期性喚醒及執行的先後順序。所以用RT-Thread 操作系統提供的軟定時器,timer1_pit_entry線程周期運行,周期為1個系統節拍。在定時器入口函數裏面完成賽道環境的采集和處理,差比和計算,以及PWM波輸出。

顯示屏和串口作為display_entry線程,優先級設置為31。並且通過撥碼開關决定是否顯示。蜂鳴器作為buzzer_entry線程,優先級設置為20。並且通過郵箱决定是否響以及響的時間,郵箱大小為5個字節,采用 FIFO 方式進行線程等待。這裏顯示屏和串口優先級最低,而蜂鳴器優先級更高,比較合理。

利用FinSH在實際小車運行過程中,我們使用ps命令列出系統中所有線程信息,包括線程優先級、狀態、棧的最大使用量等,以此監測智能車的線程運行情况。

下圖3.3為大while+中斷控制邏輯,3.4為RTT多線程同步操作系統在四輪小車上的部署方案。

▲ 圖3.3 大 while +中斷算法邏輯

▲ 圖3.3 大 while +中斷算法邏輯

▲ 圖3.4 RT-Thread系統算法邏輯

▲ 圖3.4 RT-Thread系統算法邏輯

3.4 PID運動控制器

一核心指的是運動控制器,本次比賽我們采用經典控制算法PID控制,如圖3.5和圖3.6中所示,根據系統輸入與預定輸出的偏差的大小運用比例、積分、微分計算出一個控制量,將這個控制量輸入系統,獲得輸出量,通過反饋回路再次檢測該輸出量的偏差,循環上述過程,以使輸出達到預定值。

▲ 圖3.5 使用PID控制器的經典負反饋算法

▲ 圖3.5 使用PID控制器的經典負反饋算法

▲ 圖3.6 使用PID控制器的負反饋控制算法應用

▲ 圖3.6 使用PID控制器的負反饋控制算法應用

比特置式PID算法可由公式(2)錶達:

而增量式PID算法可由比特置式PID算法推導得到,如公式(3)中所示:

在PID算法中,比例環節P的作用是成比例地反映控制系統的偏差信號e(t),一旦產生偏差,比例控制環節立即產生控制作用以减小偏差。積分環節I的作用是消除靜差,提高系統的無差度,在使用積分控制時,常常對積分項進行限幅以减少積分飽和的影響。微分環節D的作用是反映偏差信號的變化趨勢,能够在偏差信號變化之前先引入一個有效的早期修正信號來提前修正偏差,加快系統的動作速度,减少調節時間。

比特置式PID特點主要是一方面控制的輸出與整個過去的狀態有關,用到了誤差的累加值(即用誤差累加代替積分),另一方面公式輸出直接對應對象的輸出,對系統影響大。

而增量式PID特點有不需要累加誤差,控制增量僅與最近幾次的誤差有關,不容易產生誤差累積,僅輸出控制量增量,誤動作影響小,不會嚴重影響系統。

兩者的主要區別在於,比特置式PID控制算法是一種非遞推算法,其輸出直接控制執行機構,輸出的量與執行機構的比特置(例如閥門的開關量)一一對應。增量式PID控制算法是一種遞推算法,輸出的只是控制量的增量,技術輸出的增量對應的是本次執行機構比特置的增量而不是實際比特置。PID控制並不一定要三者都出現,也可以只是PI、PD控制,關鍵决定於控制的對象。

舵機通常采用比特置式PD算法,對於舵機的控制,因為不需要記錄之前的誤差因此這裏將積分環節去掉,這樣公式就變為。舵機控制代碼實現如下:差因此這裏將積分環節去掉,這樣公式就變為。舵機控制代碼實現如下:

//保存上次誤差
last_elect_val = curr_elect_val;
//計算本次誤差
curr_elect_val=SET_POSITION-position;
elect_val_delta = curr_elect_val - last_elect_val; //電磁變化率
smotor_duty=(int16)(__skp*curr_elect_val + __skd*elect_val_delta);//進行PD運算
smotor_duty=(int32)limit_ab((int16)smotor_duty, LMAX_DUTY, RMAX_DUTY);//限幅,
pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER+smotor_duty);//控制舵機轉動

電機通常采用增量式PID算法,因為增量式PID不容易造成電機的正反轉切換,對速度的調節更為平滑。電機控制代碼實現如下:

ek2=ek1;//保存上上次誤差
ek1=ek;//保存上次誤差
set_speed=SPEED;
//進行增量式PID計算
out_increment=(int16)(kp*(ek-ek1)+ki*ek+kd*(ek-2*ek2+ek2));//計算增量
out +=out_increment; //輸出增量
out =limit(out,GTM_ATOM0_PWM_DUTY_MAX/2 ); //輸出限幅,不能超過占空比最大值
motor_duty=(int32)out; //强制轉換為整數後賦值給電機占空比變量
if(motor_duty>=0) //前進
{

pwm_duty(MOTOR2_CHANNEL, 0);
pwm_duty(MOTOR1_CHANNEL, motor_duty);
}
else //後退
{

pwm_duty(MOTOR1_CHANNEL, 0);
pwm_duty(MOTOR2_CHANNEL, -motor_duty);
}

(注:這裏只展示PID算法部分,變量定義及初始化略)

3.5 電磁信號采集和處理

3.5.1 ADC介紹

模擬數字轉換器即A/D轉換器,或簡稱ADC,通常是指一個將模擬信號轉變為數字信號的電子元件。通常的模數轉換器是將一個輸入電壓信號轉換為一個輸出的數字信號。由於數字信號本身不具有實際意義,僅僅錶示一個相對大小。故任何一個模數轉換器都需要一個參考模擬量作為轉換的標准,比較常見的參考標准為最大的可轉換信號大小。而輸出的數字量則錶示輸入信號相對於參考信號的大小。

3.5.2 ADC轉換過程

ADC轉換過程通常有采樣(取樣),保持,量化,編碼四個階段。

▲ 圖3.7 ADC轉換過程

▲ 圖3.7 ADC轉換過程

(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/adc/adc)

3.5.3 ADC轉換分辨率

通常以輸出二進制或十進制數字的比特數錶示ADC分辨率的高低,因為比特數越多,量化單比特越小,對輸入信號的分辨能力就越高。

例如:輸入模擬電壓的變化範圍為0~5 V,輸出8比特二進制數可以分辨的最小模擬電壓為5 V×2-8=20 mV;而輸出12比特二進制數可以分辨的最小模擬電壓為5 V×2-12≈1.22 mV。

智能車ADC轉換分辨率由板載ADC的具體型號决定,一般有8bit,12bit等。TC264D一般電磁循迹用8bit足够了。

3.5.4 電磁信號處理

首先要讀取電磁信號,然後濾波,歸一化處理。

讀取電磁信號調用官方的庫即可。這裏注意adc句柄和通道號要與所畫pcb的管脚定義相對應。

rt_adc_read(ADC_0, ADC0_CH0_A0);

濾波方法有很多,常用的有均值濾波,平滑濾波,卡爾曼濾波等等。這裏注意的是,不要貪多求全,適合自己的才是最好的。聽上去很厲害的濾波方法可能效果不一定那麼好,有的方法有可能是不收斂的。所以最好對自己使用的濾波方法心裏有數。我們在加上三大濾波方法之後,發現濾波效果反而變差了,所以就只用了基本的均值濾波。其實現代碼也很簡單。

sum = 0;
for(i=0; i<count; i++)
{

sum += rt_adc_read(ADC_0, ADC0_CH0_A0);
}
sum = sum/count;

歸一化處理:每一個場地的電感數據,往往不盡相同,甚至出現極大的偏差,可能是場地地下鋼筋等因素的影響,所以歸一化是十分必要的。這樣可以方便適應不同的賽道環境,使得比賽場上的電感值最接近平時調車時的電感值。歸一化思路如下:先把車放在直道上,采集各電感的最大值(使該電感貼近或垂直電磁線)。寫一個記錄最大值、最小值的數組。進行以下運算。

需要注意的是:如果有限幅操作(1-100),則記錄歸一化數值時需要把車放在整個賽道上電感值最大的地方(如環島),否則可能造成電感“飽和”的假象。如果沒有限幅操作,則放在直道上讀數即可。

for(int i=0;i<7;i++)
{

AD_G_S[i]=( (AD_data[i]-AD_min[i])*1.0/(s_AD_max[i]-AD_min[i])*100 );
if(AD_G_S[i]>100)
AD_G_S[i]=100;
else if(AD_G_S[i]<1)
AD_G_S[i]=1;
}

3.6 利用定時器實現巡線

循迹一般有電磁循迹和攝像頭循迹兩種方案。前者簡單易上手,穩定性高,不容易受到賽道環境的影響。後者獲取的信息量更大,上限更高,穩定性較低,受場地陽光等影響較大。由於今年四輪組要求攝像頭高度不超過不容易受到賽道環境的影響。後者獲取的信息量更大,上限更高,穩定性較低,受場地陽光等影響較大。由於今年四輪組要求攝像頭高度不超過不容易受到賽道環境的影響。後者獲取的信息量更大,上限更高,穩定性較低,受場地陽光等影響較大。由於今年四輪組要求攝像頭高度不超過不容易受到賽道環境的影響。後者獲取的信息量更大,上限更高,穩定性較低,受場地陽光等影響較大。由於今年四輪組要求攝像頭高度不超過

循迹算法常用的是差比和算法,差比和偏差曲線與理想偏差曲線如圖3.8中所示。

▲ 圖3.8 差比和偏差曲線與理想偏差曲線

▲ 圖3.8 差比和偏差曲線與理想偏差曲線

算法基本思路如下:

Position= (a-b)/(a+b)(a和b是左右電感的值)

計算出的數值絕對值大小錶示偏離賽道的程度,在一定範圍內車模偏離賽道越遠計算出來的值越大。下面利用這個數據就可以控制舵機,來使得車模一直沿著賽道中心線前進了。

//差比和
__S_diff= (elect_L - elect_R)*100;
__S_sum= (elect_L + elect_R);
S_position=__S_diff/__S_sum;
//丟線保護
if(__S_sum<__LOSELINE){

if (elect_L >= elect_R) {

pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER-L_DUTY);//左打死
}
else {

pwm_duty(SMOTOR_CHANNEL, SMOTOR_CENTER-R_DUTY); //右打死
}
}
else{

//計算本次誤差
curr_elect_val=SET_POSITION-position;
}

這樣一來就可以實現電磁巡線的效果。

把這一系列電感采集與處理、pid計算、pwm輸出放在一起,就可以實現初步的智能小車巡線了。下面展示這一功能的實現過程。放在定時器進程裏面運行,來保證執行周期是固定的。創建定時器和創建線程的方法類似,創建線程的方法放在3.4.1部分詳細說明。

rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);

這裏timer1錶示定時器的名稱,限制在8個字符以內。

timer1_pit_entry錶示時間到了之後需要執行的函數,可以理解為stm32裏的超時回調函數。

RT_NULL錶示不需要傳遞參數,

1錶示定時器的超時時間為1個系統tick,系統周期為1毫秒,則1錶示1毫秒。為什麼要用1個系統tick?這樣可以保證定時器足够精確。

RT_TIMER_FLAG_PERIODIC錶示定時器以周期運行,如果設置為RT_TIMER_FLAG_ONE_SHOT則只會運行一次。

這些內容可以通過RTT官方文檔和API手册來了解。

void timer_pit_init(void)
{

rt_timer_t timer;
//創建一個定時器 周期運行
timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
//啟動定時器
if(RT_NULL != timer)
{

rt_timer_start(timer);
}
}

創建好定時器以後,就可以編寫定時器超時函數了。可以理解為周期性執行的線程,而之前無限次執行的線程一般周期性不能得到保證。

每進函數一次,time++。

if(0 == (time%5)) 保證5個周期進一次。
然後依次執行,電磁信號采集、歸一化、舵機PID控制,采集編碼器數據和電機的PID控制。

void timer1_pit_entry(void *parameter)
{

static uint32 time;
time++;
if(0 == (time%5))
{

//電磁信號采集、歸一化、PID控制
elec_calculate();
//采集編碼器數據
encoder_get();
//控制電機轉動
motor_control();
}
}

這樣小車就基本具備自主巡線的功能,同時擁有舵機的方向環和電機的速度環。

3.7 調試手段

3.7.1 顯示屏與低優先級線程

顯示屏可以在車上直接看數據,如圖3.9中所示,相比串口通信調試更加直觀好方便,顯示信息更加豐富,由於采用了spi協議,速度也更快。

▲ 圖3.9 輔具調試顯示屏

▲ 圖3.9 輔具調試顯示屏

顯示屏顯示作為一個獨立的線程,首先需要初始化。初始化由以下幾部分組成,創建線程句柄,初始化外設(這個直接調廠商的庫函數),然後創建顯示線程並啟動,優先級設置為31。這裏注意的是,我們做車一般32個優先級就够了,0為最高的優先級(不同的廠家對優先級的順序處理可能不一樣,RTT和ST都是0最高)。為什麼顯示線程的優先級是最後一個呢?這是因為,相對於信號的采集、處理、計算,以及pwm輸出等操作,顯示部分只影響我們讀數,不影響小車的正常運轉,所以顯示就不那麼重要。因此,顯示的線程最低,為31。調試過程中發現,顯示屏顯示和串口傳輸是非常消耗資源的一件事,如果把這些任務放在中斷裏則無法保證中斷的實時性和周期性。統一放在main()的while(1)裏面則許多任務無法區分優先級。

這時候,RT-Thread的線程概念就完美解决了這個問題。把不同的任務放在不同的線程,並分配好相應的優先級。則低優先級任務不會影響高優先級任務的執行,高優先級任務掛起的時候,也可以很好的利用處理器的空閑資源來處理低優先級任務。

下面是線程創建的具體操作。創建好之後,就需要啟動顯示線程。由於是本文中首次接觸線程的創建,所以帶有詳細的注釋講解。

void display_init(void)
{

// 線程控制塊指針,創建線程時的標准操作
rt_thread_t tid;
//lcd初始化,相關外設的初始化
lcd_init();
// 創建動態線程,利用系統的動態內存管理,比較方便,一般都用動態線程
//創建顯示線程 優先級設置為31
tid = rt_thread_create("display", // 線程名稱
display_entry, // 線程入口函數
RT_NULL, // 線程參數,RT_NULL錶示無參,類似於void
256, // 256 個字節的棧空間,留有一定的餘量
31, // 線程優先級為31,數值越小,優先級越高,0為最高優先級。
30); // 時間片為30個系統節拍
//啟動顯示線程
if(RT_NULL != tid)
{

rt_thread_startup(tid);
}
}

在顯示線程入口函數處,編寫需要的內容即可。我們是通過撥碼開關確定顯示與否(把撥碼開關當成一個普通的io處理即可),然後選擇顯示原始電感值還是歸一化後的電感值。

void display_entry(void *parameter)
{

while(1)
{

if(gpio_get(P14_4)){

if(0){

for(int16 i=0;i<7;i++){

lcd_showuint16(5, i, signals_long[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,signals_short[i]);
}
rt_kprintf("point_num: %d\n", point_sum);
}
if(1){

for(int16 i=0;i<7;i++){

lcd_showuint16(5,i,AD_G_L[i]);
lcd_showuint16(120, i, AD_G_S[i]);
}
if(ShortSmotorFlag==0){

lcd_showstr(120,7," ");
lcd_showuint16(5,7,0);
}
else{

lcd_showstr(5,7," ");
lcd_showuint16(120,7,1);
}
virtual_Osc_Test();
rt_kprintf("point_num: %d\n", point_sum);
}
}
rt_thread_mdelay(10);
}
}

入口函數是在哪,被誰調用的?答案是,入口函數是被系統內核調用的,只需要創建好並啟動,系統就會自動調用入口函數。實際上,由於我們的線程是無限循環模式,即限循環模式,即限循環模式,即限循環模式,即
rt_thread_mdelay(10);

這樣做的意義在於:在實時操作系統中,線程中不能陷入死循環操作,必須要有讓出 CPU 使用權的動作,如循環中調用延時函數或者主動掛起。

而放在while(1)的目的,就是為了讓這個線程一直被系統循環調度運行,永不删除。與之相對應的就是順序執行或有限次循環模式,如簡單的順序語句、do while() 或 for()循環等,此類線程不會循環或不會永久循環,可謂是 “一次性” 線程,一定會被執行完畢。在執行完畢後,線程將被系統自動删除。

3.7.2 蜂鳴器和郵箱的完美配合

在做一些賽道元素判斷的時候,我們需要知道到底有沒有觸發條件。如果調試結果不如預期,就需要知道到底是車模運動的問題,還是判斷的問題。而車在跑的過程中,看顯示屏是不現實的,串口傳輸也很不方便。所以在某些元素比特置滿足條件讓蜂鳴器響,是方便高效的調試辦法。

和顯示屏一樣,蜂鳴器也需要創建線程,啟動線程,編寫入口函數。這裏優先級設置為20,比顯示屏更高,理由是蜂鳴器需要更及時的響應。

void buzzer_init(void)
{

rt_thread_t tid;
//初始化蜂鳴器所使用的GPIO
gpio_init(BUZZER_PIN, GPO, 0, PUSHPULL);
//創建郵箱
buzzer_mailbox = rt_mb_create("buzzer", 5, RT_IPC_FLAG_FIFO);
//創建蜂鳴器的線程
tid = rt_thread_create("buzzer", buzzer_entry, RT_NULL, 256, 20, 2);
//啟動線程
if(RT_NULL != tid)
{

rt_thread_startup(tid);
}
}

如何讓蜂鳴器響?這裏使用的是有源蜂鳴器,所以只需要給它一個高電平。但是如果給一個高電平就不管了,那麼蜂鳴器就會一直響,制造出讓人無法忍受的噪音。所以需要先拉高,持續一段時間再拉低。同樣的,蜂鳴器也需要無限循環模式。

這裏有三種操作蜂鳴器方式的比較:

  1. 手動操作gpio型。滿足條件拉高,否則拉低。這是最樸素最原始的方案。優點簡單易行。缺點有二,一是如果此條件滿足拉高了,下一個判斷條件不滿足又拉低了,由於程序執行速度很快以至於我們是聽不到蜂鳴器響的。如果只滿足條件拉高而不拉低,則全實驗室就會充斥著蜂鳴器尖利的嘯叫,同行們可能會把你的車子扔出去,當然也可能包括你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。

  2. 傳統裸機編程delay()型。自己寫一個程序,拉高,延時一段時間再拉低。這樣做的優點就是克服了第1種方式的缺點。缺點則是delay()的過程,相當於cpu什麼也不做,十分浪費資源。且如果delay()時間過長,相當於這段時間內信號也不采集,電機也不驅動,僅僅是為了等蜂鳴器響够一定時間。這是非常可怕的。

  3. RT-Thread系統郵箱+掛起型。既保留了第2種方式的所有優點,也克服了上述兩種方式所有的缺點。不需要全局變量,每次響多長時間由郵件內容决定,在延時的過程中,其他的程序可以繼續執行,當延時結束後,再回來拉低gpio。可以說,RT-Thread系統的優勢在這一個小點上已得到充分的展現。

其實現流程是:蜂鳴器入口函數裏接收郵箱數據,如果沒有數據則持續等待並釋放CPU控制權。

void buzzer_entry(void *parameter)
{

uint32 mb_data;
while(1)
{

//接收郵箱數據,如果沒有數據則持續等待並釋放CPU控制權
rt_mb_recv(buzzer_mailbox, &mb_data, RT_WAITING_FOREVER);
gpio_set(BUZZER_PIN, 1); //打開蜂鳴器
rt_thread_mdelay(mb_data); //延時
gpio_set(BUZZER_PIN, 0); //關閉蜂鳴器
}
}

接收模式是由這個宏定義確認的。

#define RT_WAITING_FOREVER -1 /**< Block forever until get resource. */ 

在需要響的比特置給蜂鳴器發一個郵件,讓蜂鳴器響一定時間。由於程序是放在定時器裏周期性執行的,所以會實現蜂鳴器以不同的頻率鳴響。比如斑馬線處是“嘟嘟”,在岔路處是“滴滴”。

if(ForkRoadstate){

forkroadcount++;
// pwm_duty(MOTOR1_CHANNEL, 3000);//减速
//發個消息讓蜂鳴器響
rt_mb_send(buzzer_mailbox, BBFORK);
}

3.7.3 攝像頭與信號量的保護

同樣的,攝像頭部分也有類似的操作。這裏攝像頭就不再單獨創建一個線程,而是直接放在main()函數的while(1)裏。

為什麼要等待攝像頭采集完畢才開始處理攝像頭圖像呢?因為雙方使用的數組是同一個,屬於公共資源。如果不做這個處理,就有可能出現圖像處理的過程中攝像頭改寫了相應的數組,使得這一張是由前一張的一半和下一張的一半拼接而成,也有可能造成流水線等待的遲滯。所以當攝像頭采集完畢之後,會釋放信號量,rt_sem_take(camera_sem, RT_WAITING_FOREVER);語句得到信號量之後繼續執行下面的語句,否則持續等待並釋放CPU控制權。
信號量的英文是semaphore,可以理解成信號旗,只有得到了信號,才做相應的反應。這裏的應用可以說非常直觀了。

while(1) {

//等待攝像頭采集完畢
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//開始處理攝像頭圖像
DealGarage();
//處理完成需要將標志比特置0
mt9v03x_finish_flag = 0;
//翻轉LED,閃燈錶示程序正在運行沒有死機
time++;
if(time==500){

time=0;
gpio_toggle(P20_8);
gpio_toggle(P20_9);
}
}

這樣以後,圖像處理就完成了。

這裏有個小Tips,就是可以在while(1)裏翻轉LED的GPIO引脚,閃燈則錶示程序正在運行沒有死機。

▲ 圖3.10 攝像頭調試

▲ 圖3.10 攝像頭調試

3.7.4 通過按鍵釋放不同的信號量

利用定時器創建一個周期性按鍵掃描的線程,並且創建按鍵的信號量,當按鍵按下就釋放該信號量,在需要使用按鍵的地方獲取信號量即可。這裏簡單演示一下。初始化的時候先定義一個定時器的句柄 timer1 ,初始化對應的GPIO,創建按鍵的信號量,定時器設置為入口函數是button_entry,不傳參,周期為20個系統節拍(即20ms),模式設置為RT_TIMER_FLAG_PERIODIC即周期性執行。為什麼是20ms呢,因為要考慮按鍵按下的這樣一個時間,如果太短,可能兩次都是按下的狀態,如果太長,則可能兩次都是沒按下的狀態。

void button_init(void)
{

rt_timer_t timer1;
gpio_init(KEY_1, GPI, GPIO_HIGH, PULLUP); // 初始化為GPIO浮空輸入 默認上拉高電平
gpio_init(KEY_2, GPI, GPIO_HIGH, PULLUP);
key1_sem = rt_sem_create("key1", 0, RT_IPC_FLAG_FIFO); //創建按鍵的信號量
key2_sem = rt_sem_create("key2", 0, RT_IPC_FLAG_FIFO);
timer1 = rt_timer_create("button", button_entry, RT_NULL, 20, RT_TIMER_FLAG_PERIODIC);
if(RT_NULL != timer1)
{

rt_timer_start(timer1);
}
}

那麼在入口函數這裏編寫自己需要的功能。一般是檢測到按鍵按下之後並放開,釋放一次信號量,然後再需要的比特置接收信號量即可。

void button_entry(void *parameter)
{

//保存按鍵狀態
key1_last_status = key1_status;
key2_last_status = key2_status;
//讀取當前按鍵狀態
key1_status = gpio_get(KEY_1);
key2_status = gpio_get(KEY_2);
//檢測到按鍵按下之後並放開 釋放一次信號量
if(key1_status && !key1_last_status)
{

rt_sem_release(key1_sem);
}
}

3.7.5 虛擬示波器線程與時間片輪轉

我們利用山外多功能調試助手的協議做了一個虛擬示波器,如圖3.11中所示,可以將采集得到的電感值、計算出的比特置和打角、車模運行速度等變量實時打印出來,十分直觀。

▲ 圖3.11 上比特機顯示的虛擬示波器圖像

▲ 圖3.11 上比特機顯示的虛擬示波器圖像

▲ 圖3.12 利用虛擬示波器和顯示屏調試

▲ 圖3.12 利用虛擬示波器和顯示屏調試

同樣的,這裏將示波器也作為一個獨立的線程。由於和顯示屏的地比特一樣,所以優先級、大小等設置和顯示屏線程是一樣的。

void virtual_Osc_init(void)
{

rt_thread_t tid;
//創建示波器線程 優先級設置為31
tid = rt_thread_create("virtual_Osc", virtual_Osc_entry, RT_NULL, 256, 31, 25);
//啟動示波器線程
if(RT_NULL != tid)
{

rt_thread_startup(tid);
}
}

同樣的,編寫線程入口函數,同樣采用無限循環模式。

void virtual_Osc_entry(void *parameter)
{

while(1)
{

virtual_Osc_Test();
rt_thread_mdelay(10);
}
}

不同優先級之間的線程可以有所區分,按優先級大小進行調度,那麼優先級相同的線程如何調度呢?這裏就要引出RT-Thread系統的另一大特色,時間片輪轉調度方式,如圖3.11中所示。

▲ 圖3.13 時間片輪轉調度方式

▲ 圖3.13 時間片輪轉調度方式

(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/thread/thread)

簡單來說,就是同優先級的任務依次循環排隊進行,時間片的作用就是保證一定的實時性。比如當線程B的執行時間已經達到了給定的時間片長度,而該任務還沒有執行結束,該任務就會被强制掛起,執行下一個任務。等排隊的任務都執行結束或時間片結束之後再接著執行線程B。

我們回顧一下顯示屏線程的創建。最後一個參數為時間片參數,我們設為30,錶示30個系統節拍(即30ms)。

//創建顯示線程 優先級設置為31
tid = rt_thread_create("display", display_entry, RT_NULL, 256, 31, 30);

而虛擬示波器線程的時間片則為25個系統節拍,即25ms.

//創建示波器線程 優先級設置為31
tid = rt_thread_create("virtual_Osc", virtual_Osc_entry, RT_NULL, 256, 31, 25);

如果在時間片內執行結束,則語句末的rt_thread_mdelay(10);會釋放cpu的使用權,如果超時則會被强制掛起。
這樣就很好的利用了時間片輪轉調度特性完成了顯示屏顯示和無線串口虛擬示波器任務的“並行處理”。

3.7.6 使用FinSH實時監測單片機運行情况

FinSH 是 RT-Thread 的命令行組件(shell),提供一套供用戶在命令行調用的操作接口,主要用於調試或查看系統信息。它可以使用串口 / 以太網 / USB 等與 PC 機進行通信。在智能車運行中,我們常常需要實時監測單片機的運行情况,包括變量的值、線程運行狀態、內存使用情况。通常我們會編寫相關函數然後通過無線串口傳輸在上比特機監測單片機的運行情况。這些調試所用的函數往往不便於更新和維護,也會使得程序變得複雜、冗餘。所以我們需要一套成熟的命令行系統,而RT-Thread系統的FinSH控制臺正好為我們提供了這個功能。

在實際小車運行過程中,我們使用ps命令列出系統中所有線程信息,包括線程優先級、狀態、棧的最大使用量等,以此監測智能車的線程運行情况。

▲ 圖3.14 使用PS命令檢測線程信息

▲ 圖3.14 使用PS命令檢測線程信息

另外,FinSH還提供了自定義命令的功能,使用者根據需要自定義命令。比如,我們編寫了如,我們編寫了如,我們編寫了如,我們編寫了

MSH_CMD_EXPORT(stop_car,Car Is Stop);

3.8本章小結

本章將裸機大while()+中斷方式與RT-Thread系統做了比較,分析了兩者的優劣勢,並得出了RT-Thread系統優勢巨大的結論。接著將基礎四輪小車面臨的任務抽象為“一核心”PID控制,“兩關鍵”(環境變量輸入和控制量輸出),“多輔助”(led、顯示屏、蜂鳴器等),詳細說明了基於RT-Thread操作系統的基礎四輪車的軟件開發流程,靈活利用RT-Thread操作系統的線程管理、時鐘管理、郵箱、信號量、時間片輪轉調度等特性,完成了小車的巡線,以及高效的調試和維護。在3.7.2小節利用蜂鳴器與RT-Thread系統的完美配合初步展現了RT-thread系統的魅力,其可重用性及豐富的軟件包支持也將成為物聯網時代的弄潮兒。

利用嵌入式實時操作系統開發的重點在於摒弃傳統的大while(1)+中斷模式前後臺思維,擁抱多線程並發、模塊化開發的思維。將任務分解成一個一個的線程,合理配置優先級和時間片,從而保證任務的順利完成。
還要有通信思維的轉變。從傳統的全局變量標志比特思維轉變為利用郵箱、信號量、互斥量等全局通信方式通信。這樣做最大的好處除了减少全局變量的使用及其帶來的一系列問題以外,不僅更加方便,還可以實現在條件詢問時條件不滿足可以釋放cpu使用權,提高cpu利用率。同樣的,所有的delay()都不會使得cpu空轉,而是會先把cpu釋放出來,去處理其他的任務。這樣大大提高了cpu資源的使用率。

 

§04 習與遷移


4.1 學習過程

學習過程主要分為三部分,先分析小車裸機中斷+大while()模式的架構,然後了解RT-Thread基本的特性,學習RT-Thread的內核部分。最後是遷移部分。

學習路徑:通過RTT官方文檔和逐飛的開源庫進行學習,加以一些參考書作為輔助。

4.1.1學習線程管理

//------------------------------------------------------------

// @brief 線程1入口函數
// @param parameter 參數
// @return void
// Sample usage:
//------------------------------------------------------------
void thread1_entry (void *parameter)
{

while(1)
{

// 調度器上鎖,上鎖後將不再切換到其他線程,僅響應中斷
rt_enter_critical();
// 以下進入臨界區
count += 10; // 計數值+10
// 調度器解鎖
rt_exit_critical();
rt_kprintf("thread = %d , count = %d\n", 10, count);
rt_thread_mdelay(1000);
}
}
//------------------------------------------------------------
// @brief 線程創建以及啟動
// @param void 空
// @return void
// Sample usage:
//------------------------------------------------------------
int critical_section_example(void)
{

// 線程控制塊指針
rt_thread_t tid;
// 創建動態線程
tid = rt_thread_create("thread_10", // 線程名稱
thread1_entry, // 線程入口函數
RT_NULL, // 線程參數
256, // 棧空間
5, // 線程優先級為5,數值越小,優先級越高,0為最高優先級。
// 可以通過修改rt_config.h中的RT_THREAD_PRIORITY_MAX宏定義(默認值為8)來修改最大支持的優先級
10); // 時間片
if(tid != RT_NULL) // 線程創建成功
{

// 運行該線程
rt_thread_startup(tid);
}
return 0;
}
// 使用INIT_APP_EXPORT宏自動初始化,也可以通過在其他線程內調用critical_section_example函數進行初始化
INIT_APP_EXPORT(critical_section_example); // 應用初始化

學習完畢,總結:創建一個線程需要做三方面的事,線程入口函數,包括調度器上鎖、進入臨界區、調度器解鎖三個步驟,其中重要代碼需要放在臨界區;線程創建及啟動函數,包括線程控制指針、創建動態線程(入口函數、優先級等),啟動線程rt_thread_startup(tid)。然後編譯運行,符合實驗預期。

4.1.2學習線程間通信——郵箱

郵箱服務是實時操作系統中一種典型的線程間通信方法。舉一個簡單的例子,有兩個線程,線程 1 檢測按鍵狀態並發送,線程 2 讀取按鍵狀態並根據按鍵的狀態相應地改變 LED 的亮滅。這裏就可以使用郵箱的方式進行通信,線程 1 將按鍵的狀態作為郵件發送到郵箱,線程 2 在郵箱中讀取郵件獲得按鍵狀態並對 LED 執行亮滅操作。

這裏的線程 1 也可以擴展為多個線程。例如,共有三個線程,線程 1 檢測並發送按鍵狀態,線程 2 檢測並發送 ADC 采樣信息,線程 3 則根據接收的信息類型不同,執行不同的操作。

線程1負責接收郵件,線程2負責發送郵件。

static char mb_str1[] = "i am a mail!"; // 創建一個字符串
static rt_mailbox_t mb; // 郵箱控制塊指針
void thread1_entry (void *parameter);
void thread2_entry (void *parameter);
int mailbox_example (void);
//------------------------------------------------------------
// @brief 線程入口
// @param parameter 參數
// @return void
// Sample usage:
//------------------------------------------------------------
void thread1_entry (void *parameter)
{

char *str;
rt_kprintf("thread1:try to recv a mail.\n");
if(rt_mb_recv(mb, // 郵箱控制塊
(rt_ubase_t *)&str, // 接收郵箱的字符串 接收 32bit 大小的郵件
RT_WAITING_FOREVER) // 一直等待
== RT_EOK)
{

rt_kprintf("thread1:get a mail from mailbox.\nthe content :%s\n", str); // 輸出接收信息
}
rt_mb_delete(mb); // 删除郵箱
}
//------------------------------------------------------------
// @brief 線程入口
// @param parameter 參數
// @return void
// Sample usage:
//------------------------------------------------------------
void thread2_entry (void *parameter)
{

rt_kprintf("thread2:try to send a mail.\n");
// 這裏是使用的方式是將字符串的地址取值 得到 32bit 的地址值發送
rt_mb_send(mb, (rt_uint32_t)&mb_str1); // 發送郵件
}
//------------------------------------------------------------
// @brief 郵箱創建以及啟動
// @param void
// @return void
// Sample usage:
//------------------------------------------------------------
int mailbox_example (void)
{

rt_thread_t t1,t2;
// 創建郵箱
mb = rt_mb_create("mb",
4, // 設置 緩沖區為 4 封郵件
RT_IPC_FLAG_FIFO // 先進先出
);
t1 = rt_thread_create(
"thread1", // 線程名稱
thread1_entry, // 線程入口函數
RT_NULL, // 線程參數
256, // 棧空間大小
4, // 設置線程優先級
5); // 時間片
if(t1 != RT_NULL) // 線程創建成功
{

rt_thread_startup(t1); // 運行該線程
}
t2 = rt_thread_create(
"thread2", // 線程名稱
thread2_entry, // 線程入口函數
RT_NULL, // 線程參數
256, // 棧空間大小
3, // 設置線程優先級
5); // 時間片
if(t2 != RT_NULL) // 線程創建成功
{

rt_thread_startup(t2); // 運行該線程
}
return 0;
}

4.1.3學習線程間同步——信號量

例如一項工作中的兩個線程:一個線程從傳感器中接收數據並且將數據寫到共享內存中,同時另一個線程周期性的從共享內存中讀取數據並發送去顯示。

信號量工作機制:信號量是一種輕型的用於解决線程間同步問題的內核對象,線程可以獲取或釋放它,從而達到同步或互斥的目的。信號量工作示意圖如下圖4.1中所示,每個信號量對象都有一個信號量值和一個線程等待隊列,信號量的值對應了信號量對象的實例數目、資源數目。假如信號量值為 5,則錶示共有 5 個信號量實例(資源)可以被使用,當信號量實例數目為零時,再申請該信號量的線程就會被掛起在該信號量的等待隊列上,等待可用的信號量實例(資源)。

▲ 圖4.1 線程間銅箔-信號量機制

▲ 圖4.1 線程間銅箔-信號量機制

(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1?id=%e4%bf%a1%e5%8f%b7%e9%87%8f)

當選擇 RT_IPC_FLAG_FIFO(先進先出)方式時,那麼等待線程隊列將按照先進先出的方式排隊,先進入的線程將先獲得等待的信號量。

camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);

信號量也能够方便地應用於中斷與線程間的同步,例如一個中斷觸發,中斷服務例程需要通知線程進行相應的數據處理。這個時候可以設置信號量的初始值是 0,線程在試圖持有這個信號量時,由於信號量的初始值是 0,線程直接在這個信號量上掛起直到信號量被釋放。當中斷觸發時,先進行與硬件相關的動作,例如從硬件的 I/O 口中讀取相應的數據,並確認中斷以清除中斷源,而後釋放一個信號量來喚醒相應的線程以做後續的數據處理。例如 FinSH 線程的處理方式,如下圖5.2中所示:

▲ 圖5.2 FinSH 的中斷、線程間同步示意圖

▲ 圖5.2 FinSH 的中斷、線程間同步示意圖

(引自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1?id=%e4%bf%a1%e5%8f%b7%e9%87%8f)

信號量的值初始為 0,當 FinSH 線程試圖取得信號量時,因為信號量值是 0,所以它會被掛起。當 console 設備有數據輸入時,產生中斷,從而進入中斷服務例程。在中斷服務例程中,它會讀取 console 設備的數據,並把讀得的數據放入 UART buffer 中進行緩沖,而後釋放信號量,釋放信號量的操作將喚醒 shell 線程。在中斷服務例程運行完畢後,如果系統中沒有比 shell 線程優先級更高的就緒線程存在時,shell 線程將持有信號量並運行,從 UART buffer 緩沖區中獲取輸入的數據。

4.2 遷移准備

4.2.1 小車代碼部分分析

整體遷移方案是將原來的單機大while()形式遷移到RTT操作系統下面。

具體的大while()+中斷形式代碼分析見3.2.

4.2.2 遷移思路

將大while()裏面的顯示屏、串口分解為兩個不同的線程,優先級相同,利用時間片輪轉方式進行調度。5ms一次的pit中斷用RT-Thread系統的軟時鐘來替代。用信號量解决攝像頭獲取和處理圖像的關系,用郵箱更好地處理蜂鳴器使用不靈活不方便的問題。

由於小車速度很快,且感知、决策、控制存在必然的順序,所以必須保證三者的周期性喚醒及執行的先後順序。所以用RT-Thread 操作系統提供的軟定時器,timer1_pit_entry線程周期運行,周期為1個系統節拍。在定時器入口函數裏面完成賽道環境的采集和處理,差比和計算,以及PWM波輸出。

顯示屏和串口作為display_entry線程,優先級設置為31。並且通過撥碼開關决定是否顯示。蜂鳴器作為buzzer_entry線程,優先級設置為20。並且通過郵箱决定是否響以及響的時間,郵箱大小為5個字節,采用 FIFO 方式進行線程等待。這裏顯示屏和串口優先級最低,而蜂鳴器優先級更高,比較合理。

遷移思路如圖4.3中所示,將前述智能車任務分解為多線程並發、系統自動調度機制下的RT-Thread系統模式。

▲ 圖4.3 遷移思路

▲ 圖4.3 遷移思路

4.3 遷移過程

4.3.1用信號量處理攝像頭獲取和處理圖像的關系

現象:只有當圖像處理結束以後,攝像頭才能獲取下一張。當采集結束以後,才開始處理。類似於adc轉換的采樣-保持-量化-編碼階段的關系。之前是使用全局變量的方式通信的,中斷裏放采集+大while()放處理模式。為什麼要用信號量處理攝像頭獲取和處理圖像的關系?

原因有二:一是因為原來用全局變量的方式去進行通信是不太好的習慣,,因為全局變量可能會增加系統的不確定性。所以我們對於全局變量的使用都是持“少用,慎用,不用”的態度。但是中斷+大while()編程是離不開全局變量的。這也讓我一直十分疑惑,不知道怎麼解决這個矛盾的問題。而系統級別的通信方式就解决了這個問題。按照RT-Thread的特性,用信號量處理這個問題就十分方便。二是因為如果詢問方式采用了while()等待的方式,那麼如果條件不滿足,cpu就會一直在while()裏空轉,直到條件滿足。這樣可能會造成cpu的無謂等待,降低cpu的利用率。

具體操作過程:

先創建攝像頭的信號量,

camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);

選擇 RT_IPC_FLAG_FIFO(先進先出)方式,使得等待線程隊列將按照先進
先出的方式排隊,先進入的線程將先獲得等待的信號量。這樣可以保證圖像處理的完整性和一貫性。
在while(1)裏面輪詢,采用無限等待的方式獲取信號量,在獲得信號量以後說明攝像頭采集完畢,這時可以開始處理攝像頭圖像,

 while(1)
{

//等待攝像頭采集完畢
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//開始處理攝像頭圖像
DealGarage();
//處理完成需要將標志比特置0
mt9v03x_finish_flag = 0;
}

在MT9V03X攝像頭場中斷裏,判斷圖像數組是否使用完畢,如果未使用完畢則不開始采集,避免出現訪問沖突。如果圖像數組使用完畢,則開啟下一輪圖像傳輸。圖像傳輸采用DMA模式。

//---------------------------------------------------------------
// @brief MT9V03X攝像頭場中斷
// @param NULL
// @return void
// @since v1.0
// Sample usage: 此函數在isr.c中被eru(GPIO中斷)中斷調用
//---------------------------------------------------------------
void mt9v03x_vsync(void)
{

CLEAR_GPIO_FLAG(MT9V03X_VSYNC_PIN);
mt9v03x_dma_int_num = 0;
if(!mt9v03x_finish_flag)//查看圖像數組是否使用完畢,如果未使用完畢則不開始采集,避免出現訪問沖突
{

if(1 == link_list_num)
{

//沒有采用鏈接傳輸模式 重新設置目的地址
DMA_SET_DESTINATION(MT9V03X_DMA_CH, camera_buffer_addr);
}
dma_start(MT9V03X_DMA_CH);
}
}

在MT9V03X攝像頭DMA完成中斷裏面,如果采集完成,就釋放攝像頭信號量。這時候while(1)裏面就開始處理圖像了。攝像頭部分就結束了。

//------------------------------------------------------------------
// @brief MT9V03X攝像頭DMA完成中斷
// @param NULL
// @return void
// @since v1.0
// Sample usage: 此函數在isr.c中被dma中斷調用
//------------------------------------------------------------------
void mt9v03x_dma(void)
{

extern rt_sem_t camera_sem;
CLEAR_DMA_FLAG(MT9V03X_DMA_CH);
mt9v03x_dma_int_num++;
if(mt9v03x_dma_int_num >= link_list_num)
{

//采集完成
mt9v03x_dma_int_num = 0;
mt9v03x_finish_flag = 1;//一副圖像從采集開始到采集結束耗時3.8MS左右(50FPS、188*120分辨率)
rt_sem_release(camera_sem); //釋放攝像頭信號量
dma_stop(MT9V03X_DMA_CH);
}
}

帶來的實際好處:使用RT-Thread的信號量特性,方便地解决中斷與線程間同步。因為獲取和傳輸圖像是在中斷進行的,處理圖像是在main()線程裏完成的。系統級別的通信方式大大鋪開,則全局變量的使用將會大大减少,這樣减少了許多安全隱患,也有助於我們養成良好的編程習慣。另一方面,利用系統特性,如果條件不滿足會將線程掛起,釋放cpu,减少了cpu詢問等待的空轉時間,提高了cpu的利用率。

4.3.2用郵箱解决蜂鳴器使用不靈活不方便的問題

現象:原來的蜂鳴器兩種使用方式,手動拉高拉低GPIO型或者寫個函數+delay()型。這兩種方案各有缺點。手動拉蜂鳴器GPIO型的缺點有二,一是如果此條件滿足拉高了,下一個判斷條件不滿足又拉低了,由於程序執行速度很快以至於我們是聽不到蜂鳴器響的。如果只滿足條件拉高而不拉低,則全實驗室就會充斥著蜂鳴器尖利的嘯叫,同行們可能會把你的車子扔出去,當然也可能包括你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於你。二是不同的元素處無法通過蜂鳴器的頻率區分出來,響聲是一樣的,如果誤判了,無法確定是哪一部分誤判。手動寫個函數最大的缺點在於寫個函數+delay()型:

void BEEP()
{

gpio_set(P02_6,1);
systick_delay_ms(STM0, 1);
gpio_set(P02_6,0);
}

為什麼要用郵箱+蜂鳴器線程的方式?因為這種方式方式——RT-Thread系統郵箱+掛起型,既保留了第2種方式的所有優點,也克服了上述兩種方式所有的缺點。不需要全局變量,每次響多長時間由郵件內容决定,在延時的過程中,其他的程序可以繼續執行,當延時結束後,再回來拉低gpio。蜂鳴器入口函數裏接收郵箱數據,如果沒有數據則持續等待並釋放CPU控制權,不會造成cpu空轉。

具體操作:

首先是初始化buzzer(蜂鳴器),要做的就是初始化蜂鳴器所使用的GPIO、創建郵箱、創建蜂鳴器的線程、啟動線程四步。郵箱大小為5個字節,其實實際過程中只需要傳一個字節的數據即可,采用 FIFO 方式進行線程等待。具體的使用方式在3.4.2小節已有詳細介紹。這樣一來方便調試,也方便在比賽場上隨時知道小車所處的狀態。
帶來的實際好處:上面已經多次提到,這樣一方面减少了全局變量的使用,一方面减少了cpu的空轉,增加了cpu的利用率,同時使得蜂鳴器的使用更加的靈活方便。

4.3.3用軟時鐘代替原來的pit中斷

為什麼要用軟定時器?由於小車感知、决策、控制存在必然的順序,且要保證執行周期是固定的,所以要放在定時器線程裏面運行。所以用RT-Thread 操作系統提供的軟定時器,timer1_pit_entry線程周期運行,周期為1個系統節拍。在定時器入口函數裏面完成賽道環境的采集和處理,差比和計算,以及PWM波輸出。

具體操作:

創建一個定時器周期運行,這裏周期是一個系統節拍(1ms),可以保證精度。

timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);

然後在時鐘線程裏面,加入感知、决策、控制的代碼。進一次線程time++,然後用(0 == (time%5))保證5個系統周期。

詳細的操作過程在3.6部分已有詳細說明。按鍵和虛擬示波器線程前文已詳述,此處不再贅述。

帶來的好處:軟定時器起到了代替原來PIT中斷的作用,更多的是RT-Thread系統整體使用帶來的一系列好處。軟定時器帶來的好處最明顯的是可以有許多個定時器線程同時工作,不受硬件平臺的限制,也提高了代碼可移植性。

4.3.4利用FinSH提高調試效率

現象:在近一年的調車過程中,有一個問題一直困擾著我們,即如何在智能車運行過程中,監測某個變量的值有沒有改變,何時改變,改變了多少?如果使用開發平臺的調試模式,則只能用有線方式,用手推小車,無法模擬小車真實運行狀態。且調試十分不便。如果是直接在線程或中斷裏面rt_kprintf,則由於發送頻率太高容易造成系統死機。如果是獨立線程,則變量的聲明也十分不便。小車制作過程中,有許多參數需要不斷地調整,除了PD參數,還有期望速度、元素響應時長、舵機打角等,以期達到一個比較好的效果。而調參一般都是通過重新燒錄代碼來進行的,受到編譯環境、電腦性能影響可能編譯燒錄代碼要至少1分鐘,而沒有配置好每次都重新編譯的話就是至少三分鐘。所以如何更方便的調參,而不用每次都重新燒錄是一直在思考的問題。

為什麼要使用FinSH ?有了 shell,就像在我們和小車之間架起了一座溝通的橋梁,開發者能很方便的獲取系統的運行情况,並通過命令控制系統的運行。特別是在調車過程中,利用shell,除了能更快的定比特到問題之外,也能利用 shell 調用測試函數,改變測試函數的參數,减少代碼的燒錄次數,提高小車的調試效率。FinSH 是 RT-Thread 的命令行組件(shell),提供一套供用戶在命令行調用的操作接口,主要用於調試或查看系統信息。

▲ 圖4.14 FinSH中斷示意圖

▲ 圖4.14 FinSH中斷示意圖

(引用自https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/finsh/finsh)

具體操作:

相比於之前的快速移植,我們在這裏遇到了困難,經過很長時間才解决。原來是因為我們主要用的是無線串口uart2,但是我們把相關的函數放在了串口1的中斷裏面,所以發送命令過去無響應,後面才發現這個問題。

  1. 包括相關的庫文件。和前面的移植操作一樣,需要把相關文件都放在工程文件夾裏面,由於我們用的是完整版系統,所以相關的文件都已經被包括了。

▲ 圖4.5 文件數

▲ 圖4.5 文件數

  1. 下面實現控制臺的輸出rt_hw_console_output()。首先需要串口初始化,一並放在一並放在一並放在一並放在
void rt_hw_board_init()
{

get_clk();//獲取時鐘頻率 務必保留
rt_hw_systick_init();
/* USART driver initialization is open by default */
#ifdef RT_USING_SERIAL 
rt_hw_usart_init();
#endif 
/* Set the shell console output device */
#ifdef RT_USING_CONSOLE 
// rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif 
#ifdef RT_USING_HEAP 
//rt_system_heap_init(buf, (void*)(buf + sizeof(buf)));
rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif 
/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INIT 
rt_components_board_init();
#endif 
IfxSrc_init(&SRC_GPSR_GPSR0_SR0, IfxSrc_Tos_cpu0, SERVICE_REQUEST_PRIO);
IfxSrc_enable(&SRC_GPSR_GPSR0_SR0);
uart_mb = rt_mb_create("uart_mb", 10, RT_IPC_FLAG_FIFO);
}

然後實現控制臺的輸出,需要在這裏將串口輸出函數放在這裏(調用逐飛包裝的英飛淩底層庫)。

void rt_hw_console_output(const char *str)
{

uart_putstr(DEBUG_UART, str);
}

這裏我們在main()開頭寫一句測試函數,看控制臺輸出是否成功。我們可以看出,控制臺成功輸出了看出,控制臺成功輸出了看出,控制臺成功輸出了看出,控制臺成功輸出了看出,控制臺成功輸出了看出,控制臺成功輸出了

rt_kprintf("test\n");

▲ 圖4.6 控制臺實現輸出

▲ 圖4.6 控制臺實現輸出

  1. 使能FinSH,在rtconfig.h裏面,定義RT_USING_FINSH
* Command shell */
#define RT_USING_FINSH 
#define FINSH_THREAD_NAME "tshell" 
#define FINSH_USING_HISTORY 
#define FINSH_HISTORY_LINES 5 
#define FINSH_USING_SYMTAB 
#define FINSH_USING_DESCRIPTION 
#define FINSH_THREAD_PRIORITY 20 
#define FINSH_THREAD_STACK_SIZE 4096 
#define FINSH_CMD_SIZE 80 
#define FINSH_USING_MSH 
#define FINSH_USING_MSH_DEFAULT 
#define FINSH_ARG_MAX 10 
  1. 實現 rt_hw_console_getchar().

既可以打印也能輸入命令進行調試,控制臺已經實現了打印功能,現在還需要在 board.c 中對接控制臺輸入函數,實現字符輸入。接收字符有兩種方式,一種是查詢方式,一種是中斷方式。這裏我們用中斷方式。

在英飛淩的isr.c中斷文件頁面,在串口中斷函數裏面我們加入串口處理程序。注意,這裏我們用的是無線串口模塊,所以應該把函數放在uart2的中斷服務函數裏面。另外這裏是接收部分,所以是在IFX_INTERRUPT(uart2_rx_isr, 0, UART2_RX_INT_PRIO)裏。由於這裏有個無線串口回調函數,仿照HAL庫的編程邏輯,我把處理操作放在了回調函數裏面。不過把處理操作放在這裏更加直觀。

//串口2默認連接到無線轉串口模塊
IFX_INTERRUPT(uart2_tx_isr, 0, UART2_TX_INT_PRIO)
{

enableInterrupts();//開啟中斷嵌套
IfxAsclin_Asc_isrTransmit(&uart2_handle);
}
IFX_INTERRUPT(uart2_rx_isr, 0, UART2_RX_INT_PRIO)
{

enableInterrupts();//開啟中斷嵌套
IfxAsclin_Asc_isrReceive(&uart2_handle);
wireless_uart_callback();
}

在wireless_uart_callback(void)函數裏,先調用uart_getchar()函數接收數據,然後將數據放在郵件裏發出去。這裏沒有使用全局變量的方式,也是為了避免while()查詢條件的一直等待,從而可以不滿足條件自動掛起線程,釋放cpu,提高cpu的利用率。

void wireless_uart_callback(void)
{

//while(uart_query(WIRELESS_UART, &wireless_rx_buffer));
//讀取收到的所有數據
extern rt_mailbox_t uart_mb;
uint8 dat;
enableInterrupts();//開啟中斷嵌套
IfxAsclin_Asc_isrReceive(&uart2_handle);
uart_getchar(DEBUG_UART, &dat);
rt_mb_send(uart_mb, dat); // 發送郵件
}

在rt_hw_console_getchar(void)函數裏接收郵件,使用RT_WAITING_FOREVER的等待方式。如果接收到,則會返回dat.

char rt_hw_console_getchar(void)
{

uint32 dat;
//等待郵件
rt_mb_recv(uart_mb, &dat, RT_WAITING_FOREVER);
//uart_getchar(DEBUG_UART, &dat);
return (char)dat;
}

而在shell.c文件裏面,finsh_getchar(void)函數會調用rt_hw_console_getchar(void)函數,從而完成通訊。

static int finsh_getchar(void)
{

#ifdef RT_USING_DEVICE 
#ifdef RT_USING_POSIX 
return getchar();
#else 
char ch = 0;
RT_ASSERT(shell != RT_NULL);
while (rt_device_read(shell->device, -1, &ch, 1) != 1)
rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);
return (int)ch;
#endif 
#else 
extern char rt_hw_console_getchar(void);
return rt_hw_console_getchar();
#endif 
}

其實FinSH是作為一個線程去查詢接收的,這個線程在FinSH初始化的時候會被創建。
移植效果如下圖所示。注意,發送的命令後面需要加一個回車。

▲ 圖4.7 控制臺實現輸入輸出

▲ 圖4.7 控制臺實現輸入輸出

通過help命令可以看到可以調用的命令。這裏顯示了內置的命令和自定義命令。如果需要自定義命令,在自己寫的函數下面加一句MSH_CMD_EXPORT(hello , say hello to RT-Thread);即可。第一個參數是命令的名稱,第二個參數是命令的描述。(這裏只用了無參命令)

void hello(void)
{

rt_kprintf("hello RT-Thread!\n");
}
MSH_CMD_EXPORT(hello , say hello to RT-Thread);

至此,FinSH組件移植成功。相關命令的使用在3.7.6節做了說明。
帶來的實際好處:FinSH組件可以讓我們更方便的監測智能車的運行情况,對內存的使用、線程的調度可以有宏觀的把握,也可以自定義更多的命令來提高調試效率。這樣站在巨人的肩膀上,减少了自己從頭開發一套串口調試命令的工作量,也增加了工作的可靠性。

4.4 本章小結

本章介紹了RT-Thread系統的學習和遷移過程,首先了解了本系統的系統特性和曆史由來,然後借著例程學習了內核如線程管理、線程間通信——郵箱、線程間同步——信號量等,最後遷移過程首先分析了裸機大while()+中斷方式的控制邏輯,然後經過四個部分遷移結束。由於RT-Thread 主要采用 C 語言編寫,淺顯易懂,方便移植的特性,整個學習和遷移工作在1天內完成。初期調試工作花了半天時間。

使用RT-Thread操作系統,將原來的PIT中斷函數用操作系統的軟定時器模擬,大while()裏面的顯示器、蜂鳴器、串口打印等操作分成不同的線程,並合理配置優先級。這樣就慢慢觸到了多線程並發+模塊化開發的好處,還有系統級通信方式郵箱、信號量等的使用,减少了全局變量的應用,也使得標志比特的設置和改變、操作一些外設(如蜂鳴器)更為方便。FinSH的成功移植則大大提高了調試效率。

過程中也出現了一些意想不到的問題,比如如果1ms改一次電機的PWM,程序會死機,分析原因可能是1ms一次太頻繁,程序還沒執行結束下一個中斷又開始了,造成“棧爆”的結果。所以改為5ms一次後就解决了。還有一些其他的問題,也一一解决了。經檢驗,移植效果很好。

 

§05 結與反思


5.1硬件電路設計的學習過程與心得

  1. 由於舵機在打角過程中會因為地面摩擦力的原因會出現短暫的堵轉,從而產生過載大電流。在調試過程中發現舵機的堵住會導致舵機供電部分的穩壓芯片發熱比較嚴重。所以在智能車主板的繪制過程中,應充分考慮舵機供電部分的穩壓芯片MIC29302WU的散熱問題。為此,應在PCB板上穩壓芯片的比特置下放置散熱孔矩陣。
  2. 在智能車的調試過程中,對參數的調節是不可避免的,而智能車使用的單片機程序燒錄時間長,僅僅調節一個參數就重新燒錄程序就造成了時間上的浪費。為此,在主板電路設計的過程中,可以使用多路撥碼開關,然後結合程序實現簡易調參,大大節約了調試時間。
  3. 在智能車主板設計過程中,應充分考慮到驅動電路、電機、舵機等器件易損壞的問題。為此智能車主板上應留出備用的接口,以便不時之需。

5.2智能車機械結構設計和調整的學習過程與心得

  1. 前輪後束有助於小車的入彎,但在直道上的穩定性較差,同時過度的前輪後束會導致過彎半徑過大,在一些急彎會沖出賽道。采用前輪前束可以提高小車跑直道的能力,同時在過彎時的錶現也能够滿足要求。
  2. 車輪和賽道的幹濕程度和清潔度對於輪胎的摩擦因數影響很大,進而影響到舵機PD系數的調節。輪胎不淨會使車在高速時側滑現象增多,嚴重影響小車的過彎能力,所以在發車和調車時,一定要先用幹淨浸潤的濕毛巾擦拭車輪和賽道,使車輪的摩擦因數盡量保持在一個較為穩定的數值。
  3. 電磁電感安裝板與信號板一開始使用杜邦線進行連接,但這種連接方式會產生較大的電磁信號幹擾,使得電感數值不穩定,影響後續相關參數的計算。後續便將連接處改為了FPC接口,使用FPC線進行連接,這樣在最大程度上减少了電磁幹擾,提高了讀數的准確性,令車身在行駛時更加穩定。
  4. 在導線和PCB板的連接處及電感安裝處使用熱熔膠進行封塗,一方面可以避免短路和接觸不良,另一方面,電感經過熱熔膠的固定,可以减少因碰撞帶來的移比特,起到保護電感的作用。
  5. 輪胎在長時間使用後需用軟化劑進行軟化處理,提高輪胎的摩擦力,使小車行駛更加順暢,不易出現打滑的現象,提高小車的速度上限。
  6. PCB板和舵機電機等器件一定要有備用件,以應對可能到來的突發損壞。

5.3智能車RT-Thread操作系統移植過程的學習過程與心得

  1. 使用RT-Thread操作系統,將原來的PIT中斷函數用操作系統的軟定時器代替,大while()裏面的顯示器顯示、蜂鳴器、串口打印等操作分成不同的線程,優先級低於定時器線程。使得程序邏輯更加清晰,小車跑得更為順滑。
  2. 在移植過程中也出現了一些意想不到的問題,比如如果1ms改一次電機的PWM程序會死機,1ms發一次串口系統也會死機。分析原因可能是1ms一次太頻繁,程序還沒執行結束下一個中斷又開始了。所以改為5ms一次後就解决了。還有一些其他的問題,也一一解决了。
  3. 經檢驗,小車達到預期效果,可以順利完成比賽任務。在華東賽預賽、决賽賽場上經受住了考驗,順利完賽,取得了預賽第12(44.005s)、决賽第7(103.007s)的較好成績。

▲ 圖5.1 基礎四輪組華東賽區初賽

▲ 圖5.1 基礎四輪組華東賽區初賽

▲ 圖5.2 華東賽區决賽基礎四輪組賽場

▲ 圖5.2 華東賽區决賽基礎四輪組賽場

▲ 圖5.3 華東賽區决賽頒獎現場圖片

▲ 圖5.3 華東賽區决賽頒獎現場圖片

5.5關於智能車搭建、調試過程中的總結與反思

  1. 在使用電感值的特征變化檢測環島的過程中,我們發現當前的電感排布方案並不能很精確檢測到環島。原因在於我們沒有根據環島附近磁場的變化趨勢設計出一種更為契合的電感排布方案。為此我們可以通過調整電感離地高度、方向、數量來設計出一種能准確檢測出特殊賽道元素的電感排布方案。
  2. 關於嵌入式開發和純軟件開發的最大不同點。嵌入式開發的輸出是靠許多設備的響應來實現的。因此編程過程中需要注意程序與引脚的對應。

5.6 關於基於RT-Thread操作系統進行智能車開發的總結與反思

5.6.1.不用怕,重要是的開始了解。

之前覺得操作系統比較難,不敢去觸碰,但是當有了一學期的嵌入式課程的學習和競賽經曆以後,移植操作是相對比較容易的。重要的前提是對RT-Thread操作系統有先驗的知識,關於嵌入式實時操作系統的理解,對線程、時鐘管理、優先級、郵箱、事件、信號量等內容的理解。快速的移植上手和RT-Thread系統本身淺顯易懂、方便移植的特性是分不開的。

5.6.2.如何更快的移植?

重要的是參考RTT官方的文檔和逐飛的庫。站在巨人的肩膀上可以更快的達成目的,完成移植。具體而言,即首先創建需要的線程及其初始化,創建時鐘,然後把自己編寫的函數.c和.h文件都複制到對應的文件目錄,包含頭文件和進行函數調用。然後就可以調試了。

5.6.3.减少無用的cpu空轉。

裸機大while()+中斷模式,有兩個操作會使得cpu空轉,其一是延時函數delay(),流水燈和一些任務都離不開delay().但是delay()一方面會使得cpu利用率大大降低,另一方面也會降低並列任務的執行頻率,影響執行結果,所以後續增加的任務可能會影響前面正常運行的任務,給調試、拓展和維護造成很大的不便。中斷裏面用delay()則會帶來許多麻煩,特別是delay()時間超過中斷周期。其二是while()詢問等待。如果條件不滿足,就會一直空轉,條件滿足才能繼續。造成的結果和delay()差不多。而RT-Thread系統很好的解决了這兩個問題。系統的delay()會釋放cpu的使用權,在這段時間cpu可以執行其他任務。詢問等待也是一樣,如果條件不滿足同樣會將當前進程掛起釋放cpu的使用權,條件滿足才繼續。這樣一來,大大减少了無用的cpu空轉,提高了cpu的利用效率,也减少了任務之間的互相影響,提高了開發、拓展和維護的效率。

5.6.4.减少全局變量的使用。

對於嵌入式系統程序開發,對於全局變量應該“少用,慎用,甚至不用”,以避免不必要的意外情况。但是智能車備賽過程中,傳統編程模式下,使用大量全局變量是不可避免的,因為總要進行不同任務間的通信。而RT-Thread系統完全拋弃了全局變量的通信模式,而是用郵箱、信號量、互斥量等方式完成通信。利用系統級別的通信方式,一方面方便管理線程之間的通信,另一方面也很好的規避了全局變量的大量使用帶來的問題,大大改善了程序結構。

5.6.5.利用RT-Thread系統開發的優勢總結

利用傳統裸機大while()+中斷模式也可以基本實現智能車的功能和完成比賽任務。但是利用RT-Thread系統,使用模塊化多線程並發+系統級通信的方式,除了可以極大的提高cpu的使用率,也可以使開發過程更為順暢,編程邏輯更為清晰,後續調試更為方便,更加順利地完成智能車比賽任務以外,代碼的可移植性和系統編程風格的一貫性對我們有利無害,尤其是對於可能要准備兩年智能車比賽、切換組別的同學,可减少從頭開始熟悉編程平臺的時間。更重要的是,熟悉和掌握物聯網時代嵌入式開發的思維方式,熟悉RT-Thread豐富的軟件生態,對於我們未來的發展和科創任務的進行一定大有裨益。

5.7 不足與展望

由於時間有限,目前我們對於RT-Thread系統的內核特性應用較多,而對組件部分只使用了FinSH控制臺,,希望在接下來的開發過程中不斷了解虛擬文件系統、ulog日志等其他有力組件,充分發揮RT-Thread的性能和優勢。

參考文獻:

[1] 卓晴,任豔頻,江永亨,王京春. 為你癡為你狂,小車載我夢飛翔-----全國大學生智能汽車競賽12周年紀念文集[M]. 北京:清華大學出版社, 2019.
[2] 王盼寶,樊越驍,曹楠,等. 能車制作——從元器件、機電系統、控制算法到完整的智能車設計[M]. 北京:清華大學出版社, 2018.
[3] 全國大學生智能車競賽組織委員會. 第十六届全國大學生智能汽車競賽競速比賽規則[EB/OL].[2020-12-25] https://bj.bcebos.com/cdstm-hyetecforthesmartcar-bucket/source/doc-6xpb9limppg0.pdf
[5] 李洋,趙寧,張磊. 第十五届全國大學生智能汽車競賽四輪組技術報告–哈爾濱工業大學紫丁香一隊[EB/OL].[2020-09-20] https://blog.csdn.net/zhuoqingjoking97298/article/details/108506709
[6] Infineon Technologies AG. TC26x B-step User Manual[EB/OL]. [2019-03-29] https://www.infineon.com/dgdl/Infineon-TC26x_B-step-UM-v01_03-EN.pdf?fileId=5546d46269bda8df0169ca09970023e8
[7] 童詩白,華成英. 模擬電子技術基礎(第五版)[M]. 北京:高等教育出版社, 2015.
[8] 閻石. 數字電子技術基礎(第六版)[M]. 北京: 高等教育出版社, 2016.
[9] 胡壽松. 自動控制原理(第七版)[M]. 北京:科學出版社, 2019.
[10] RT-Thread Development Team. RT-Thread API參考手册v3.1.1[EB/OL].[2021-06-24] https://www.rt-thread.org/document/api/
[11] RT-Thread Development Team. RT-Thread文檔中心(標准版本)[EB/OL].[2021-06-24] https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/README
[12] 邱禕,熊譜翔,朱天龍. 嵌入式實時操作系統:RT-Thread設計與實現[M]. 北京:機械工業出版社, 2019.
[13] 劉火良,楊森. RT-Thread內核實現與應用開發實戰指南–基於STM32[M]. 北京:機械工業出版社, 2019.
[14] 李楊,金華.基於RT-Thread的智能小車控制系統設計[J].計算機產品與流通,2020(08):93.
[15] 熊斯年,王海軍,吳小濤.基於RT-Thread的波浪滑翔器控制系統設計與實現[J].數字海洋與水下攻防,2020,3(04):339-344.
[16] 鄭蕊,王曉榮,吳棋,李明朗,張冬華.基於STM32F4大氣監測系統微站的軟硬件設計[J].儀錶技術與傳感器,2020(09):98-100+105.

附錄A.遷移時間錶:

  • 6.25 17:59

  • 收到了TC264適配RT-Thread操作系統的消息。

  • 6.27 18:00

  • 確定了嵌入式Project的主題,就是給小車適配RT-Thread

  • 6.28 10:00-14:00

  • 收集相關資料,開始寫報告,並學習RTT官方文檔

  • 6.28 22:00-24:00

  • 學習逐飛官方的庫,並開始移植

  • 6.29 13:30-16:40

  • 上車調試,依次解决各種問題,並產生新問題。

  • 6.30 16:50

  • 移植成功,測試通過。

  • 7.01-7.15

  • 實驗室內部測試。

  • 7.16-7.18

  • 華東賽區初賽及决賽。

附錄B 部分源代碼

Cpu0_Main.c
#include "Cpu0_Main.h" 
#include "headfile.h" 
#include "display.h" 
#include "timer_pit.h" 
#include "encoder.h" 
#include "buzzer.h" 
#include "button.h" 
#include "motor.h" 
#include "elec.h" 
#include "My_head.h" 
rt_sem_t camera_sem;
int main(void)
{

//等待所有核心初始化完畢
IfxCpu_emitEvent(&g_cpuSyncEvent);
IfxCpu_waitEvent(&g_cpuSyncEvent, 0xFFFF);
camera_sem = rt_sem_create("camera", 0, RT_IPC_FLAG_FIFO);
rt_kprintf("test\n");
mt9v03x_init();
//icm20602_init_spi();
rt_kprintf("test finish\n");
display_init();
encoder_init();
buzzer_init();
button_init();
motor_init();
elec_init();
timer_pit_init();
//初始化LED引脚
gpio_init(P20_8, GPO, 1, PUSHPULL);
gpio_init(P20_9, GPO, 1, PUSHPULL);
//與驅動板引脚沖突
// gpio_init(P21_4, GPO, 1, PUSHPULL);
// gpio_init(P21_5, GPO, 1, PUSHPULL);
gpio_init(P14_4,GPI,0,NO_PULL);
//車一啟動先響一聲
rt_mb_send(buzzer_mailbox, 100);
while(1)
{

//等待攝像頭采集完畢
rt_sem_take(camera_sem, RT_WAITING_FOREVER);
//開始處理攝像頭圖像
DealGarage();
//處理完成需要將標志比特置0
mt9v03x_finish_flag = 0;
//翻轉LED引脚
gpio_toggle(P20_8);
gpio_toggle(P20_9);
}
}
#pragma section all restore 
timer_pit.c
#include "encoder.h" 
#include "motor.h" 
#include "timer_pit.h" 
#include "elec.h" 
#include"signal.h" 
#include"My_head.h" 
void timer1_pit_entry(void *parameter)
{

static uint32 time;
time++;
// if(0 == (time%100))
// {

// //采集編碼器數據
// encoder_get();
// }
if(0 == (time%5))
{

//電磁信號采集、歸一化、PID控制
elec_calculate();
//采集編碼器數據
encoder_get();
//控制電機轉動
motor_control();
}
//rt_kprintf("test motor \n");
}
void timer_pit_init(void)
{

rt_timer_t timer;
//創建一個定時器 周期運行
timer = rt_timer_create("timer1", timer1_pit_entry, RT_NULL, 1, RT_TIMER_FLAG_PERIODIC);
//啟動定時器
if(RT_NULL != timer)
{

rt_timer_start(timer);
}
Buzzer.c
#include "buzzer.h" 
#define BUZZER_PIN P02_6 // 定義主板上蜂鳴器對應引脚 
rt_mailbox_t buzzer_mailbox;
void buzzer_entry(void *parameter)
{

uint32 mb_data;
while(1)
{

//接收郵箱數據,如果沒有數據則持續等待並釋放CPU控制權
rt_mb_recv(buzzer_mailbox, &mb_data, RT_WAITING_FOREVER);
gpio_set(BUZZER_PIN, 1); //打開蜂鳴器
rt_thread_mdelay(mb_data); //延時
gpio_set(BUZZER_PIN, 0); //關閉蜂鳴器
}
}
void buzzer_init(void)
{

rt_thread_t tid;
//初始化蜂鳴器所使用的GPIO
gpio_init(BUZZER_PIN, GPO, 0, PUSHPULL);
//創建郵箱
buzzer_mailbox = rt_mb_create("buzzer", 5, RT_IPC_FLAG_FIFO);
//創建蜂鳴器的線程
tid = rt_thread_create("buzzer", buzzer_entry, RT_NULL, 256, 20, 2);
//啟動線程
if(RT_NULL != tid)
{

rt_thread_startup(tid);
}
}
Display.c
#include "headfile.h" 
#include "encoder.h" 
#include "display.h" 
#include"signal.h" 
#include "My_head.h" 
extern int point_sum ;
void display_entry(void *parameter)
{

while(1)
{

if(gpio_get(P14_4)){

if(0){

for(int16 i=0;i<7;i++){

lcd_showuint16(5, i, signals_long[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,signals_short[i]);
}
printf("point_num: %d\n", point_sum);
}
if(0){

for(int16 i=0;i<7;i++){

lcd_showuint16(5, i, AD_data[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,AD_data[i]);
}
}
if(0){

for(int16 i=0;i<7;i++){

lcd_showuint16(5, i, AD_G_S[i]);
//lcd_showint8(60,i,send_buff[i+2]);
lcd_showuint16(120,i,AD_G_S[i]);
}
}
if(1){

for(int16 i=0;i<7;i++){

lcd_showuint16(5,i,AD_G_L[i]);
lcd_showuint16(120, i, AD_G_S[i]);
}
if(ShortSmotorFlag==0){

lcd_showstr(120,7," ");
lcd_showuint16(5,7,0);
}
else{

lcd_showstr(5,7," ");
lcd_showuint16(120,7,1);
}
virtual_Osc_Test();
printf("point_num: %d\n", point_sum);
}
}
rt_thread_mdelay(10);
}
}
void display_init(void)
{

rt_thread_t tid;
//lcd初始化
lcd_init();
//創建顯示線程 優先級設置為6
tid = rt_thread_create("display", display_entry, RT_NULL, 256, 31, 30);
//啟動顯示線程
if(RT_NULL != tid)
{

rt_thread_startup(tid);
}
}

附錄C第十六届全國大學智能車競賽華東賽區基礎四輪組一等獎隊伍信息
來源頁面:
卓晴。第十六届全國大學智能車競賽華東賽區成績匯總。
https://zhuoqing.blog.csdn.net/article/details/119098182

版权声明:本文为[卓晴]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815125150337g.html