嵌入式系統(tǒng)不只是ARM+Linux,不是只有安卓,凡是電子產(chǎn)品都可稱為嵌入式系統(tǒng)。物聯(lián)網(wǎng)行業(yè)的興起,也提升了FreeRTOS市場占有率。本文就是介紹FreeRTOS基礎(chǔ)及其應(yīng)用,只是個人整理,可能存在問題,其目的只是簡要介紹系統(tǒng)的基礎(chǔ),只能作為入門資料。
目錄
一、為什么要學(xué)習(xí)RTOS?
二、操作系統(tǒng)基礎(chǔ)
三、初識 FreeRTOS
四、任務(wù)
五、隊列?
六、軟件定時器
七、信號量
八、事件?
九、任務(wù)通知
十、內(nèi)存管理
十一、通用接口
一、 為什么要學(xué)習(xí) RTOS
進入嵌入式這個領(lǐng)域,入門首先接觸的是單片機編程,尤其是C51 單片機來,基礎(chǔ)的單片機編程通常都是指裸機編程,即不加入任何 RTOS(Real Time Operating System 實時操作系統(tǒng))。常用的有國外的FreeRTOS、μC/OS、RTX 和國內(nèi)的 RT-thread、Huawei LiteOS 和 AliOS-Things 等,其中開源且免費的 FreeRTOS 的市場占有率較高。
1.1前后臺系統(tǒng)
在裸機系統(tǒng)中,所有的操作都是在一個無限的大循環(huán)里面實現(xiàn),支持中斷檢測。外部中斷緊急事件在中斷里面標記或者響應(yīng),中斷服務(wù)稱為前臺,main 函數(shù)里面的while(1)無限循環(huán)稱為后臺,按順序處理業(yè)務(wù)功能,以及中斷標記的可執(zhí)行的事件。小型的電子產(chǎn)品用的都是裸機系統(tǒng),而且也能夠滿足需求。
1.2多任務(wù)系統(tǒng)
多任務(wù)系統(tǒng)的事件響應(yīng)也是在中斷中完成的,但是事件的處理是在任務(wù)中完成的。如果事件對應(yīng)的任務(wù)的優(yōu)先級足夠高,中斷對應(yīng)的事件會立刻執(zhí)行。相比前后臺系統(tǒng),多任務(wù)系統(tǒng)的實時性又被提高了。
在多任務(wù)系統(tǒng)中,根據(jù)程序的功能,把這個程序主體分割成一個個獨立的,無限循環(huán)且不能返回的子程序,稱之為任務(wù)。每個任務(wù)都是獨立的,互不干擾的,且具備自身的優(yōu)先級,它由操作系統(tǒng)調(diào)度管理。加入操作系統(tǒng)后,開發(fā)人員不需要關(guān)注每個功能模塊之間的沖突,重心放在子程序的實現(xiàn)。缺點是整個系統(tǒng)隨之帶來的額外RAM開銷,但對目前的單片機的來影響不大。
1.3學(xué)習(xí)RTOS的意義
學(xué)習(xí) RTOS,一是項目需要,隨著產(chǎn)品要實現(xiàn)的功能越來越多,單純的裸機系統(tǒng)已經(jīng)不能完美地解決問題,反而會使編程變得更加復(fù)雜,如果想降低編程的難度,就必須引入 RTOS實現(xiàn)多任務(wù)管理。二是技能需要,掌握操作系統(tǒng),和基于RTOS的編程,實現(xiàn)更好的職業(yè)規(guī)劃,對個人發(fā)展尤其是錢途是必不可少的。
以前一直覺得學(xué)操作系統(tǒng)就必須是linux,實際每個系統(tǒng)都有其應(yīng)用場景,對于物聯(lián)網(wǎng)行業(yè),殺雞焉用牛刀,小而美,且應(yīng)用廣泛的FreeRTOS 是首選。有一個操作系統(tǒng)的基礎(chǔ),即使后續(xù)基于其他系統(tǒng)開發(fā)軟件,也可觸類旁通,對新技術(shù)快速入門。目前接觸的幾款芯片都是基于FreeRTOS。
如何學(xué)習(xí)RTOS?最簡單的就是在別人移植好的系統(tǒng)之上,看看 RTOS 里面的 API 使用說明,然后調(diào)用這些 API 實現(xiàn)自己想要的功能即可。完全不用關(guān)心底層的移植,這是最簡單快速的入門方法。這種學(xué)習(xí)方式,如果是做產(chǎn)品,可以快速的實現(xiàn)功能,弊端是當(dāng)程序出現(xiàn)問題的時候,如果對RTOS不夠了解,會導(dǎo)致調(diào)試困難,無從下手。
各種RTOS內(nèi)核實現(xiàn)方式都差不多,我們只需要深入學(xué)習(xí)其中一款就行。萬變不離其宗,正如掌握了C51基礎(chǔ),后續(xù)換其他型號或者更高級的ARM單片機,在原理和方法上,都是有借鑒意義,可以比較快的熟悉并掌握新單片機的使用。
二、 操作系統(tǒng)基礎(chǔ)
2.1鏈表
鏈表作為 C 語言中一種基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),在平時寫程序的時候用的并不多,但在操作系統(tǒng)里面使用的非常多。FreeRTOS 中存在著大量的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)鏈表和鏈表項的操作(list 和 list item)。FreeRTOS 中與鏈表相關(guān)的操作均在 list.h 和 list.c 這兩個文件中實現(xiàn)。
鏈表比數(shù)組,最大優(yōu)勢是占用的內(nèi)存空間可以隨著需求擴大或縮小,動態(tài)調(diào)整。實際FreeRTOS中各種任務(wù)的記錄都是依靠鏈表動態(tài)管理,具體的可以參考源碼的任務(wù)控制塊tskTCB。任務(wù)切換狀態(tài),就是將對應(yīng)的鏈表進行操作,鏈表操作涉及創(chuàng)建和插入、刪除和查找。
2.2隊列
隊列是一種只允許在表的前端(front)進行刪除操作,而在表的后端(rear)進行插入操作。隊尾放入數(shù)據(jù),對頭擠出。先進先出,稱為FIFO
2.3任務(wù)
在裸機系統(tǒng)中,系統(tǒng)的主體就是 main 函數(shù)里面順序執(zhí)行的無限循環(huán),這個無限循環(huán)里面 CPU 按照順序完成各種事情。在多任務(wù)系統(tǒng)中,根據(jù)功能的不同,把整個系統(tǒng)分割成一個個獨立的且無法返回的函數(shù),這個函數(shù)我們稱為任務(wù)。系統(tǒng)中的每一任務(wù)都有多種運行狀態(tài)。系統(tǒng)初始化完成后,創(chuàng)建的任務(wù)就可以在系統(tǒng)中競爭一定的資源,由內(nèi)核進行調(diào)度。?就緒(Ready):該任務(wù)在就緒列表中,就緒的任務(wù)已經(jīng)具備執(zhí)行的能力,只等待調(diào)度器進行調(diào)度,新創(chuàng)建的任務(wù)會初始化為就緒態(tài)。?
?運行(Running):該狀態(tài)表明任務(wù)正在執(zhí)行,此時它占用處理器,調(diào)度器選擇運行的永遠是處于最高優(yōu)先級的就緒態(tài)任務(wù)。?
?阻塞(Blocked):任務(wù)當(dāng)前正在等待某個事件,比如信號量或外部中斷。?
?掛起態(tài)(Suspended):處于掛起態(tài)的任務(wù)對調(diào)度器而言是不可見的。
掛起態(tài)與阻塞態(tài)的區(qū)別,當(dāng)任務(wù)有較長的時間不允許運行的時候,我們可以掛起任務(wù),這樣子調(diào)度器就不會管這個任務(wù)的任何信息,直到調(diào)用恢復(fù)任務(wù)的 接口;而任務(wù)處于阻塞態(tài)的時候,系統(tǒng)還需要判斷阻塞態(tài)的任務(wù)是否超時,是否可以解除阻塞。
各任務(wù)運行時使用消息、信號量等方式進行通信,不能是全局變量。任務(wù)通常會運行在一個死循環(huán)中,不會退出,如果不再需要,可以調(diào)用刪除任務(wù)。
2.4臨界區(qū)
臨界區(qū)就是一段在執(zhí)行的時候不能被中斷的代碼段。在多任務(wù)操作系統(tǒng)里面,對全局變量的操作不能被打斷,不能執(zhí)行到一半就被其他任務(wù)再次操作。一般被打斷,原因就是系統(tǒng)調(diào)度或外部中斷。對臨界區(qū)的保護控制,歸根到底就是對系統(tǒng)中斷的使能控制。在使用臨界區(qū)時,關(guān)閉中斷響應(yīng),對部分優(yōu)先級的中斷進行屏蔽,因此臨界區(qū)不允許運行時間過長。為了對臨界區(qū)進行控制,就需要使用信號量通信,實現(xiàn)同步或互斥操作。
三、初識 FreeRTOS
3.1FreeRTOS源碼
FreeRTOS 由美國的 Richard Barry 于 2003 年發(fā)布, 2018 年被亞馬遜收購,改名為 AWS FreeRTOS,版本號升級為 V10,支持MIT開源協(xié)議,亞馬遜收購 FreeRTOS 也是為了進入物聯(lián)網(wǎng)和人工智能,新版本增加了物聯(lián)網(wǎng)行業(yè)的網(wǎng)絡(luò)協(xié)議等功能。
FreeRTOS 是開源免費的,可從官網(wǎng) www.freertos.org 下載源碼和說明手冊。例如展銳的UIS8910使用的是V10。以FreeRTOSv10.4.1為例,包含 Demo 例程,Source內(nèi)核的源碼,License許可文件。
3.1.1Source 文件夾
FreeRTOS/ Source 文件夾下的文件:
包括FreeRTOS 的通用的頭文件include和 C 文件,包括任務(wù)、隊列、定時器等,適用于各種編譯器和處理器,是通用的。
需要特殊處理適配的在portblle文件夾,其下內(nèi)容與編譯器和處理器相關(guān), FreeRTOS 要想運行在一個單片機上面,它們就必須關(guān)聯(lián)在一起,通常由匯編和 C 聯(lián)合編寫。通常難度比較高,不過一般芯片原廠提供移植好的接口文件。這里不介紹移植的方法,因為自己也不明白。
Portblle/MemMang 文件夾下存放的是跟內(nèi)存管理相關(guān)的,總共有五個 heap 文件,有5種內(nèi)存動態(tài)分配方式,一般物聯(lián)網(wǎng)產(chǎn)品選用 heap4.c 。
3.1.2Demo 文件夾
里面包含了 FreeRTOS 官方為各個單片機移植好的工程代碼,F(xiàn)reeRTOS 為了推廣自己,會給針對不同半導(dǎo)體廠商的評估板實現(xiàn)基礎(chǔ)功能范例, Demo下就是參考范例。
3.1.3FreeRTOSConfig.h配置
FreeRTOSConfig.h頭文件對FreeRTOS 所需的功能的宏均做了定義,需要根據(jù)應(yīng)用情況配置合適的參數(shù),其作用類似MTK功能機平臺的主mak文件,部分定義如下:
?
1.?#define?configUSE_PREEMPTION????????????1?? 2.?#define?configUSE_IDLE_HOOK?????????????0?? 3.?#define?configUSE_TICK_HOOK?????????????0?? 4.?#define?configCPU_CLOCK_HZ??????????????(?SystemCoreClock?)?? 5.?#define?configTICK_RATE_HZ??????????????(?(?TickType_t?)?1000?)??
?
例如系統(tǒng)時鐘tick等參數(shù)在就這個文件配置,具體作用可以看注釋。一般情況下使用SDK不需要改動,特殊情況下咨詢原廠再調(diào)整。
3.2FreeRTOS 編碼規(guī)范
接觸一個新平臺或者SDK,明白它的編碼規(guī)范,文件作用,可以提高源碼閱讀效率,快速熟悉其內(nèi)部實現(xiàn)。
3.2.1數(shù)據(jù)類型
FreeRTOS針對不同的處理器,對標準C的數(shù)據(jù)類型進行了重定義。
?
1.?#define?portCHAR????????char?? 2.?#define?portFLOAT???????float?? 3.?#define?portDOUBLE??????double?? 4.?#define?portLONG????????long?? 5.?#define?portSHORT???????short?? 6.?#define?portSTACK_TYPE??uint32_t?? 7.?#define?portBASE_TYPE???long??
?
應(yīng)用編碼中,推薦使用的是下面這種風(fēng)格。
?
1.?typedef?int?int32_t;?? 2.?typedef?short?int16_t;?? 3.?typedef?char?int8_t;?? 4.?typedef?unsigned?int?uint32_t;?? 5.?typedef?unsigned?short?uint16_t;?? 6.?typedef?unsigned?char?uint8_t;
?
3.2.2變量名
FreeRTOS 中,定義變量的時候往往會把變量的類型當(dāng)作前綴,好處看到就知道其類型。?
char 型變量的前綴是 c?
short 型變量的前綴是 s?
long 型變量的前綴是 l?
復(fù)雜的結(jié)構(gòu)體,句柄等定義的變量名的前綴是 x?
變量是無符號型的再加前綴 u,是指針變量則加前綴 p
3.2.3函數(shù)名
函數(shù)名包含了函數(shù)返回值的類型、函數(shù)所在的文件名和函數(shù)的功能,如果是私有的函數(shù)則會加一個 prv(private)的前綴。?
例如vTaskPrioritySet()函數(shù)的返回值為 void 型,在 task.c 這個文件中定義。
3.2.4宏
宏內(nèi)容是由大寫字母表示,前綴是小寫字母,表示該宏在哪個頭文件定義,如:
?
1.?#define?taskYIELD()?????????????????portYIELD()??
?
表示該宏是在task.h。
3.2.5個人解讀
1、編碼不缺編碼規(guī)范,但是實際使用中很難完全依照標準執(zhí)行,即使freeRTOS源碼也是如此。?
2、關(guān)于函數(shù)或者宏定義中帶文件名的作用,使用Source Insight 編輯代碼,該前綴的意義不大。?
3、規(guī)則是活的,只要所有人都按一個規(guī)則執(zhí)行,它就是標準。
3.3FreeRTOS應(yīng)用開發(fā)
關(guān)于freeRTOS的應(yīng)用開發(fā),主要是任務(wù)的創(chuàng)建和調(diào)度,任務(wù)間的通信與同步,涉及隊列、信號量等操作系統(tǒng)通用接口。結(jié)合應(yīng)用需求,涉及定時器、延時、中斷控制等接口。
特別說明,有些功能的實現(xiàn)方式有多種形式,只針對常用方式進行說明,例如task的創(chuàng)建,只說明動態(tài)創(chuàng)建方式,因為很少使用靜態(tài)方式。
四、任務(wù)
4.1創(chuàng)建任務(wù)
xTaskCreate()使用動態(tài)內(nèi)存的方式創(chuàng)建一個任務(wù)。
?
1.?ret?=?xTaskCreate((TaskFunction_t)?master_task_main,??/*?任務(wù)入口函數(shù)?*/(1) 2.???????????????????“MASTER”,???/*?任務(wù)名字?*/(2) 3.???????????????????64*1024,???/*?任務(wù)棧大小?*/(3) 4.???????????????????NULL,????,/*?任務(wù)入口函數(shù)參數(shù)?*/(4) 5.???????????????????TASK_PRIORITY_NORMAL,??/*?任務(wù)的優(yōu)先級?*/(5) 6.???????????????????&task_master_handler);??/*?任務(wù)控制塊指針?*/(6)
?
創(chuàng)建任務(wù)就是軟件運行時的一個while(1)的入口,一般閱讀其他代碼,找到這個函數(shù),再跟蹤到任務(wù)入口函數(shù),學(xué)習(xí)基于freeRTOS系統(tǒng)的代碼,首先就是找到main和這個接口。
(1):任務(wù)入口函數(shù),即任務(wù)函數(shù)的名稱,需要我們自己定義并且實現(xiàn)。
?(2):任務(wù)名字,字符串形式,最大長度由FreeRTOSConfig.h 中定義的 configMAX_TASK_NAME_LEN 宏指定,多余部分會被自動截掉,只是方便調(diào)試。
(3):任務(wù)堆棧大小,單位為字, 4 個字節(jié),這個要注意,否則系統(tǒng)內(nèi)存緊缺。
(4):任務(wù)入口函數(shù)形參,不用的時候配置為 0 或者NULL 即可。
(5) :任務(wù)的優(yōu)先級,在 FreeRTOS 中,數(shù)值越大優(yōu)先級越高,0代表最低優(yōu)先級?;谄銼DK開發(fā),可將自定義的所有業(yè)務(wù)功能task設(shè)為同一個優(yōu)先級,按時間片輪詢調(diào)度。
(6):任務(wù)控制塊指針,使用動態(tài)內(nèi)存的時候,任務(wù)創(chuàng)建函數(shù)xTaskCreate()會返回一個指針指向任務(wù)控制塊,也可以設(shè)為NULL,因為任務(wù)句柄后期可以不使用。
4.2開啟調(diào)度
當(dāng)任務(wù)創(chuàng)建成功后處于就緒狀態(tài)(Ready),在就緒態(tài)的任務(wù)可以參與操作系統(tǒng)的調(diào)度。操作系統(tǒng)任務(wù)調(diào)度器只啟動一次,之后就不會再次執(zhí)行了,F(xiàn)reeRTOS 中啟動任務(wù)調(diào)度器的函數(shù)是 vTaskStartScheduler(),并且啟動任務(wù)調(diào)度器的時候就不會返回,從此任務(wù)管理都由FreeRTOS 管理,此時才是真正進入實時操作系統(tǒng)中的第一步。
vTaskStartScheduler開啟調(diào)度時,順便會創(chuàng)建空閑任務(wù)和定時器任務(wù)。
FreeRTOS 為了任務(wù)啟動和任務(wù)切換使用了三個異常:SVC、PendSV 和SysTick。
SVC(系統(tǒng)服務(wù)調(diào)用,亦簡稱系統(tǒng)調(diào)用)用于任務(wù)啟動。
PendSV(可掛起系統(tǒng)調(diào)用)用于完成任務(wù)切換,它是可以像普通的中斷一樣被掛起的,它的最大特性是如果當(dāng)前有優(yōu)先級比它高的中斷在運行,PendSV會延遲執(zhí)行,直到高優(yōu)先級中斷執(zhí)行完畢,這樣產(chǎn)生的PendSV 中斷就不會打斷其他中斷的運行。
SysTick 用于產(chǎn)生系統(tǒng)節(jié)拍時鐘,提供一個時間片,如果多個任務(wù)共享同一個優(yōu)先級,則每次 SysTick 中斷,下一個任務(wù)將獲得一個時間片。
FreeRTOS 中的任務(wù)是搶占式調(diào)度機制,高優(yōu)先級的任務(wù)可打斷低優(yōu)先級任務(wù),低優(yōu)先級任務(wù)必須在高優(yōu)先級任務(wù)阻塞或結(jié)束后才能得到調(diào)度。相同優(yōu)先級的任務(wù)采用時間片輪轉(zhuǎn)方式進行調(diào)度(也就是分時調(diào)度),時間片輪轉(zhuǎn)調(diào)度僅在當(dāng)前系統(tǒng)中無更高優(yōu)先級就緒任務(wù)存在的情況下才有效。
4.3啟動方式
FreeRTOS有兩種啟動方式,效果一樣,看個人喜好。
第一種:main 函數(shù)中將硬件初始化, RTOS 系統(tǒng)初始化,所有任務(wù)的創(chuàng)建完成,最后一步開啟調(diào)度。目前看到的幾個芯片SDK都是這種方式。
第二種:main 函數(shù)中將硬件和 RTOS 系統(tǒng)先初始化好,只創(chuàng)建一個任務(wù)后就啟動調(diào)度器,然后在這個任務(wù)里面創(chuàng)建其它應(yīng)用任務(wù),當(dāng)所有任務(wù)都創(chuàng)建成功后,啟動任務(wù)再把自己刪除。
4.4任務(wù)創(chuàng)建源碼分析
xTaskCreate()創(chuàng)建任務(wù)。
?
1.?BaseType_t?xTaskCreate(?TaskFunction_t?pxTaskCode,?? 2.?????????????????????????const?char?*?const?pcName,?/*lint?!e971?Unqualified?char?types?are?allowed?for?strings?and?single?characters?only.?*/?? 3.?????????????????????????const?configSTACK_DEPTH_TYPE?usStackDepth,?? 4.?????????????????????????void?*?const?pvParameters,?? 5.?????????????????????????UBaseType_t?uxPriority,?? 6.?????????????????????????TaskHandle_t?*?const?pxCreatedTask?)?? 7.?{?? 8.?????TCB_t?*?pxNewTCB;?? 9.?????BaseType_t?xReturn;?? 10.??? 11.?????/*?If?the?stack?grows?down?then?allocate?the?stack?then?the?TCB?so?the?stack? 12.??????*?does?not?grow?into?the?TCB.??Likewise?if?the?stack?grows?up?then?allocate? 13.??????*?the?TCB?then?the?stack.?*/?? 14.?????#if?(?portSTACK_GROWTH?>?0?)?? 15.?????????{?? 16.?????????????/**/ 17.?????????}?? 18.?????#else?/*?portSTACK_GROWTH?*/?? 19.?????????{?? 20.?????????????StackType_t?*?pxStack;?? 21.??? 22.?????????????/*?Allocate?space?for?the?stack?used?by?the?task?being?created.?*/?? 23.?????????????pxStack?=?pvPortMalloc(?(?(?(?size_t?)?usStackDepth?)?*?sizeof(?StackType_t?)?)?);?/*lint?!e9079?All?values?returned?by?pvPortMalloc()?have?at?least?the?alignment?required?by?the?MCU's?stack?and?this?allocation?is?the?stack.?*/?? 24.??? 25.?????????????if(?pxStack?!=?NULL?)?? 26.?????????????{?? 27.?????????????????/*?Allocate?space?for?the?TCB.?*/?? 28.?????????????????pxNewTCB?=?(?TCB_t?*?)?pvPortMalloc(?sizeof(?TCB_t?)?);?/*lint?!e9087?!e9079?All?values?returned?by?pvPortMalloc()?have?at?least?the?alignment?required?by?the?MCU's?stack,?and?the?first?member?of?TCB_t?is?always?a?pointer?to?the?task's?stack.?*/?? 29.??? 30.?????????????????if(?pxNewTCB?!=?NULL?)?? 31.?????????????????{?? 32.?????????????????????/*?Store?the?stack?location?in?the?TCB.?*/?? 33.?????????????????????pxNewTCB->pxStack?=?pxStack;?? 34.?????????????????}?? 35.?????????????????else?? 36.?????????????????{?? 37.?????????????????????/*?The?stack?cannot?be?used?as?the?TCB?was?not?created.??Free? 38.??????????????????????*?it?again.?*/?? 39.?????????????????????vPortFree(?pxStack?);?? 40.?????????????????}?? 41.?????????????}?? 42.?????????????else?? 43.?????????????{?? 44.?????????????????pxNewTCB?=?NULL;?? 45.?????????????}?? 46.?????????}?? 47.?????#endif?/*?portSTACK_GROWTH?*/?? 48.??? 49.?????if(?pxNewTCB?!=?NULL?)?? 50.?????{?? 51.?????????#if?(?tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE?!=?0?)?/*lint?!e9029?!e731?Macro?has?been?consolidated?for?readability?reasons.?*/?? 52.?????????????{?? 53.?????????????????/*?Tasks?can?be?created?statically?or?dynamically,?so?note?this? 54.??????????????????*?task?was?created?dynamically?in?case?it?is?later?deleted.?*/?? 55.?????????????????pxNewTCB->ucStaticallyAllocated?=?tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;?? 56.?????????????}?? 57.?????????#endif?/*?tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE?*/?? 58.??? 59.?????????prvInitialiseNewTask(?pxTaskCode,?pcName,?(?uint32_t?)?usStackDepth,?pvParameters,?uxPriority,?pxCreatedTask,?pxNewTCB,?NULL?);?? 60.?????????prvAddNewTaskToReadyList(?pxNewTCB?);?//將新任務(wù)加入到就緒鏈表候著 61.?????????xReturn?=?pdPASS;?? 62.?????}?? 63.?????else?? 64.?????{?? 65.?????????xReturn?=?errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;?? 66.?????}?? 67.??? 68.?????return?xReturn;?? 69.?}
?
申請任務(wù)控制塊內(nèi)存,檢查配置參數(shù),初始化,將任務(wù)信息加入到就緒鏈表,等待調(diào)度。前面鏈表部分提到,freeRTOS的任務(wù)信息都是使用鏈表記錄,在task.c有
?
1.?PRIVILEGED_DATA?static?List_t?pxReadyTasksLists[configMAX_PRIORITIES];//就緒 2.?PRIVILEGED_DATA?static?List_t?xDelayedTaskList1;????//延時 3.?PRIVILEGED_DATA?static?List_t?xDelayedTaskList2;? 4.?PRIVILEGED_DATA?static?List_t?xPendingReadyList;??//掛起 5.?PRIVILEGED_DATA?static?List_t?xSuspendedTaskList;???//阻塞
?
分別記錄就緒態(tài)、阻塞態(tài)和掛起的任務(wù),其中阻塞態(tài)有2個,是因為特殊考慮,時間溢出 的問題,實際開發(fā)單片機項目計時超過24h的可以借鑒。其中pxReadyTasksLists鏈表數(shù)組,其下標就是任務(wù)的優(yōu)先級。4.5任務(wù)調(diào)度源碼分析
創(chuàng)建完任務(wù)的時候,vTaskStartScheduler開啟調(diào)度器,空閑任務(wù)、定時器任務(wù)也是在開啟調(diào)度函數(shù)中實現(xiàn)的。
為什么要空閑任務(wù)?因為 FreeRTOS一旦啟動,就必須要保證系統(tǒng)中每時每刻都有一個任務(wù)處于運行態(tài)(Runing),并且空閑任務(wù)不可以被掛起與刪除,空閑任務(wù)的優(yōu)先級是最低的,以便系統(tǒng)中其他任務(wù)能隨時搶占空閑任務(wù)的 CPU 使用權(quán)。這些都是系統(tǒng)必要的東西,也無需自己實現(xiàn)。
?
1.?void?vTaskStartScheduler(?void?)?? 2.?{?? 3.?????BaseType_t?xReturn;?? 4.??? 5.?????/*?Add?the?idle?task?at?the?lowest?priority.?*/?? 6.?????#if?(?configSUPPORT_STATIC_ALLOCATION?==?1?)?? 7.?????????{?? 8.??????/***/ 9.?????????}?? 10.?????#else?/*?if?(?configSUPPORT_STATIC_ALLOCATION?==?1?)?*/?? 11.?????????{?? 12.?????????????/*創(chuàng)建空閑任務(wù)*/?? 13.?????????????xReturn?=?xTaskCreate(?prvIdleTask,?? 14.????????????????????????????????????configIDLE_TASK_NAME,?? 15.????????????????????????????????????configMINIMAL_STACK_SIZE,?? 16.????????????????????????????????????(?void?*?)?NULL,?? 17.????????????????????????????????????portPRIVILEGE_BIT,??//優(yōu)先級為0 18.????????????????????????????????????&xIdleTaskHandle?);?? 19.?????????}?? 20.?????#endif?/*?configSUPPORT_STATIC_ALLOCATION?*/?? 21.??? 22.?????#if?(?configUSE_TIMERS?==?1?)?? 23.?????????{?? 24.?????????????if(?xReturn?==?pdPASS?)?? 25.?????????????{?? 26.?????????????????//創(chuàng)建定時器task,接收開始、結(jié)束定時器等命令 27.?????????????????xReturn?=?xTimerCreateTimerTask();? 28.?????????????}?? 29.?????????????else?? 30.?????????????{?? 31.?????????????????mtCOVERAGE_TEST_MARKER();?? 32.?????????????}?? 33.?????????}?? 34.?????#endif?/*?configUSE_TIMERS?*/?? 35.??? 36.?????if(?xReturn?==?pdPASS?)?? 37.?????{?? 38.?????????/*?freertos_tasks_c_additions_init()?should?only?be?called?if?the?user? 39.??????????*?definable?macro?FREERTOS_TASKS_C_ADDITIONS_INIT()?is?defined,?as?that?is? 40.??????????*?the?only?macro?called?by?the?function.?*/?? 41.?????????#ifdef?FREERTOS_TASKS_C_ADDITIONS_INIT?? 42.?????????????{?? 43.?????????????????freertos_tasks_c_additions_init();?? 44.?????????????}?? 45.?????????#endif?? 46.??? 47.?????????portDISABLE_INTERRUPTS();?? 48.??? 49.?????????#if?(?configUSE_NEWLIB_REENTRANT?==?1?)?? 50.?????????????{?? 51.?????????????????_impure_ptr?=?&(?pxCurrentTCB->xNewLib_reent?);?? 52.?????????????}?? 53.?????????#endif?/*?configUSE_NEWLIB_REENTRANT?*/?? 54.??? 55.?????????xNextTaskUnblockTime?=?portMAX_DELAY;?? 56.?????????xSchedulerRunning?=?pdTRUE;?? 57.?????????xTickCount?=?(?TickType_t?)?configINITIAL_TICK_COUNT;?? 58.??? 59.?????????portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();?? 60.??? 61.?????????traceTASK_SWITCHED_IN();?? 62.??? 63.?????????/*?Setting?up?the?timer?tick?is?hardware?specific?and?thus?in?the? 64.??????????*?portable?interface.?*/?? 65.?????????if(?xPortStartScheduler()?!=?pdFALSE?)?? 66.?????????{?? 67.?????????????/*?系統(tǒng)開始運行?*/?? 68.?????????}?? 69.?????????else?? 70.?????????{?? 71.?????????????/*?Should?only?reach?here?if?a?task?calls?xTaskEndScheduler().?*/?? 72.?????????}?? 73.?????}?? 74.?????else?? 75.?????{?? 76.????????/*****/ 77.?}?
?
4.6任務(wù)狀態(tài)切換
FreeRTOS 系統(tǒng)中的每一個任務(wù)都有多種運行狀態(tài),具體如下:
?任務(wù)掛起函數(shù)
?
vTaskSuspend()
?
掛起指定任務(wù),被掛起的任務(wù)絕不會得到 CPU 的使用權(quán)
?
vTaskSuspendAll()
?
將所有的任務(wù)都掛起?任務(wù)恢復(fù)函數(shù)
?
vTaskResume() vTaskResume() xTaskResumeFromISR()
?
任務(wù)恢復(fù)就是讓掛起的任務(wù)重新進入就緒狀態(tài),恢復(fù)的任務(wù)會保留掛起前的狀態(tài)信息,在恢復(fù)的時候根據(jù)掛起時的狀態(tài)繼續(xù)運行。xTaskResumeFromISR() 專門用在中斷服務(wù)程序中。無論通過調(diào)用一次或多次vTaskSuspend()函數(shù)而被掛起的任務(wù),也只需調(diào)用一次恢復(fù)即可解掛 。
?任務(wù)刪除函數(shù)vTaskDelete()用于刪除任務(wù)。當(dāng)一個任務(wù)可以刪除另外一個任務(wù),形參為要刪除任 務(wù)創(chuàng)建時返回的任務(wù)句柄,如果是刪除自身, 則形參為 NULL。
4.7任務(wù)使用注意點
1、中斷服務(wù)函數(shù)是不允許調(diào)用任何會阻塞運行的接口。一般在中斷服務(wù)函數(shù)中只做標記事件的發(fā)生,然后通知任務(wù),讓對應(yīng)任務(wù)去執(zhí)行相關(guān)處理 。
2、將緊急的處理事件的任務(wù)優(yōu)先級設(shè)置偏高一些。?
3、空閑任務(wù)(idle 任務(wù))是 FreeRTOS 系統(tǒng)中沒有其他工作進行時自動進入的系統(tǒng)任務(wù),永遠不會掛起空閑任務(wù),不應(yīng)該陷入死循環(huán)。
4、創(chuàng)建任務(wù)使用的內(nèi)存不要過多,按需申請。如果浪費太多,后續(xù)應(yīng)用申請大空間可能提示內(nèi)存不足。
五、隊列
5.1隊列的概念
隊列用于任務(wù)間通信的數(shù)據(jù)結(jié)構(gòu),通過消息隊列服務(wù),任務(wù)或中斷服務(wù)將消息放入消息隊列中。其他任務(wù)或者自身從消息隊列中獲得消息。實現(xiàn)隊列可以在任務(wù)與任務(wù)間、中斷和任務(wù)間傳遞信息。隊列操作支持阻塞等待,向已經(jīng)填滿的隊列發(fā)送數(shù)據(jù)或者從空隊列讀出數(shù)據(jù),都會導(dǎo)致阻塞,時間自定義。消息隊列的運作過程具如下:
5.2隊列創(chuàng)建
xQueueCreate()用于創(chuàng)建一個新的隊列并返回可用于訪問這個隊列的句柄。隊列句柄其實就是一個指向隊列數(shù)據(jù)結(jié)構(gòu)類型的指針。
?
1.?master_queue?=?xQueueCreate(50,?sizeof(task_message_struct_t));??
?
創(chuàng)建隊列,占用50個單元,每個單元為sizeof(task_message_struct_t)字節(jié),和 malloc比較類似。其最終使用的函數(shù)是 xQueueGenericCreate(),后續(xù)信號量等也是使用它創(chuàng)建,只是最后的隊列類型不同。
申請內(nèi)存后,xQueueGenericReset再對其進行初始化,隊列的結(jié)構(gòu)體xQUEUE成員:
?
1.?typedef?struct?QueueDefinition?/*?The?old?naming?convention?is?used?to?prevent?breaking?kernel?aware?debuggers.?*/?? 2.?{?? 3.?????int8_t?*?pcHead;???????????/*?
5.3隊列刪除
隊列刪除函數(shù) vQueueDelete()需傳入要刪除的消息隊列的句柄即可,刪除之后這個消息隊列的所有信息都會被系統(tǒng)回收清空,而且不能再次使用這個消息隊列了。實際應(yīng)用中很少使用。
5.4向隊列發(fā)送消息
任務(wù)或者中斷服務(wù)程序都可以給消息隊列發(fā)送消息,當(dāng)發(fā)送消息時,如果隊列未滿或者允許覆蓋入隊,F(xiàn)reeRTOS 會將消息拷貝到消息隊列隊尾,否則,會根據(jù)用戶指定的超時時間進行阻塞,消息發(fā)送接口很多,最簡單的是 xQueueSend(),用于向隊列尾部發(fā)送一個隊列消息。消息以拷貝的形式入隊,該函數(shù)絕對不能在中斷服務(wù)程序里面被調(diào)用,中斷中必須使用帶有中斷保護功能的 xQueueSendFromISR()來代替。
?
BaseType_t?xQueueSend(QueueHandle_t?xQueue,const?void*?pvItemToQueue,?TickType_t?xTicksToWait);?
用于向隊列尾部發(fā)送一個隊列消息。
參數(shù)
xQueue隊列句柄
pvItemToQueue指針,指向要發(fā)送到隊列尾部的隊列消息。?
xTicksToWait隊列滿時,等待隊列空閑的最大超時時間。如果隊列滿并且xTicksToWait 被設(shè)置成 0,函數(shù)立刻返回。超時時間的單位為系統(tǒng)節(jié)拍周期 tick,延時為 portMAX_DELAY 將導(dǎo)致任務(wù)掛起(沒有超時)。?
返回值
消息發(fā)送成功成功返回 pdTRUE,否則返回 errQUEUE_FULL。
xQueueSendToBack與xQueueSend完全相同, xQueueSendFromISR()與 xQueueSendToBackFromISR(),帶FromISR表示只能在中斷中使用,freeRTOS所以帶這個后綴的都是這個含義。xQueueSendToFront()和QueueSendToFrontFromISR()用于向隊列隊首發(fā)送一個消息。這些在任務(wù)中發(fā)送消息的函數(shù)都是 xQueueGenericSend()展開的宏定義。
?
1.?BaseType_t?xQueueGenericSend(?QueueHandle_t?xQueue,??? 2.??????????????????const?void?*?const?pvItemToQueue,??? 3.??????????????????????????TickType_t?xTicksToWait,??? 4.??????????????????const?BaseType_t?xCopyPosition?)??//發(fā)送數(shù)據(jù)到消息隊列的位置?
一般使用xQueueSend和xQueueSendFromISR,如不確定當(dāng)前運行的是系統(tǒng)服務(wù),還是中斷服務(wù),一般ARM都支持查詢中斷狀態(tài)寄存器判斷,可以封裝一層接口,只管發(fā)消息,內(nèi)部判斷是否使用支持中斷嵌套的版本,UIS8910就是如此。特殊情況下,如發(fā)送網(wǎng)絡(luò)數(shù)據(jù)包未收到服務(wù)器響應(yīng),期望立刻入隊再次發(fā)送它,可以xQueueSendToFront向隊頭發(fā)消息。
5.5從隊列讀取消息
當(dāng)任務(wù)試圖讀隊列中的消息時,可以指定一個阻塞超時時間,當(dāng)且僅當(dāng)消息隊列中有消息的時候,任務(wù)才能讀取到消息。如果隊列為空,該任務(wù)將保持阻塞狀態(tài)以等待隊列數(shù)據(jù)有效。當(dāng)其它任務(wù)或中斷服務(wù)程序往其等待的隊列中寫入了數(shù)據(jù),該任務(wù)將自動由阻塞態(tài)轉(zhuǎn)為就緒態(tài)。當(dāng)任務(wù)等待的時間超過了指定的阻塞時間,即使隊列中尚無有效數(shù)據(jù),任務(wù)也會自動從阻塞態(tài)轉(zhuǎn)移為就緒態(tài)。所有的task主入口while循環(huán)體都是按這個執(zhí)行。例如:
?
1.?static?void?track_master_task_main()?? 2.?{?? 3.?????track_task_message_struct_t?queue_item?=?{0}; 4.?????/****/ 5.??? 6.?????while(1)?? 7.?????{?? 8.?????????if(xQueueReceive(master_queue,?&queue_item,?portMAX_DELAY))//阻塞等待 9.?????????{?? 10.?????????????track_master_task_msg_handler(&queue_item);?? 11.?????????}?? 12.?????}?? 13.?}???
xQueueReceive()用于從一個隊列中接收消息并把消息從隊列中刪除。如果不想刪除消息的話,就調(diào)用 xQueuePeek()函數(shù)。xQueueReceiveFromISR()與xQueuePeekFromISR()是中斷版本,用于在中斷服務(wù)程序中接收一個隊列消息并把消息。這兩個函數(shù)只能用于中斷,是不帶有阻塞機制的,實際項目沒有使用。
5.6查詢隊列使用情況
uxQueueMessagesWaiting()查詢隊列中存儲的信息數(shù)目,具有中斷保護的版本為uxQueueMessagesWaitingFromISR()。查詢隊列的空閑數(shù)目uxQueueSpacesAvailable()。
5.7隊列使用注意點
使用隊列函數(shù)需要注意以下幾點:
1、中斷中必須使用帶FromISR后綴的接口;?
2、發(fā)送或者是接收消息都是以拷貝的方式進行,如果消息內(nèi)容過于龐大,可以將消息的地址作為消息進行發(fā)送、接收。
?
1.?typedef?struct???? 2.?{???? 3.?????TaskHandle_t?src_mod_id;???? 4.?????int?message_id;???? 5.?????int32_t?param;???? 6.?????union???? 7.?????{???? 8.?????????int32_t?result;???? 9.?????????int32_t?socket_id;???? 10.?????};???? 11.?????void*?pvdata;??//大數(shù)據(jù)使用動態(tài)申請內(nèi)存保存,隊列只傳遞指針?? 12.?}?track_task_message_struct_t;????
3、隊列并不屬于任何任務(wù),所有任務(wù)都可以向同一隊列寫入和讀出,一個隊列可以由多任務(wù)或中斷讀寫。?
4、隊列的深度要結(jié)合實際,可以多申請點,前提是每個隊列單元盡可能小。?
5、隊列存在一定限制,在隊頭沒有取出來之前,是無法取出第二個,和STL鏈表存在差異。
六、軟件定時器
6.1軟件定時器的概念
定時器有硬件定時器和軟件定時器之分,硬件定時器是芯片本身提供的定時功能精度高,并且是中斷觸發(fā)方式。軟件定時器是由操作系統(tǒng)封裝的接口,它構(gòu)建在硬件定時器基礎(chǔ)之上,使系統(tǒng)能夠提供不受硬件定時器資源限制,其實現(xiàn)的功能與硬件定時器也是類似的。
在操作系統(tǒng)中,通常軟件定時器以系統(tǒng)節(jié)拍周期為計時單位。系統(tǒng)節(jié)拍配置為configTICK_RATE_HZ,該宏在 FreeRTOSConfig.h 中,一般是100或者1000。根據(jù)實際系統(tǒng) CPU 的處理能力和實時性需求設(shè)置合適的數(shù)值,系統(tǒng)節(jié)拍周期的值越小,精度越高,但是系統(tǒng)開銷也將越大,因為這代表在 1 秒中系統(tǒng)進入時鐘中斷的次數(shù)也就越多。
6.2軟件定時器創(chuàng)建
軟件定時器需先創(chuàng)建才允許使用,動態(tài)創(chuàng)建方式是xTimerCreate(),返回一個句柄。軟件定時器在創(chuàng)建成功后是處于休眠狀態(tài)的,沒有開始計時運行。FreeRTOS的軟件定時器支持單次模式和周期模式。
單次模式:當(dāng)用戶創(chuàng)建了定時器并啟動了定時器后,定時時間到了,只執(zhí)行一次回調(diào)函數(shù),之后不再執(zhí)行。周期模式:定時器會按照設(shè)置的定時時間循環(huán)執(zhí)行回調(diào)函數(shù),直到用戶將定時器停止或刪除。
實際項目中使用這種模式對單片機喂狗就比較省事。
?
1.?TimerHandle_t?xTimerCreate(?const?char?*?const?pcTimerName,?//定時器名稱 2.?????????????????????????????const?TickType_t?xTimerPeriodInTicks,??//定時時間 3.?????????????????????????????const?UBaseType_t?uxAutoReload,??//是否自動重載 4.?????????????????????????????void?*?const?pvTimerID,??//回調(diào)函數(shù)的參數(shù) 5.?????????????????????????????TimerCallbackFunction_t?pxCallbackFunction?)??//回調(diào)函數(shù)?
6.3軟件定時器開啟
新創(chuàng)建的定時器沒有開始計時啟動,可以使用
?
xTimerStart()、 xTimerReset()、 xTimerStartFromISR()?、xTimerResetFromISR()? xTimerChangePeriod()、xTimerChangePeriodFromISR()?
這些函數(shù)將其狀態(tài)轉(zhuǎn)換為活躍態(tài),開始運行。區(qū)別:如果定時器設(shè)定60秒間隔,已經(jīng)運行了30秒,reset是將定時器重置為原來設(shè)定的時間間隔,也就是重新開始延時60秒。ChangePeriod重新設(shè)置計時周期。
6.4軟件定時器停止
xTimerStop() 用于停止一個已經(jīng)啟動的軟件定時器,xTimerStopFromISR()是中斷版本。
6.5軟件定時器刪除
xTimerDelete()用于刪除一個已經(jīng)被創(chuàng)建成功的軟件定時器,釋放資源,刪除之后不能再使用。實際項目中,任務(wù)和隊列都是按需創(chuàng)建,一直使用,但是定時器不使用的就應(yīng)該刪除,并且刪除后一定要將句柄置為NULL。
6.6軟件定時器源碼分析
軟件定時器任務(wù)是在系統(tǒng)開始調(diào)度的時候就被創(chuàng)建:vTaskStartScheduler()—xTimerCreateTimerTask。
?
1.?BaseType_t?xTimerCreateTimerTask(?void?)?? 2.?{?? 3.?????BaseType_t?xReturn?=?pdFAIL;?? 4.??? 5.?????prvCheckForValidListAndQueue();??//創(chuàng)建定時器任務(wù)的隊列 6.??? 7.?????if(?xTimerQueue?!=?NULL?)?? 8.?????{?? 9.?????????#if?(?configSUPPORT_STATIC_ALLOCATION?==?1?)?? 10.?????????????{?? 11.???????????????????????/**/ 12.?????????????}?? 13.?????????#else?/*?if?(?configSUPPORT_STATIC_ALLOCATION?==?1?)?*/?? 14.?????????????{?? 15.??????????????????//創(chuàng)建定時器任務(wù) 16.?????????????????xReturn?=?xTaskCreate(?prvTimerTask,?? 17.????????????????????????????????????????configTIMER_SERVICE_TASK_NAME,?? 18.????????????????????????????????????????configTIMER_TASK_STACK_DEPTH,?? 19.????????????????????????????????????????NULL,?? 20.????????????????????????????????????????(?(?UBaseType_t?)?configTIMER_TASK_PRIORITY?)?|?portPRIVILEGE_BIT,?? 21.????????????????????????????????????????&xTimerTaskHandle?);?? 22.?????????????}?? 23.?????????#endif?/*?configSUPPORT_STATIC_ALLOCATION?*/?? 24.?????}?? 25.??????/**/ 26.?????return?xReturn;?? 27.?}???
任務(wù)創(chuàng)建后,等候命令執(zhí)行
?
1.static?portTASK_FUNCTION(?prvTimerTask,?pvParameters?)?? 2.?{?? 3.??????/**/ 4.??? 5.?????for(?;?;?)?? 6.?????{?? 7.?????????//最近即將超時的定時器還有多長時間溢出 8.?????????xNextExpireTime?=?prvGetNextExpireTime(?&xListWasEmpty?);?? 9.??? 10.?????????//阻塞等待,定時器溢出或受到命令,進入下一步(原因不明) 11.?????????prvProcessTimerOrBlockTask(?xNextExpireTime,?xListWasEmpty?);?? 12.??? 13.?????????//接收命令并處理,見下面 14.?????????prvProcessReceivedCommands();?? 15.?????}?? 16.?}???
所有定時器接口,都是使用xTimerGenericCommand向隊列發(fā)送控制命令,命令如下:
?
1.?#define?tmrCOMMAND_START_DONT_TRACE?????????????(?(?BaseType_t?)?0?)?? 2.?#define?tmrCOMMAND_START????????????????????????(?(?BaseType_t?)?1?)?? 3.?#define?tmrCOMMAND_RESET????????????????????????(?(?BaseType_t?)?2?)?? 4.?#define?tmrCOMMAND_STOP?????????????????????????(?(?BaseType_t?)?3?)?? 5.?#define?tmrCOMMAND_CHANGE_PERIOD????????????????(?(?BaseType_t?)?4?)?? 6.?#define?tmrCOMMAND_DELETE???????????????????????(?(?BaseType_t?)?5?)???
6.7軟件定時器使用注意點
1、查看其他開源代碼,對定時器的使用并不多,但實際項目中過多依賴定時器,導(dǎo)致應(yīng)用邏輯混亂。?
2、freeRTOS 的定時器不是無限制的,其根源是接收定時器控制命令消息的隊列,默認只有10個單元。
?
1.?xTimerQueue?=?xQueueCreate(?(?UBaseType_t?)?configTIMER_QUEUE_LENGTH,?sizeof(?DaemonTaskMessage_t?)?);???
定時器過多,可能出現(xiàn)發(fā)起定時器命令失敗,原因是隊列已滿??梢詫⒛J的10擴大為15,后續(xù)盡量使用信號量來優(yōu)化代碼。?
4、軟件定時器的回調(diào)函數(shù)要快進快出,而且不能有任何阻塞任務(wù)運行的情況,不能有vTaskDelay() 以及其它能阻塞任務(wù)運行的函數(shù)。特別說明,其回調(diào)函數(shù)是在定時器任務(wù)執(zhí)行的,并不是開啟定時器的任務(wù)。
七、信號量
7.1信號量的概念
信號量(Semaphore)是一種實現(xiàn)任務(wù)間通信的機制,可以實現(xiàn)任務(wù)之間同步或臨界資源的互斥訪問,常用于協(xié)助一組相互競爭的任務(wù)來訪問臨界資源。在多任務(wù)系統(tǒng)中,各任務(wù)之間需要同步或互斥實現(xiàn)臨界資源的保護,信號量功能可以為用戶提供這方面的支持??梢院唵握J為是為支持多任務(wù)同時操作的全局變量(個人理解)。
7.1.1二值信號量
比如有一個停車位,多個人都想占用停車,這種情況就可以使用一個變量標記車位狀態(tài),它只有兩種情況,被占用或者沒被占用。在多任務(wù)中使用二值信號量表示,用于任務(wù)與任務(wù)、任務(wù)與中斷的同步。在freeRTOS中,二值信號量看作只有一個消息的隊列,因此這個隊列只能為空或滿。
7.1.2計數(shù)信號量
如果有100個停車位,可以停100輛車,每進去一輛車,車位的數(shù)量就要減一,當(dāng)停車場停滿了 100 輛車的時候,再來的車就不能停進去了。這種場景就需要計數(shù)信號量來表示多個狀態(tài)。二進制信號量可以被認為是長度為 1 的隊列,而計數(shù)信號量則可以被認為長度大于 1 的隊列,信號量使用者依然不必關(guān)心存儲在隊列中的消息,只需關(guān)心隊列是否有消息即可。
7.1.3互斥信號量
還是前面車位問題,只剩一個空車位,雖然員工車離得近,但是領(lǐng)導(dǎo)車來了,要優(yōu)先安排給領(lǐng)導(dǎo)使用,這就是由地位決定。互斥信號量其實是特殊的二值信號量,由于其特有的優(yōu)先級繼承機制從而使它更適用于簡單互鎖,也就是保護臨界資源。
優(yōu)先級翻轉(zhuǎn)問題:假設(shè)有任務(wù)H,任務(wù)M和任務(wù)L三個任務(wù),優(yōu)先級逐次降低。低優(yōu)先級的任務(wù)L搶先占有資源,導(dǎo)致高優(yōu)先級的任務(wù)H阻塞等待,此時再有中等優(yōu)先級的任務(wù)M,它不需要該資源,且優(yōu)先級高于任務(wù)L,它優(yōu)先執(zhí)行;之后再執(zhí)行任務(wù)L,最后才執(zhí)行任務(wù)H??雌饋砭褪歉邇?yōu)先級的任務(wù)反而不如低優(yōu)先級的任務(wù),即優(yōu)先級翻轉(zhuǎn)。
改進型的互斥信號量具有優(yōu)先級繼承機制,操作系統(tǒng)對獲取到臨界資源的任務(wù)提高其優(yōu)先級為所有等待該資源的任務(wù)中的最高優(yōu)先級。一旦任務(wù)釋放了該資源,就恢復(fù)到原來的優(yōu)先級。
任務(wù)L先占用資源,任務(wù)H申請不到資源會進入阻塞態(tài),同時系統(tǒng)就會把當(dāng)前正在使用資源的任務(wù)L的優(yōu)先級臨時提高到與任務(wù)H優(yōu)先級相同,即使任務(wù)M被喚醒了,因為它的優(yōu)先級比任務(wù)H低,所以無法打斷任務(wù)L,因為任務(wù)L的優(yōu)先級被臨時提升到 H;任務(wù)L使用完該資源,任務(wù)H優(yōu)先級最高,將接著搶占 CPU 的使用權(quán),這樣保證任務(wù)H在任務(wù)M前優(yōu)先執(zhí)行。
上面的這些就是為了說明,二值信號量因為優(yōu)先級翻轉(zhuǎn),不能用于對臨界區(qū)的訪問。
7.1.4遞歸互斥信號量
信號量是每獲取一次,可用信號量個數(shù)就會減少一個,釋放一次就增加一個。但是遞歸信號量則不同。對于已經(jīng)獲取遞歸互斥量的任務(wù)可以重復(fù)獲取該遞歸互斥量,該任務(wù)擁有遞歸信號量的所有權(quán)。任務(wù)成功獲取幾次遞歸互斥量,就要返還幾次,在此之前遞歸互斥量都處于無效狀態(tài),其他任務(wù)無法獲取,只有持有遞歸信號量的任務(wù)才能獲取與釋放。類似棧的效果。
7.2二值信號量的應(yīng)用
二值信號量是任務(wù)與任務(wù)間、任務(wù)與中斷間同步的重要手段。例如,任務(wù)A使用串口發(fā)出AT數(shù)據(jù)后,獲取二值信號量無效進入阻塞;
某個時間后,任務(wù)B中串口收到正確的回復(fù),釋放二值信號量。
任務(wù)A就立即從阻塞態(tài)中解除,進入就緒態(tài),等待運行。這種機制用在模塊AT交互很合適。
7.3計數(shù)信號量的應(yīng)用
計數(shù)信號量可以用于資源管理,允許多個任務(wù)獲取信號量訪問共享資源。例如有公共資源車位3個,但是有多個任務(wù)要使用,這種場景就必須使用計數(shù)信號量。三個資源最多支持 3 個任務(wù)訪問,那么第 4 個任務(wù)訪問的時候,會因為獲取不到信號量而進入阻塞。也就是第4個人無法占用車位,必須前面有車離開。等到其中一個有任務(wù)(比如任務(wù) 1) 釋放掉該資源的時候,第 4 個任務(wù)才能獲取到信號量從而進行資源的訪問。其運作的機制類似下圖。
在這里插入圖片描述
7.4互斥信號量的應(yīng)用
多任務(wù)環(huán)境下往往存在多個任務(wù)競爭同一臨界資源的應(yīng)用場景,互斥量可被用于對臨界資源的保護從而實現(xiàn)獨占式訪問?;コ饬靠梢越档托盘柫看嬖诘膬?yōu)先級翻轉(zhuǎn)問題帶來的影響。
比如有兩個任務(wù)需要對串口進行發(fā)送數(shù)據(jù),其硬件資源只有一個,那么兩個任務(wù)肯定不能同時發(fā)送,不然導(dǎo)致數(shù)據(jù)錯誤,那么就可以用互斥量對串口資源進行保護,當(dāng)一個任務(wù)正在使用串口的時候,另一個任務(wù)則無法使用串口,等到前一個任務(wù)使用串口完成后, 另外一個任務(wù)才能獲得串口的使用權(quán)。
另外需要注意的是互斥量不能在中斷服務(wù)函數(shù)中使用,因為其特有的優(yōu)先級繼承機制只在任務(wù)起作用,在中斷的上下文環(huán)境毫無意義。
互斥信號量可以在多個任務(wù)之間進行資源保護,而臨界區(qū)只能是在同一個任務(wù)進行,但是其速度快。(個人理解)
7.5信號量接口
所有信號量semaphore使用套路相近,都是創(chuàng)建creat、刪除delete、釋放give和獲取take四種;釋放和獲取支持任務(wù)級和中斷級FromISR,其中互斥量和遞歸互斥量不支持中斷。使用對應(yīng)的信號量,需要在FreeRTOSConfig.h開啟對應(yīng)的功能。
7.5.1信號量創(chuàng)建
xSemaphoreCreateBinary()用于創(chuàng)建一個二值信號量,并返回一個句柄,默認二值信號量為空,在使用函數(shù) xSemaphoreTake()獲取之前必須 先 調(diào) 用 函 數(shù) xSemaphoreGive() 釋放后才可以獲取。
xSemaphoreCreateCounting()創(chuàng)建計數(shù)信號量。
?
1.?#define?xSemaphoreCreateCounting(?uxMaxCount,?uxInitialCount?)????
uxMaxCount計數(shù)信號量的最大值,當(dāng)達到這個值的時候,信號量不能再被釋放。uxInitialCount創(chuàng)建計數(shù)信號量的初始值。
xSemaphoreCreateMutex()用于創(chuàng)建一個互斥量,并返回一個互斥量句柄,只能被同一個任務(wù)獲取一次,如果同一個任務(wù)想再次獲取則會失敗。
xSemaphoreCreateRecursiveMutex()用于創(chuàng)建一個遞歸互斥量,遞歸信號量可以被同一個任務(wù)獲取很多次,獲取多少次就需要釋放多少次。遞歸信號量與互斥量一樣,都實現(xiàn)了優(yōu)先級繼承機制,可以減少優(yōu)先級反轉(zhuǎn)的反生。
7.5.2信號量刪除
vSemaphoreDelete()用于刪除一個信號量,包括二值信號量,計數(shù)信號量,互斥量和遞 歸互斥量。如果有任務(wù)阻塞在該信號量上,暫時不要刪除該信號量。傳入的參數(shù)為創(chuàng)建時返回的句柄。
7.5.3信號量釋放
當(dāng)信號量有效的時候,任務(wù)才能獲取信號量,信號量變得有效就是釋放信號量。每調(diào)用一次該函數(shù)就釋放一個信號量,注意釋放的次數(shù),尤其是計數(shù)信號量。
xSemaphoreGive()是任務(wù)中釋放信號量的宏,可以用于二值信號量、計數(shù)信號量、互斥量的釋放,但不能釋放由函數(shù)xSemaphoreCreateRecursiveMutex()創(chuàng)建的遞歸互斥量,遞歸互斥信號量用xSemaphoreGiveRecursive()釋放。xSemaphoreGiveFromISR()帶中斷保護釋放一個信號量,被釋放的信號量可以是二值信號量和計數(shù)信號量,不能釋放互斥量和遞歸互斥量,因為互斥量和遞歸互斥量不可在中斷中使用,互斥量的優(yōu)先級繼承機制只能在任務(wù)中起作用。
7.5.4信號量獲取
與釋放信號量對應(yīng)的是獲取信號量,當(dāng)信號量有效的時候,任務(wù)才能獲取信號量,當(dāng)任務(wù)獲取了某個信號量的時候,該信號量的可用個數(shù)就減一,當(dāng)它減到0 的時候,任務(wù)就無法再獲取了,并且獲取的任務(wù)會進入阻塞態(tài)(如果設(shè)定了阻塞超時時間)。
xSemaphoreTake()函數(shù)用于獲取信號量,不帶中斷保護。獲取的信號量對象可以是二值信號量、計數(shù)信號量和互斥量,但是遞歸互斥量并不能使用它。
?
1.?#define?xSemaphoreTake(?xSemaphore,?xBlockTime?)???
xSemaphore信號量句柄?
xBlockTime等待信號量可用的最大超時時間,單位為 tick?
獲取 成 功 則 返 回 pdTRUE ,在 指定的 超時 時間 中 沒 有 獲 取 成 功 則 返 回errQUEUE_EMPTY。
使用xSemaphoreTakeRecursive()獲取遞歸互斥量。xSemaphoreTakeFromISR()是獲取信號量的中斷版本,是一個不帶阻塞機制獲取信號量的函數(shù),獲取對象必須由是已經(jīng)創(chuàng)建的信號量,信號量類型可以是二值信號量和計數(shù)信號量,它與 xSemaphoreTake()函數(shù)不同,它不能用于獲取互斥量,因為互斥量不可以在中斷中使用,并且互斥量特有的優(yōu)先級繼承機制只能在任務(wù)中起作用,而在中斷中毫無意義。
7.6信號量使用注意點
1、建議合理使用信號量進行事件同步處理,減少對定時器的依賴。
2、使用前合理設(shè)定超時時間和依賴關(guān)系,避免多個任務(wù)互相等待對方釋放的信號量而死鎖。
八、事件
8.1事件的概念
信號量用于單個任務(wù)與任務(wù)或任務(wù)與中斷之間的同步,但有些任務(wù)可能與多個任務(wù)由關(guān)聯(lián),此時信號量實現(xiàn)就比較麻煩,可以使用事件機制。
事件是一種實現(xiàn)任務(wù)間通信的機制,多任務(wù)環(huán)境下,任務(wù)、中斷之間往往需要同步操作,一個事件發(fā)生會告知等待中的任務(wù),即形成一個任務(wù)與任務(wù)、中斷與任務(wù)間的同步。事件可以提供一對多、多對多的同步操作。一對多同步模型:一個任務(wù)等待多個事件的觸發(fā),這種情況是比較常見的。
任務(wù)可以通過設(shè)置事件位來實現(xiàn)事件的觸發(fā)和等待操作。FreeRTOS 的事件僅用于同步,不提供數(shù)據(jù)傳輸功能。
8.2事件的應(yīng)用
在某些場合,可能需要多個事件發(fā)生了才能進行下一步操作。各個事件可分別發(fā)送或一起操作事件標志組,而任務(wù)可以等待多個事件,任務(wù)僅對感興趣的事件進行關(guān)注。當(dāng)有感興趣的事件發(fā)生時并且符合感興趣的條件,任務(wù)將被喚醒并進行后續(xù)的處理動作。
其機制類似一個全局變量,子任務(wù)使用特殊的接口函數(shù)對指定的位進行寫1或者清零,主任務(wù)阻塞等待該變量滿足設(shè)定的規(guī)則,則返回運行。
例如項目中的喂狗機制,多個任務(wù),只要有一個任務(wù)發(fā)生異常,則主任務(wù)停止喂狗,等待被重啟。
不使用事件機制,則3個任務(wù)定時向主master task發(fā)送消息,表明自身任務(wù)運行正常;同時master task定時查詢,是否收到3個任務(wù)的消息,如果全都收到表示正常,清除進入下一個定時檢查周期;如果其中一個未收到則表示對應(yīng)任務(wù)異常,故意停止喂狗等待被重啟。
使用事件機制,則相對容易,3個任務(wù)定時設(shè)置對應(yīng)的標志位,master task只需要等待指定的事件位,超時就表示異常;不需要自身定時查詢,也省去了定時發(fā)消息。當(dāng)然缺點是master task只能阻塞等待事件不能執(zhí)行其他業(yè)務(wù)邏輯。
8.3事件接口
xEventGroupCreate()用于創(chuàng)建一個事件組,vEventGroupDelete()刪除事件對象控制塊來釋放系統(tǒng)資源。
事件組置位,任務(wù)中使用 xEventGroupSetBits(),中斷中使用xEventGroupSetBitsFromISR();
xEventGroup事件句柄。uxBitsToSet指定事件中的事件標志位。如設(shè)置 uxBitsToSet 為 0x09 則位 3和位 0 都需要被置位。返回調(diào)用 xEventGroupSetBits() 時事件組中的值。
事件組清除位,任務(wù)中使用xEventGroupClearBits(),中斷中使用 xEventGroupClearBitsFromISR(),都是用于清除事件組指定的位,如果在獲取事件的時候沒有將對應(yīng)的標志位清除,那么就需要用這個函數(shù)來進行顯式清除。
xEventGroup事件句柄。uxBitsToClear指定事件組中的哪個位需要清除。如設(shè)置 uxBitsToSet 為 0x09則位 3和位 0 都需要被清除。
讀取事件標志,任務(wù)中使用 xEventGroupGetBits(),中斷中使用xEventGroupGetBitsFromISR()。
重點是等待事件函數(shù) xEventGroupWaitBits(),獲取任務(wù)感興趣的事件且支持等待超時機制,當(dāng)且僅當(dāng)任務(wù)等待的事件發(fā)生時,任務(wù)才能獲取到事件信息。否則任務(wù)將保持阻塞狀態(tài)以等待事件發(fā)生。當(dāng)其它任務(wù)或中斷服務(wù)程序往其等待的事件設(shè)置對應(yīng)的標志位,該任務(wù)將自動由阻塞態(tài)轉(zhuǎn)為就緒態(tài)。
EventGroupWaitBits()用于獲取事件組中的一個或多個事件發(fā)生標志,當(dāng)要讀取的事件標志位沒有被置位時,任務(wù)將進入阻塞等待狀態(tài)。要想使用該函數(shù)必 須 把FreeRTOS/source/event_groups.c 這個 C 文件添加到工程中。
?
1.?EventBits_t?xEventGroupWaitBits(?EventGroupHandle_t?xEventGroup,?? 2.??????????????????????????????????const?EventBits_t?uxBitsToWaitFor,?? 3.??????????????????????????????????const?BaseType_t?xClearOnExit,?? 4.??????????????????????????????????const?BaseType_t?xWaitForAllBits,?? 5.??????????????????????????????????TickType_t?xTicksToWait?)???
參數(shù)
xEventGroup事件句柄。?
?uxBitsToWaitFor一個按位或的值,指定需要等待事件組中的哪些位置1。如需要等待 bits 0 and/or bit 1 and/or bit 2則 uxBitsToWaitFor 配置為 0x07(0111b)。
xClearOnExitpdTRUE:xEventGroupWaitBits() 等待到滿足任務(wù)喚醒的事件時,系統(tǒng)將清除由形參uxBitsToWaitFor 指定的事件標志位。pdFALSE:不會清除由形參 uxBitsToWaitFor 指定的事件標志位。
xWaitForAllBitspdTRUE :當(dāng)形參 uxBitsToWaitFor 指定的位都置位的時候,xEventGroupWaitBits()才滿足任務(wù)喚醒的條件,這也是“邏輯與”等待事件,并且在沒有超時的情況下返回對應(yīng)的事件標志位的值。pdFALSE:當(dāng)形參 uxBitsToWaitFor 指定的位有其中任意一個置位的時候,這也是常說的“邏輯或”等待事件,在沒有超時的情況下 函數(shù)返回對應(yīng)的事件標志位的值。xTicksToWait最大超時時間,單位為系統(tǒng)節(jié)拍周期
返回值
返回事件中的哪些事件標志位被置位,返回值很可能并不是用戶指定的事件位,需要對返回值進行判斷再處理 。
其應(yīng)用類似某個全局變量,等待事件的任務(wù)在設(shè)定的時間內(nèi),監(jiān)控該變量某些位的值;該值由其他任務(wù)或中斷修改。
九、任務(wù)通知
FreeRTOS 從 V8.2.0 版本開始提供任務(wù)通知這個功能,可以在一定場合下替代 FreeRTOS 的信號量,隊列、事件組等,但是使用也有局限性。將宏定義 configUSE_TASK_NOTIFICATIONS 設(shè)置為 1才能開啟開功能。但該功能并不常用。
十、內(nèi)存管理
10.1內(nèi)存管理的概念
FreeRTOS 內(nèi)存管理模塊管理用于系統(tǒng)中內(nèi)存資源,它是操作系統(tǒng)的核心模塊之一。主要包括內(nèi)存的初始化、分配以及釋放。一般不同的平臺移植代碼,內(nèi)存的動態(tài)申請和釋放接口需要替換。嵌入式實時操作系統(tǒng)中,一般不支持標準C庫中的 malloc()和 free(),其內(nèi)存有限,隨著內(nèi)存不斷被分配和釋放,整個系統(tǒng)內(nèi)存區(qū)域會產(chǎn)生越來越多的碎片。
FreeRTOS提供了 5 種內(nèi)存管理算法,源文件在SourceportableMemMang 路徑下,使用的時候選擇其中一個。heap_1.c、heap_2.c 和 heap_4.c 這三種內(nèi)存管理方案,內(nèi)存堆實際上是一個很大的 數(shù) 組ucHeap。
heap_1.c內(nèi)存管理方案簡單,它只能申請內(nèi)存而不能進行內(nèi)存釋放。有些嵌入式系統(tǒng)并不會經(jīng)常動態(tài)申請與釋放內(nèi)存,一般都是在系統(tǒng)啟動后就一直使用下去,永不刪除,適合這種方式。
heap_2.c 方案支持釋放申請的內(nèi)存,但是它不能把相鄰的兩個小的內(nèi)存塊合成一個大的內(nèi)存塊,對于每次申請內(nèi)存大小都比較固定的;但每次申請并不是固定內(nèi)存大小的則會造成內(nèi)存碎片。如下圖,隨著不斷的申請釋放,空閑空間會變成很多小片段。
heap_3.c 方案只是封裝了標準 C 庫中的 malloc()和 free()函數(shù),由編譯器提供,需要通過編譯器或者啟動文件設(shè)置堆空間。
heap_4.c 方案是在heap_2.c 基礎(chǔ)上,對內(nèi)存碎片進行了改進,能把相鄰的空閑的內(nèi)存塊合并成一個更大的塊,這樣可以減少內(nèi)存碎片。
heap_5.c 方案在實現(xiàn)動態(tài)內(nèi)存分配時與 heap4.c 方案一樣,采用最佳匹配算法和合并算法,并且允許內(nèi)存堆跨越多個非連續(xù)的內(nèi)存區(qū),也就是允許在不連續(xù)的內(nèi)存堆中實現(xiàn)內(nèi)存分配,比如做圖形顯示,可能芯片內(nèi)部的 RAM 不足,額外擴展SDRAM,那這種內(nèi)存管理方案則比較合適。
一般物聯(lián)網(wǎng)平臺使用的是heap_4.c。
10.2內(nèi)存管理接口
不管其內(nèi)部的管理如何實現(xiàn)的,對上層應(yīng)用層的接口都是一樣的。
?
1.?void?*pvPortMalloc(?size_t?xSize?);?//內(nèi)存申請函數(shù)??? 2.?void?vPortFree(?void?*pv?);??????????//內(nèi)存釋放函數(shù)??? 3.?void?vPortInitialiseBlocks(?void?);?//初始化內(nèi)存堆函數(shù)??? 4.?size_t?xPortGetFreeHeapSize(?void?);????//獲取當(dāng)前未分配的內(nèi)存堆大小??? 5.?size_t?xPortGetMinimumEverFreeHeapSize(?void?);?//獲取未分配的內(nèi)存堆歷史最小值???
一般主要是使用內(nèi)存申請和釋放兩個接口,用法和注意事項同malloc/free一樣,成對使用。內(nèi)存釋放后盡量將指針設(shè)為NULL。
十一、通用接口
一些常用接口進行說明。
11.1臨界段
進入和退出臨界段的宏在 task.h 中定義,進入和退出臨界段的宏分中斷保護版本和非中斷版本,但最終都是通過開/關(guān)中斷來實現(xiàn)。主要用于對全局變量的控制,系統(tǒng)使用非常多,但實際項目中沒使用,因為全局變量的異常訪問時小概率問題,只是測試沒發(fā)現(xiàn),理論上是存在問題的。
?
1.?/*?在中斷場合*/??{??? 2.?????uint32_t?ulReturn;??? 3.????? 4.?????ulReturn?=?taskENTER_CRITICAL_FROM_ISR();?/*?進入臨界段,臨界段可以嵌套?*/??? 5.????? 6.?????/*?臨界段代碼?*/?????? 7.???????? 8.?????taskEXIT_CRITICAL_FROM_ISR(?ulReturn?);??}???/*?退出臨界段?*/1.??/*?在非中斷場合?*/??{??? 2.??????? 3.?????taskENTER_CRITICAL();?????/*?進入臨界段?*/? 4.? 5.?????/*?臨界段代碼?*/???? 6.????? 7.?????taskEXIT_CRITICAL();??}???/*?退出臨界段*/???
11.2任務(wù)阻塞延時
vTaskDelay ()阻塞延時,任務(wù)調(diào)用該延時函數(shù)后會被剝離 CPU 使用權(quán),進入阻塞狀態(tài),直到延時結(jié)束。但是該函數(shù)不能用在中斷服務(wù)和定時回調(diào)函數(shù)。延時單位是tick。
11.3獲取系統(tǒng)時鐘計數(shù)值
?
1.?TickType_t?xTaskGetTickCount(?void?)?? 2.?TickType_t?xTaskGetTickCountFromISR(?void?)???
注意該接口分任務(wù)版和中斷版,該接口獲取的是tick計數(shù)值,需要結(jié)合系統(tǒng)時鐘頻率轉(zhuǎn)換成時間。
11.4中斷回調(diào)函數(shù)
和其它平臺不同,中斷回調(diào)中釋放中斷標記即可,freeRTOS中,中斷觸發(fā)后,可能某些阻塞的任務(wù)獲取了相關(guān)信號,需要立刻執(zhí)行,因此中斷服務(wù)發(fā)送消息后,需要主動查詢阻塞任務(wù)的情況,執(zhí)行任務(wù)切換動作。
?
1.?static?uint32_t?ulExampleInterruptHandler(?void?)?? 2.?{?? 3.?????BaseType_t?xHigherPriorityTaskWoken;?? 4.? 5.?????xQueueSendToBackFromISR?(xQueueRx,&cChar,&xHigherPriorityTaskWoken);?? 6.?????portYIELD_FROM_ISR(xHigherPriorityTaskWoken);?? 7.?}??? ? ? 審核編輯:彭靜
評論