微信公眾號:嵌入式系統(tǒng)
嵌入式系統(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
進(jìn)入嵌入式這個領(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),支持中斷檢測。外部中斷緊急事件在中斷里面標(biāo)記或者響應(yīng),中斷服務(wù)稱為前臺,main 函數(shù)里面的while(1)無限循環(huán)稱為后臺,按順序處理業(yè)務(wù)功能,以及中斷標(biāo)記的可執(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)試?yán)щy,無從下手。
各種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)存空間可以隨著需求擴(kuò)大或縮小,動態(tài)調(diào)整。實際FreeRTOS中各種任務(wù)的記錄都是依靠鏈表動態(tài)管理,具體的可以參考源碼的任務(wù)控制塊tskTCB。任務(wù)切換狀態(tài),就是將對應(yīng)的鏈表進(jìn)行操作,鏈表操作涉及創(chuàng)建和插入、刪除和查找。
2.2 隊列
隊列是一種只允許在表的前端(front)進(jìn)行刪除操作,而在表的后端(rear)進(jìn)行插入操作。隊尾放入數(shù)據(jù),對頭擠出。先進(jìn)先出,稱為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)核進(jìn)行調(diào)度。? 就緒(Ready):該任務(wù)在就緒列表中,就緒的任務(wù)已經(jīng)具備執(zhí)行的能力,只等待調(diào)度器進(jìn)行調(diào)度,新創(chuàng)建的任務(wù)會初始化為就緒態(tài)。
? 運行(Running):該狀態(tài)表明任務(wù)正在執(zhí)行,此時它占用處理器,調(diào)度器選擇運行的永遠(yuǎn)是處于最高優(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ù)運行時使用消息、信號量等方式進(jìn)行通信,不能是全局變量。任務(wù)通常會運行在一個死循環(huán)中,不會退出,如果不再需要,可以調(diào)用刪除任務(wù)。
2.4 臨界區(qū)
臨界區(qū)就是一段在執(zhí)行的時候不能被中斷的代碼段。在多任務(wù)操作系統(tǒng)里面,對全局變量的操作不能被打斷,不能執(zhí)行到一半就被其他任務(wù)再次操作。一般被打斷,原因就是系統(tǒng)調(diào)度或外部中斷。對臨界區(qū)的保護(hù)控制,歸根到底就是對系統(tǒng)中斷的使能控制。在使用臨界區(qū)時,關(guān)閉中斷響應(yīng),對部分優(yōu)先級的中斷進(jìn)行屏蔽,因此臨界區(qū)不允許運行時間過長。為了對臨界區(qū)進(jìn)行控制,就需要使用信號量通信,實現(xiàn)同步或互斥操作。
三、 初識 FreeRTOS
3.1 FreeRTOS源碼
FreeRTOS 由美國的 Richard Barry 于 2003 年發(fā)布, 2018 年被亞馬遜收購,改名為 AWS FreeRTOS,版本號升級為 V10,支持MIT開源協(xié)議,亞馬遜收購 FreeRTOS 也是為了進(jìn)入物聯(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.1 Source 文件夾
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.2 Demo 文件夾
里面包含了 FreeRTOS 官方為各個單片機移植好的工程代碼,F(xiàn)reeRTOS 為了推廣自己,會給針對不同半導(dǎo)體廠商的評估板實現(xiàn)基礎(chǔ)功能范例, Demo下就是參考范例。
3.1.3 FreeRTOSConfig.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.2 FreeRTOS 編碼規(guī)范
接觸一個新平臺或者SDK,明白它的編碼規(guī)范,文件作用,可以提高源碼閱讀效率,快速熟悉其內(nèi)部實現(xiàn)。
3.2.1 數(shù)據(jù)類型
FreeRTOS針對不同的處理器,對標(biāo)準(zhǔn)C的數(shù)據(jù)類型進(jìn)行了重定義。
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ī)范,但是實際使用中很難完全依照標(biāo)準(zhǔn)執(zhí)行,即使freeRTOS源碼也是如此。
2、關(guān)于函數(shù)或者宏定義中帶文件名的作用,使用Source Insight 編輯代碼,該前綴的意義不大。
3、規(guī)則是活的,只要所有人都按一個規(guī)則執(zhí)行,它就是標(biāo)準(zhǔn)。
3.3 FreeRTOS應(yīng)用開發(fā)
關(guān)于freeRTOS的應(yīng)用開發(fā),主要是任務(wù)的創(chuàng)建和調(diào)度,任務(wù)間的通信與同步,涉及隊列、信號量等操作系統(tǒng)通用接口。結(jié)合應(yīng)用需求,涉及定時器、延時、中斷控制等接口。
特別說明,有些功能的實現(xiàn)方式有多種形式,只針對常用方式進(jì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)先級。基于其SDK開發(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 管理,此時才是真正進(jìn)入實時操作系統(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)方式進(jì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ù)組,其下標(biāo)就是任務(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ù)重新進(jìn)入就緒狀態(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ù)中只做標(biāo)記事件的發(fā)生,然后通知任務(wù),讓對應(yīng)任務(wù)去執(zhí)行相關(guān)處理 。
2、將緊急的處理事件的任務(wù)優(yōu)先級設(shè)置偏高一些。
3、空閑任務(wù)(idle 任務(wù))是 FreeRTOS 系統(tǒng)中沒有其他工作進(jìn)行時自動進(jìn)入的系統(tǒng)任務(wù),永遠(yuǎn)不會掛起空閑任務(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再對其進(jìn)行初始化,隊列的結(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; /*< Points to the beginning of the queue storage area. */
4. int8_t * pcWriteTo; /*< Points to the free next place in the storage area. */
5. //類型
6. union
7. {
8. QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */
9. SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
10. } u;
11.
12. //當(dāng)前向隊列寫數(shù)據(jù)阻塞的任務(wù)列表或者從隊列取數(shù)阻塞的鏈表
13. List_t xTasksWaitingToSend;
14. List_t xTasksWaitingToReceive;
15.
16. //隊列里有多少個單元被占用,應(yīng)用中需要
17. volatile UBaseType_t uxMessagesWaiting;
18.
19. UBaseType_t uxLength; /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */
20. UBaseType_t uxItemSize; /*< The size of each items that the queue will hold. */
21.
22. /******/
23. } xQUEUE;
5.3 隊列刪除
隊列刪除函數(shù) vQueueDelete()需傳入要刪除的消息隊列的句柄即可,刪除之后這個消息隊列的所有信息都會被系統(tǒng)回收清空,而且不能再次使用這個消息隊列了。實際應(yīng)用中很少使用。
5.4 向隊列發(fā)送消息
任務(wù)或者中斷服務(wù)程序都可以給消息隊列發(fā)送消息,當(dāng)發(fā)送消息時,如果隊列未滿或者允許覆蓋入隊,F(xiàn)reeRTOS 會將消息拷貝到消息隊列隊尾,否則,會根據(jù)用戶指定的超時時間進(jìn)行阻塞,消息發(fā)送接口很多,最簡單的是 xQueueSend(),用于向隊列尾部發(fā)送一個隊列消息。消息以拷貝的形式入隊,該函數(shù)絕對不能在中斷服務(wù)程序里面被調(diào)用,中斷中必須使用帶有中斷保護(hù)功能的 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ù)目,具有中斷保護(hù)的版本為uxQueueMessagesWaitingFromISR()。查詢隊列的空閑數(shù)目uxQueueSpacesAvailable()。
5.7 隊列使用注意點
使用隊列函數(shù)需要注意以下幾點:
1、中斷中必須使用帶FromISR后綴的接口;
2、發(fā)送或者是接收消息都是以拷貝的方式進(jìn)行,如果消息內(nèi)容過于龐大,可以將消息的地址作為消息進(jìn)行發(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)進(jìn)入時鐘中斷的次數(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. //阻塞等待,定時器溢出或受到命令,進(jìn)入下一步(原因不明)
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 的定時器不是無限制的,其根源是接收定時器控制命令消息的隊列,默認(rèn)只有10個單元。
1. xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );
定時器過多,可能出現(xiàn)發(fā)起定時器命令失敗,原因是隊列已滿??梢詫⒛J(rèn)的10擴(kuò)大為15,后續(xù)盡量使用信號量來優(yōu)化代碼。
4、軟件定時器的回調(diào)函數(shù)要快進(jìn)快出,而且不能有任何阻塞任務(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)臨界資源的保護(hù),信號量功能可以為用戶提供這方面的支持??梢院唵握J(rèn)為是為支持多任務(wù)同時操作的全局變量(個人理解)。
7.1.1 二值信號量
比如有一個停車位,多個人都想占用停車,這種情況就可以使用一個變量標(biāo)記車位狀態(tài),它只有兩種情況,被占用或者沒被占用。在多任務(wù)中使用二值信號量表示,用于任務(wù)與任務(wù)、任務(wù)與中斷的同步。在freeRTOS中,二值信號量看作只有一個消息的隊列,因此這個隊列只能為空或滿。
7.1.2 計數(shù)信號量
如果有100個停車位,可以停100輛車,每進(jìn)去一輛車,車位的數(shù)量就要減一,當(dāng)停車場停滿了 100 輛車的時候,再來的車就不能停進(jìn)去了。這種場景就需要計數(shù)信號量來表示多個狀態(tài)。二進(jìn)制信號量可以被認(rèn)為是長度為 1 的隊列,而計數(shù)信號量則可以被認(rèn)為長度大于 1 的隊列,信號量使用者依然不必關(guān)心存儲在隊列中的消息,只需關(guān)心隊列是否有消息即可。
7.1.3 互斥信號量
還是前面車位問題,只剩一個空車位,雖然員工車離得近,但是領(lǐng)導(dǎo)車來了,要優(yōu)先安排給領(lǐng)導(dǎo)使用,這就是由地位決定。互斥信號量其實是特殊的二值信號量,由于其特有的優(yōu)先級繼承機制從而使它更適用于簡單互鎖,也就是保護(hù)臨界資源。
優(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)。
改進(jìn)型的互斥信號量具有優(yōu)先級繼承機制,操作系統(tǒng)對獲取到臨界資源的任務(wù)提高其優(yōu)先級為所有等待該資源的任務(wù)中的最高優(yōu)先級。一旦任務(wù)釋放了該資源,就恢復(fù)到原來的優(yōu)先級。
任務(wù)L先占用資源,任務(wù)H申請不到資源會進(jìn)入阻塞態(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ù)后,獲取二值信號量無效進(jìn)入阻塞;
某個時間后,任務(wù)B中串口收到正確的回復(fù),釋放二值信號量。
任務(wù)A就立即從阻塞態(tài)中解除,進(jìn)入就緒態(tài),等待運行。這種機制用在模塊AT交互很合適。
7.3 計數(shù)信號量的應(yīng)用
計數(shù)信號量可以用于資源管理,允許多個任務(wù)獲取信號量訪問共享資源。例如有公共資源車位3個,但是有多個任務(wù)要使用,這種場景就必須使用計數(shù)信號量。三個資源最多支持 3 個任務(wù)訪問,那么第 4 個任務(wù)訪問的時候,會因為獲取不到信號量而進(jìn)入阻塞。也就是第4個人無法占用車位,必須前面有車離開。等到其中一個有任務(wù)(比如任務(wù) 1) 釋放掉該資源的時候,第 4 個任務(wù)才能獲取到信號量從而進(jìn)行資源的訪問。其運作的機制類似下圖。
在這里插入圖片描述
7.4 互斥信號量的應(yīng)用
多任務(wù)環(huán)境下往往存在多個任務(wù)競爭同一臨界資源的應(yīng)用場景,互斥量可被用于對臨界資源的保護(hù)從而實現(xiàn)獨占式訪問?;コ饬靠梢越档托盘柫看嬖诘膬?yōu)先級翻轉(zhuǎn)問題帶來的影響。
比如有兩個任務(wù)需要對串口進(jìn)行發(fā)送數(shù)據(jù),其硬件資源只有一個,那么兩個任務(wù)肯定不能同時發(fā)送,不然導(dǎo)致數(shù)據(jù)錯誤,那么就可以用互斥量對串口資源進(jìn)行保護(hù),當(dāng)一個任務(wù)正在使用串口的時候,另一個任務(wù)則無法使用串口,等到前一個任務(wù)使用串口完成后, 另外一個任務(wù)才能獲得串口的使用權(quán)。
另外需要注意的是互斥量不能在中斷服務(wù)函數(shù)中使用,因為其特有的優(yōu)先級繼承機制只在任務(wù)起作用,在中斷的上下文環(huán)境毫無意義。
互斥信號量可以在多個任務(wù)之間進(jìn)行資源保護(hù),而臨界區(qū)只能是在同一個任務(wù)進(jìn)行,但是其速度快。(個人理解)
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)建一個二值信號量,并返回一個句柄,默認(rèn)二值信號量為空,在使用函數(shù) xSemaphoreTake()獲取之前必須 先 調(diào) 用 函 數(shù) xSemaphoreGive() 釋放后才可以獲取。
xSemaphoreCreateCounting()創(chuàng)建計數(shù)信號量。
1. #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )
uxMaxCount 計數(shù)信號量的最大值,當(dāng)達(dá)到這個值的時候,信號量不能再被釋放。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()帶中斷保護(hù)釋放一個信號量,被釋放的信號量可以是二值信號量和計數(shù)信號量,不能釋放互斥量和遞歸互斥量,因為互斥量和遞歸互斥量不可在中斷中使用,互斥量的優(yōu)先級繼承機制只能在任務(wù)中起作用。
7.5.4 信號量獲取
與釋放信號量對應(yīng)的是獲取信號量,當(dāng)信號量有效的時候,任務(wù)才能獲取信號量,當(dāng)任務(wù)獲取了某個信號量的時候,該信號量的可用個數(shù)就減一,當(dāng)它減到0 的時候,任務(wù)就無法再獲取了,并且獲取的任務(wù)會進(jìn)入阻塞態(tài)(如果設(shè)定了阻塞超時時間)。
xSemaphoreTake()函數(shù)用于獲取信號量,不帶中斷保護(hù)。獲取的信號量對象可以是二值信號量、計數(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、建議合理使用信號量進(jìn)行事件同步處理,減少對定時器的依賴。
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ā)生了才能進(jìn)行下一步操作。各個事件可分別發(fā)送或一起操作事件標(biāo)志組,而任務(wù)可以等待多個事件,任務(wù)僅對感興趣的事件進(jìn)行關(guān)注。當(dāng)有感興趣的事件發(fā)生時并且符合感興趣的條件,任務(wù)將被喚醒并進(jìn)行后續(xù)的處理動作。
其機制類似一個全局變量,子任務(wù)使用特殊的接口函數(shù)對指定的位進(jìn)行寫1或者清零,主任務(wù)阻塞等待該變量滿足設(shè)定的規(guī)則,則返回運行。
例如項目中的喂狗機制,多個任務(wù),只要有一個任務(wù)發(fā)生異常,則主任務(wù)停止喂狗,等待被重啟。不使用事件機制,則3個任務(wù)定時向主master task發(fā)送消息,表明自身任務(wù)運行正常;同時master task定時查詢,是否收到3個任務(wù)的消息,如果全都收到表示正常,清除進(jìn)入下一個定時檢查周期;如果其中一個未收到則表示對應(yīng)任務(wù)異常,故意停止喂狗等待被重啟。
使用事件機制,則相對容易,3個任務(wù)定時設(shè)置對應(yīng)的標(biāo)志位,master task只需要等待指定的事件位,超時就表示異常;不需要自身定時查詢,也省去了定時發(fā)消息。當(dāng)然缺點是master task只能阻塞等待事件不能執(zhí)行其他業(yè)務(wù)邏輯。
8.3 事件接口
xEventGroupCreate()用于創(chuàng)建一個事件組,vEventGroupDelete()刪除事件對象控制塊來釋放系統(tǒng)資源。
事件組置位,任務(wù)中使用 xEventGroupSetBits(),中斷中使用xEventGroupSetBitsFromISR();
xEventGroup 事件句柄。uxBitsToSet 指定事件中的事件標(biāo)志位。如設(shè)置 uxBitsToSet 為 0x09 則位 3和位 0 都需要被置位。返回調(diào)用 xEventGroupSetBits() 時事件組中的值。
事件組清除位,任務(wù)中使用xEventGroupClearBits(),中斷中使用 xEventGroupClearBitsFromISR(),都是用于清除事件組指定的位,如果在獲取事件的時候沒有將對應(yīng)的標(biāo)志位清除,那么就需要用這個函數(shù)來進(jìn)行顯式清除。
xEventGroup 事件句柄。uxBitsToClear 指定事件組中的哪個位需要清除。如設(shè)置 uxBitsToSet 為 0x09則位 3和位 0 都需要被清除。
讀取事件標(biāo)志,任務(wù)中使用 xEventGroupGetBits(),中斷中使用xEventGroupGetBitsFromISR()。
重點是等待事件函數(shù) xEventGroupWaitBits(),獲取任務(wù)感興趣的事件且支持等待超時機制,當(dāng)且僅當(dāng)任務(wù)等待的事件發(fā)生時,任務(wù)才能獲取到事件信息。否則任務(wù)將保持阻塞狀態(tài)以等待事件發(fā)生。當(dāng)其它任務(wù)或中斷服務(wù)程序往其等待的事件設(shè)置對應(yīng)的標(biāo)志位,該任務(wù)將自動由阻塞態(tài)轉(zhuǎn)為就緒態(tài)。
EventGroupWaitBits()用于獲取事件組中的一個或多個事件發(fā)生標(biāo)志,當(dāng)要讀取的事件標(biāo)志位沒有被置位時,任務(wù)將進(jìn)入阻塞等待狀態(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)。
xClearOnExit pdTRUE:xEventGroupWaitBits() 等待到滿足任務(wù)喚醒的事件時,系統(tǒng)將清除由形參 uxBitsToWaitFor 指定的事件標(biāo)志位。pdFALSE:不會清除由形參 uxBitsToWaitFor 指定的事件標(biāo)志位。
xWaitForAllBits pdTRUE :當(dāng)形參 uxBitsToWaitFor 指定的位都置位的時候,xEventGroupWaitBits()才滿足任務(wù)喚醒的條件,這也是“邏輯與”等待事件,并且在沒有超時的情況下返回對應(yīng)的事件標(biāo)志位的值。pdFALSE:當(dāng)形參 uxBitsToWaitFor 指定的位有其中任意一個置位的時候,這也是常說的“邏輯或”等待事件,在沒有超時的情況下 函數(shù)返回對應(yīng)的事件標(biāo)志位的值。xTicksToWait 最大超時時間,單位為系統(tǒng)節(jié)拍周期
返回值
返回事件中的哪些事件標(biāo)志位被置位,返回值很可能并不是用戶指定的事件位,需要對返回值進(jìn)行 判斷再處理 。
其應(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)中,一般不支持標(biāo)準(zhǔn)C庫中的 malloc()和 free(),其內(nèi)存有限,隨著內(nèi)存不斷被分配和釋放,整個系統(tǒng)內(nèi)存區(qū)域會產(chǎn)生越來越多的碎片。
FreeRTOS提供了 5 種內(nèi)存管理算法,源文件在Source\portable\MemMang 路徑下,使用的時候選擇其中一個。heap_1.c、heap_2.c 和 heap_4.c 這三種內(nèi)存管理方案,內(nèi)存堆實際上是一個很大的 數(shù) 組ucHeap。
heap_1.c內(nèi)存管理方案簡單,它只能申請內(nèi)存而不能進(jìn)行內(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 方案只是封裝了標(biāo)準(zhǔn) C 庫中的 malloc()和 free()函數(shù),由編譯器提供,需要通過編譯器或者啟動文件設(shè)置堆空間。
heap_4.c 方案是在heap_2.c 基礎(chǔ)上,對內(nèi)存碎片進(jìn)行了改進(jìn),能把相鄰的空閑的內(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 不足,額外擴(kuò)展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。
十一、 通用接口
一些常用接口進(jìn)行說明。
11.1 臨界段
進(jìn)入和退出臨界段的宏在 task.h 中定義,進(jìn)入和退出臨界段的宏分中斷保護(hù)版本和非中斷版本,但最終都是通過開/關(guān)中斷來實現(xiàn)。主要用于對全局變量的控制,系統(tǒng)使用非常多,但實際項目中沒使用,因為全局變量的異常訪問時小概率問題,只是測試沒發(fā)現(xiàn),理論上是存在問題的。
1. /* 在中斷場合*/ {
2. uint32_t ulReturn;
3.
4. ulReturn = taskENTER_CRITICAL_FROM_ISR(); /* 進(jìn)入臨界段,臨界段可以嵌套 */
5.
6. /* 臨界段代碼 */
7.
8. taskEXIT_CRITICAL_FROM_ISR( ulReturn ); } /* 退出臨界段 */
1. /* 在非中斷場合 */ {
2.
3. taskENTER_CRITICAL(); /* 進(jìn)入臨界段 */
4.
5. /* 臨界段代碼 */
6.
7. taskEXIT_CRITICAL(); } /* 退出臨界段*/
11.2 任務(wù)阻塞延時
vTaskDelay ()阻塞延時,任務(wù)調(diào)用該延時函數(shù)后會被剝離 CPU 使用權(quán),進(jì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)中釋放中斷標(biā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. }
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!