當前位置:首頁 > 公眾號精選 > wenzi嵌入式軟件
[導讀]嵌入式系統不只是ARM+Linux,不是隻有安卓,凡是電子產品都可稱為嵌入式系統。物聯網行業的興起,也提升了FreeRTOS市場佔有率。本文就是介紹FreeRTOS基礎及其應用,只是個人整理,可能存在問題,其目的只是簡要介紹系統的基礎,只能作為入門資料。

微信公眾號:嵌入式系統

嵌入式系統不只是ARM+Linux,不是隻有安卓,凡是電子產品都可稱為嵌入式系統。物聯網行業的興起,也提升了FreeRTOS市場佔有率。本文就是介紹FreeRTOS基礎及其應用,只是個人整理,可能存在問題,其目的只是簡要介紹系統的基礎,只能作為入門資料。

目錄

一、 為什麼要學習RTOS 

二、 操作系統基礎

三、 初識 FreeRTOS

四、 任務

五、 隊列 

六、 軟件定時器

七、 信號量

八、 事件 

九、 任務通知

十、 內存管理

十一、 通用接口

一、 為什麼要學習 RTOS

進入嵌入式這個領域,入門首先接觸的是單片機編程,尤其是C51 單片機來,基礎的單片機編程通常都是指裸機編程,即不加入任何 RTOS(Real Time Operating System 實時操作系統)。常用的有國外的FreeRTOS、μC/OS、RTX 和國內的 RT-thread、Huawei LiteOS 和 AliOS-Things 等,其中開源且免費的 FreeRTOS 的市場佔有率較高。

1.1 前後台系統

在裸機系統中,所有的操作都是在一個無限的大循環裏面實現,支持中斷檢測。外部中斷緊急事件在中斷裏面標記或者響應,中斷服務稱為前台,main 函數裏面的while(1)無限循環稱為後台,按順序處理業務功能,以及中斷標記的可執行的事件。小型的電子產品用的都是裸機系統,而且也能夠滿足需求。

1.2 多任務系統

多任務系統的事件響應也是在中斷中完成的,但是事件的處理是在任務中完成的。如果事件對應的任務的優先級足夠高,中斷對應的事件會立刻執行。相比前後台系統,多任務系統的實時性又被提高了。

在多任務系統中,根據程序的功能,把這個程序主體分割成一個個獨立的,無限循環且不能返回的子程序,稱之為任務。每個任務都是獨立的,互不干擾的,且具備自身的優先級,它由操作系統調度管理。加入操作系統後,開發人員不需要關注每個功能模塊之間的衝突,重心放在子程序的實現。缺點是整個系統隨之帶來的額外RAM開銷,但對目前的單片機的來影響不大。

1.3 學習RTOS的意義

學習 RTOS,一是項目需要,隨着產品要實現的功能越來越多,單純的裸機系統已經不能完美地解決問題,反而會使編程變得更加複雜,如果想降低編程的難度,就必須引入 RTOS實現多任務管理。二是技能需要,掌握操作系統,和基於RTOS的編程,實現更好的職業規劃,對個人發展尤其是錢途是必不可少的。

以前一直覺得學操作系統就必須是linux,實際每個系統都有其應用場景,對於物聯網行業,殺雞焉用牛刀,小而美,且應用廣泛的FreeRTOS 是首選。有一個操作系統的基礎,即使後續基於其他系統開發軟件,也可觸類旁通,對新技術快速入門。目前接觸的幾款芯片都是基於FreeRTOS。

如何學習RTOS?最簡單的就是在別人移植好的系統之上,看看 RTOS 裏面的 API 使用説明,然後調用這些 API 實現自己想要的功能即可。完全不用關心底層的移植,這是最簡單快速的入門方法。這種學習方式,如果是做產品,可以快速的實現功能,弊端是當程序出現問題的時候,如果對RTOS不夠了解,會導致調試困難,無從下手。

各種RTOS內核實現方式都差不多,我們只需要深入學習其中一款就行。萬變不離其宗,正如掌握了C51基礎,後續換其他型號或者更高級的ARM單片機,在原理和方法上,都是有借鑑意義,可以比較快的熟悉並掌握新單片機的使用。

二、 操作系統基礎

2.1 鏈表

鏈表作為 C 語言中一種基礎的數據結構,在平時寫程序的時候用的並不多,但在操作系統裏面使用的非常多。FreeRTOS 中存在着大量的基礎數據結構鏈表和鏈表項的操作(list 和 list item)。FreeRTOS 中與鏈表相關的操作均在 list.h 和 list.c 這兩個文件中實現。

鏈表比數組,最大優勢是佔用的內存空間可以隨着需求擴大或縮小,動態調整。實際FreeRTOS中各種任務的記錄都是依靠鏈表動態管理,具體的可以參考源碼的任務控制塊tskTCB。任務切換狀態,就是將對應的鏈表進行操作,鏈表操作涉及創建和插入、刪除和查找。

2.2 隊列

隊列是一種只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作。隊尾放入數據,對頭擠出。先進先出,稱為FIFO

2.3 任務

在裸機系統中,系統的主體就是 main 函數裏面順序執行的無限循環,這個無限循環裏面 CPU 按照順序完成各種事情。在多任務系統中,根據功能的不同,把整個系統分割成一個個獨立的且無法返回的函數,這個函數我們稱為任務。系統中的每一任務都有多種運行狀態。系統初始化完成後,創建的任務就可以在系統中競爭一定的資源,由內核進行調度。 就緒(Ready):該任務在就緒列表中,就緒的任務已經具備執行的能力,只等待調度器進行調度,新創建的任務會初始化為就緒態。 

 運行(Running):該狀態表明任務正在執行,此時它佔用處理器,調度器選擇運行的永遠是處於最高優先級的就緒態任務。 

 阻塞(Blocked):任務當前正在等待某個事件,比如信號量或外部中斷。 

 掛起態(Suspended):處於掛起態的任務對調度器而言是不可見的。

掛起態與阻塞態的區別,當任務有較長的時間不允許運行的時候,我們可以掛起任務,這樣子調度器就不會管這個任務的任何信息,直到調用恢復任務的 接口;而任務處於阻塞態的時候,系統還需要判斷阻塞態的任務是否超時,是否可以解除阻塞。

各任務運行時使用消息、信號量等方式進行通信,不能是全局變量。任務通常會運行在一個死循環中,不會退出,如果不再需要,可以調用刪除任務。

2.4 臨界區

臨界區就是一段在執行的時候不能被中斷的代碼段。在多任務操作系統裏面,對全局變量的操作不能被打斷,不能執行到一半就被其他任務再次操作。一般被打斷,原因就是系統調度或外部中斷。對臨界區的保護控制,歸根到底就是對系統中斷的使能控制。在使用臨界區時,關閉中斷響應,對部分優先級的中斷進行屏蔽,因此臨界區不允許運行時間過長。為了對臨界區進行控制,就需要使用信號量通信,實現同步或互斥操作。

三、 初識 FreeRTOS

3.1 FreeRTOS源碼

FreeRTOS 由美國的 Richard Barry 於 2003 年發佈, 2018 年被亞馬遜收購,改名為 AWS FreeRTOS,版本號升級為 V10,支持MIT開源協議,亞馬遜收購 FreeRTOS 也是為了進入物聯網和人工智能,新版本增加了物聯網行業的網絡協議等功能。

FreeRTOS 是開源免費的,可從官網 www.freertos.org 下載源碼和説明手冊。例如展鋭的UIS8910使用的是V10。以FreeRTOSv10.4.1為例,包含 Demo 例程,Source內核的源碼,License許可文件。

3.1.1 Source 文件夾

FreeRTOS/ Source 文件夾下的文件:

包括FreeRTOS 的通用的頭文件include和 C 文件,包括任務、隊列、定時器等,適用於各種編譯器和處理器,是通用的。

需要特殊處理適配的在portblle文件夾,其下內容與編譯器和處理器相關, FreeRTOS 要想運行在一個單片機上面,它們就必須關聯在一起,通常由彙編和 C 聯合編寫。通常難度比較高,不過一般芯片原廠提供移植好的接口文件。這裏不介紹移植的方法,因為自己也不明白。

Portblle/MemMang 文件夾下存放的是跟內存管理相關的,總共有五個 heap 文件,有5種內存動態分配方式,一般物聯網產品選用 heap4.c 。

3.1.2 Demo 文件夾

裏面包含了 FreeRTOS 官方為各個單片機移植好的工程代碼,FreeRTOS 為了推廣自己,會給針對不同半導體集運的評估板實現基礎功能範例, Demo下就是參考範例。

3.1.3 FreeRTOSConfig.h配置

FreeRTOSConfig.h頭文件對FreeRTOS 所需的功能的宏均做了定義,需要根據應用情況配置合適的參數,其作用類似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 )  

例如系統時鐘tick等參數在就這個文件配置,具體作用可以看註釋。一般情況下使用SDK不需要改動,特殊情況下諮詢原廠再調整。

3.2 FreeRTOS 編碼規範

接觸一個新平台或者SDK,明白它的編碼規範,文件作用,可以提高源碼閲讀效率,快速熟悉其內部實現。

3.2.1 數據類型

FreeRTOS針對不同的處理器,對標準C的數據類型進行了重定義。

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  

應用編碼中,推薦使用的是下面這種風格。

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 中,定義變量的時候往往會把變量的類型當作前綴,好處看到就知道其類型。 

char 型變量的前綴是 c 

short 型變量的前綴是 s 

long 型變量的前綴是 l 

複雜的結構體,句柄等定義的變量名的前綴是 x 

變量是無符號型的再加前綴 u,是指針變量則加前綴 p

3.2.3 函數名

函數名包含了函數返回值的類型、函數所在的文件名和函數的功能,如果是私有的函數則會加一個 prv(private)的前綴。 

例如vTaskPrioritySet()函數的返回值為 void 型,在 task.c 這個文件中定義。

3.2.4 宏

宏內容是由大寫字母表示,前綴是小寫字母,表示該宏在哪個頭文件定義,如:

1. #define taskYIELD()                 portYIELD()  

表示該宏是在task.h。

3.2.5 個人解讀

1、編碼不缺編碼規範,但是實際使用中很難完全依照標準執行,即使freeRTOS源碼也是如此。 

2、關於函數或者宏定義中帶文件名的作用,使用Source Insight 編輯代碼,該前綴的意義不大。 

3、規則是活的,只要所有人都按一個規則執行,它就是標準。

3.3 FreeRTOS應用開發

關於freeRTOS的應用開發,主要是任務的創建和調度,任務間的通信與同步,涉及隊列、信號量等操作系統通用接口。結合應用需求,涉及定時器、延時、中斷控制等接口。

特別説明,有些功能的實現方式有多種形式,只針對常用方式進行説明,例如task的創建,只説明動態創建方式,因為很少使用靜態方式。

四、 任務

4.1 創建任務

xTaskCreate()使用動態內存的方式創建一個任務。

1. ret = xTaskCreate((TaskFunction_t) master_task_main,  /* 任務入口函數 */(1)
2.                   “MASTER”,   /* 任務名字 */(2)
3.                   64*1024,   /* 任務棧大小 */(3)
4.                   NULL,    ,/* 任務入口函數參數 */(4)
5.                   TASK_PRIORITY_NORMAL,  /* 任務的優先級 */(5)
6.                   &task_master_handler);  /* 任務控制塊指針 */(6)

創建任務就是軟件運行時的一個while(1)的入口,一般閲讀其他代碼,找到這個函數,再跟蹤到任務入口函數,學習基於freeRTOS系統的代碼,首先就是找到main和這個接口。

(1):任務入口函數,即任務函數的名稱,需要我們自己定義並且實現。

 (2):任務名字,字符串形式,最大長度由 FreeRTOSConfig.h 中定義的 configMAX_TASK_NAME_LEN 宏指定,多餘部分會被自動截掉,只是方便調試。

(3):任務堆棧大小,單位為字, 4 個字節,這個要注意,否則系統內存緊缺。

(4):任務入口函數形參,不用的時候配置為 0 或者NULL 即可。


(5) :任務的優先級,在 FreeRTOS 中,數值越大優先級越高,0 代表最低優先級。基於其SDK開發,可將自定義的所有業務功能task設為同一個優先級,按時間片輪詢調度。

(6):任務控制塊指針,使用動態內存的時候,任務創建函數 xTaskCreate()會返回一個指針指向任務控制塊,也可以設為NULL,因為任務句柄後期可以不使用。

4.2 開啓調度

當任務創建成功後處於就緒狀態(Ready),在就緒態的任務可以參與操作系統的調度。操作系統任務調度器只啓動一次,之後就不會再次執行了,FreeRTOS 中啓動任務調度器的函數是 vTaskStartScheduler(),並且啓動任務調度器的時候就不會返回,從此任務管理都由FreeRTOS 管理,此時才是真正進入實時操作系統中的第一步。

vTaskStartScheduler開啓調度時,順便會創建空閒任務和定時器任務。

FreeRTOS 為了任務啓動和任務切換使用了三個異常:SVC、PendSV 和SysTick。

SVC(系統服務調用,亦簡稱系統調用)用於任務啓動。

PendSV(可掛起系統調用)用於完成任務切換,它是可以像普通的中斷一樣被掛起的,它的最大特性是如果當前有優先級比它高的中斷在運行,PendSV會延遲執行,直到高優先級中斷執行完畢,這樣產生的PendSV 中斷就不會打斷其他中斷的運行。

SysTick 用於產生系統節拍時鐘,提供一個時間片,如果多個任務共享同一個優先級,則每次 SysTick 中斷,下一個任務將獲得一個時間片。

FreeRTOS 中的任務是搶佔式調度機制,高優先級的任務可打斷低優先級任務,低優先級任務必須在高優先級任務阻塞或結束後才能得到調度。相同優先級的任務採用時間片輪轉方式進行調度(也就是分時調度),時間片輪轉調度僅在當前系統中無更高優先級就緒任務存在的情況下才有效。

4.3 啓動方式

FreeRTOS有兩種啓動方式,效果一樣,看個人喜好。

第一種:main 函數中將硬件初始化, RTOS 系統初始化,所有任務的創建完成,最後一步開啓調度。目前看到的幾個芯片SDK都是這種方式。

第二種:main 函數中將硬件和 RTOS 系統先初始化好,只創建一個任務後就啓動調度器,然後在這個任務裏面創建其它應用任務,當所有任務都創建成功後,啓動任務再把自己刪除。

4.4 任務創建源碼分析

xTaskCreate()創建任務。

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 ); //將新任務加入到就緒鏈表候着
61.         xReturn = pdPASS;  
62.     }  
63.     else  
64.     {  
65.         xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;  
66.     }  
67.   
68.     return xReturn;  
69. }

申請任務控制塊內存,檢查配置參數,初始化,將任務信息加入到就緒鏈表,等待調度。前面鏈表部分提到,freeRTOS的任務信息都是使用鏈表記錄,在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;   //阻塞

分別記錄就緒態、阻塞態和掛起的任務,其中阻塞態有2個,是因為特殊考慮,時間溢出 的問題,實際開發單片機項目計時超過24h的可以借鑑。其中pxReadyTasksLists鏈表數組,其下標就是任務的優先級。4.5 任務調度源碼分析

創建完任務的時候,vTaskStartScheduler開啓調度器,空閒任務、定時器任務也是在開啓調度函數中實現的。

為什麼要空閒任務?因為 FreeRTOS一旦啓動,就必須要保證系統中每時每刻都有一個任務處於運行態(Runing),並且空閒任務不可以被掛起與刪除,空閒任務的優先級是最低的,以便系統中其他任務能隨時搶佔空閒任務的 CPU 使用權。這些都是系統必要的東西,也無需自己實現。

1. void vTaskStartSchedulervoid )  
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.             /*創建空閒任務*/  
13.             xReturn = xTaskCreate( prvIdleTask,  
14.                                    configIDLE_TASK_NAME,  
15.                                    configMINIMAL_STACK_SIZE,  
16.                                    ( void * ) NULL,  
17.                                    portPRIVILEGE_BIT,  //優先級為0
18.                                    &xIdleTaskHandle );  
19.         }  
20.     #endif /* configSUPPORT_STATIC_ALLOCATION */  
21.   
22.     #if ( configUSE_TIMERS == 1 )  
23.         {  
24.             if( xReturn == pdPASS )  
25.             {  
26.                 //創建定時器task,接收開始、結束定時器等命令
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.             /* 系統開始運行 */  
68.         }  
69.         else  
70.         {  
71.             /* Should only reach here if a task calls xTaskEndScheduler(). */  
72.         }  
73.     }  
74.     else  
75.     {  
76.        /*****/
77. } 

4.6 任務狀態切換

FreeRTOS 系統中的每一個任務都有多種運行狀態,具體如下:

 任務掛起函數

vTaskSuspend()

掛起指定任務,被掛起的任務絕不會得到 CPU 的使用權

vTaskSuspendAll()

將所有的任務都掛起

 任務恢復函數

vTaskResume()
vTaskResume()
xTaskResumeFromISR()

任務恢復就是讓掛起的任務重新進入就緒狀態,恢復的任務會保留掛起前的狀態信息,在恢復的時候根據掛起時的狀態繼續運行。xTaskResumeFromISR() 專門用在中斷服務程序中。無論通過調用一次或多次vTaskSuspend()函數而被掛起的任務,也只需調用一次恢復即可解掛 。

 任務刪除函數 vTaskDelete()用於刪除任務。當一個任務可以刪除另外一個任務,形參為要刪除任 務創建時返回的任務句柄,如果是刪除自身, 則形參為 NULL。

4.7 任務使用注意點

1、中斷服務函數是不允許調用任何會阻塞運行的接口。一般在中斷服務函數中只做標記事件的發生,然後通知任務,讓對應任務去執行相關處理 。

2、將緊急的處理事件的任務優先級設置偏高一些。 

3、空閒任務(idle 任務)是 FreeRTOS 系統中沒有其他工作進行時自動進入的系統任務,永遠不會掛起空閒任務,不應該陷入死循環。

4、創建任務使用的內存不要過多,按需申請。如果浪費太多,後續應用申請大空間可能提示內存不足。

五、 隊列

5.1 隊列的概念

隊列用於任務間通信的數據結構,通過消息隊列服務,任務或中斷服務將消息放入消息隊列中。其他任務或者自身從消息隊列中獲得消息。實現隊列可以在任務與任務間、中斷和任務間傳遞信息。隊列操作支持阻塞等待,向已經填滿的隊列發送數據或者從空隊列讀出數據,都會導致阻塞,時間自定義。消息隊列的運作過程具如下:

5.2 隊列創建

xQueueCreate()用於創建一個新的隊列並返回可用於訪問這個隊列的句柄。隊列句柄其實就是一個指向隊列數據結構類型的指針。

1. master_queue = xQueueCreate(50sizeof(task_message_struct_t));  

創建隊列,佔用50個單元,每個單元為sizeof(task_message_struct_t)字節,和 malloc比較類似。其最終使用的函數是 xQueueGenericCreate(),後續信號量等也是使用它創建,只是最後的隊列類型不同。

申請內存後,xQueueGenericReset再對其進行初始化,隊列的結構體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.     //當前向隊列寫數據阻塞的任務列表或者從隊列取數阻塞的鏈表
13.     List_t xTasksWaitingToSend;  
14.     List_t xTasksWaitingToReceive;   
15.   
16.     //隊列裏有多少個單元被佔用,應用中需要
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 隊列刪除

隊列刪除函數 vQueueDelete()需傳入要刪除的消息隊列的句柄即可,刪除之後這個消息隊列的所有信息都會被系統回收清空,而且不能再次使用這個消息隊列了。實際應用中很少使用。

5.4 向隊列發送消息

任務或者中斷服務程序都可以給消息隊列發送消息,當發送消息時,如果隊列未滿或者允許覆蓋入隊,FreeRTOS 會將消息拷貝到消息隊列隊尾,否則,會根據用户指定的超時時間進行阻塞,消息發送接口很多,最簡單的是 xQueueSend(),用於向隊列尾部發送一個隊列消息。消息以拷貝的形式入隊,該函數絕對不能在中斷服務程序裏面被調用,中斷中必須使用帶有中斷保護功能的 xQueueSendFromISR()來代替。

BaseType_t xQueueSend(QueueHandle_t xQueue,const void* pvItemToQueue, TickType_t xTicksToWait);

用於向隊列尾部發送一個隊列消息。

參數

xQueue 隊列句柄

pvItemToQueue 指針,指向要發送到隊列尾部的隊列消息。 

xTicksToWait 隊列滿時,等待隊列空閒的最大超時時間。如果隊列滿並且xTicksToWait 被設置成 0,函數立刻返回。超時時間的單位為系統節拍週期 tick,延時為 portMAX_DELAY 將導致任務掛起(沒有超時)。 

返回值

消息發送成功成功返回 pdTRUE,否則返回 errQUEUE_FULL。

xQueueSendToBack與xQueueSend完全相同, xQueueSendFromISR()與 xQueueSendToBackFromISR(),帶FromISR表示只能在中斷中使用,freeRTOS所以帶這個後綴的都是這個含義。xQueueSendToFront()和QueueSendToFrontFromISR()用於向隊列隊首發送一個消息。這些在任務中發送消息的函數都是 xQueueGenericSend()展開的宏定義。

1. BaseType_t xQueueGenericSend( QueueHandle_t xQueue,   
2.                  const void * const pvItemToQueue,   
3.                          TickType_t xTicksToWait,   
4.                  const BaseType_t xCopyPosition )
  //發送數據到消息隊列的位置

一般使用xQueueSend和xQueueSendFromISR,如不確定當前運行的是系統服務,還是中斷服務,一般ARM都支持查詢中斷狀態寄存器判斷,可以封裝一層接口,只管發消息,內部判斷是否使用支持中斷嵌套的版本,UIS8910就是如此。特殊情況下,如發送網絡數據包未收到服務器響應,期望立刻入隊再次發送它,可以xQueueSendToFront向隊頭髮消息。

5.5 從隊列讀取消息

當任務試圖讀隊列中的消息時,可以指定一個阻塞超時時間,當且僅當消息隊列中有消息的時候,任務才能讀取到消息。如果隊列為空,該任務將保持阻塞狀態以等待隊列數據有效。當其它任務或中斷服務程序往其等待的隊列中寫入了數據,該任務將自動由阻塞態轉為就緒態。當任務等待的時間超過了指定的阻塞時間,即使隊列中尚無有效數據,任務也會自動從阻塞態轉移為就緒態。所有的task主入口while循環體都是按這個執行。例如:

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()用於從一個隊列中接收消息並把消息從隊列中刪除。如果不想刪除消息的話,就調用 xQueuePeek()函數。xQueueReceiveFromISR()與xQueuePeekFromISR()是中斷版本,用於在中斷服務程序中接收一個隊列消息並把消息。這兩個函數只能用於中斷,是不帶有阻塞機制的,實際項目沒有使用。

5.6 查詢隊列使用情況

uxQueueMessagesWaiting()查詢隊列中存儲的信息數目,具有中斷保護的版本為uxQueueMessagesWaitingFromISR()。查詢隊列的空閒數目uxQueueSpacesAvailable()。

5.7 隊列使用注意點

使用隊列函數需要注意以下幾點:

1、中斷中必須使用帶FromISR後綴的接口; 

2、發送或者是接收消息都是以拷貝的方式進行,如果消息內容過於龐大,可以將消息的地址作為消息進行發送、接收。

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;  //大數據使用動態申請內存保存,隊列只傳遞指針  
12. } track_task_message_struct_t;   

3、隊列並不屬於任何任務,所有任務都可以向同一隊列寫入和讀出,一個隊列可以由多任務或中斷讀寫。 

4、隊列的深度要結合實際,可以多申請點,前提是每個隊列單元儘可能小。 

5、隊列存在一定限制,在隊頭沒有取出來之前,是無法取出第二個,和STL鏈表存在差異。

六、 軟件定時器

6.1 軟件定時器的概念

定時器有硬件定時器和軟件定時器之分,硬件定時器是芯片本身提供的定時功能精度高,並且是中斷觸發方式。軟件定時器是由操作系統封裝的接口,它構建在硬件定時器基礎之上,使系統能夠提供不受硬件定時器資源限制,其實現的功能與硬件定時器也是類似的。

在操作系統中,通常軟件定時器以系統節拍週期為計時單位。系統節拍配置為configTICK_RATE_HZ,該宏在 FreeRTOSConfig.h 中,一般是100或者1000。根據實際系統 CPU 的處理能力和實時性需求設置合適的數值,系統節拍週期的值越小,精度越高,但是系統開銷也將越大,因為這代表在 1 秒中系統進入時鐘中斷的次數也就越多。

6.2 軟件定時器創建

軟件定時器需先創建才允許使用,動態創建方式是xTimerCreate(),返回一個句柄。軟件定時器在創建成功後是處於休眠狀態的,沒有開始計時運行。FreeRTOS的軟件定時器支持單次模式和週期模式。

單次模式:當用户創建了定時器並啓動了定時器後,定時時間到了,只執行一次回調函數,之後不再執行。週期模式:定時器會按照設置的定時時間循環執行回調函數,直到用户將定時器停止或刪除。

實際項目中使用這種模式對單片機餵狗就比較省事。

1. TimerHandle_t xTimerCreateconst char * const pcTimerName, //定時器名稱
2.                             const TickType_t xTimerPeriodInTicks,  //定時時間
3.                             const UBaseType_t uxAutoReload,  //是否自動重載
4.                             void * const pvTimerID,  //回調函數的參數
5.                             TimerCallbackFunction_t pxCallbackFunction )
  //回調函數

6.3 軟件定時器開啓

新創建的定時器沒有開始計時啓動,可以使用

xTimerStart()、
xTimerReset()、
xTimerStartFromISR() 、xTimerResetFromISR() 
xTimerChangePeriod()、xTimerChangePeriodFromISR()

這些函數將其狀態轉換為活躍態,開始運行。區別:如果定時器設定60秒間隔,已經運行了30秒,reset是將定時器重置為原來設定的時間間隔,也就是重新開始延時60秒。ChangePeriod重新設置計時週期。

6.4 軟件定時器停止

xTimerStop() 用於停止一個已經啓動的軟件定時器,xTimerStopFromISR()是中斷版本。

6.5 軟件定時器刪除

xTimerDelete()用於刪除一個已經被創建成功的軟件定時器,釋放資源,刪除之後不能再使用。實際項目中,任務和隊列都是按需創建,一直使用,但是定時器不使用的就應該刪除,並且刪除後一定要將句柄置為NULL。

6.6 軟件定時器源碼分析

軟件定時器任務是在系統開始調度的時候就被創建:vTaskStartScheduler()—xTimerCreateTimerTask。

1. BaseType_t xTimerCreateTimerTaskvoid )  
2. 
{  
3.     BaseType_t xReturn = pdFAIL;  
4.   
5.     prvCheckForValidListAndQueue();  //創建定時器任務的隊列
6.   
7.     if( xTimerQueue != NULL )  
8.     {  
9.         #if ( configSUPPORT_STATIC_ALLOCATION == 1 )  
10.             {  
11.                       /**/
12.             }  
13.         #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */  
14.             {  
15.                  //創建定時器任務
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. }  

任務創建後,等候命令執行

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向隊列發送控制命令,命令如下:

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、查看其他開源代碼,對定時器的使用並不多,但實際項目中過多依賴定時器,導致應用邏輯混亂。 

2、freeRTOS 的定時器不是無限制的,其根源是接收定時器控制命令消息的隊列,默認只有10個單元。

1. xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );  

定時器過多,可能出現發起定時器命令失敗,原因是隊列已滿。可以將默認的10擴大為15,後續儘量使用信號量來優化代碼。 

4、軟件定時器的回調函數要快進快出,而且不能有任何阻塞任務運行的情況,不能有vTaskDelay() 以及其它能阻塞任務運行的函數。特別説明,其回調函數是在定時器任務執行的,並不是開啓定時器的任務。

七、 信號量

7.1 信號量的概念

信號量(Semaphore)是一種實現任務間通信的機制,可以實現任務之間同步或臨界資源的互斥訪問,常用於協助一組相互競爭的任務來訪問臨界資源。在多任務系統中,各任務之間需要同步或互斥實現臨界資源的保護,信號量功能可以為用户提供這方面的支持。可以簡單認為是為支持多任務同時操作的全局變量(個人理解)。

7.1.1 二值信號量

比如有一個停車位,多個人都想佔用停車,這種情況就可以使用一個變量標記車位狀態,它只有兩種情況,被佔用或者沒被佔用。在多任務中使用二值信號量表示,用於任務與任務、任務與中斷的同步。在freeRTOS中,二值信號量看作只有一個消息的隊列,因此這個隊列只能為空或滿。

7.1.2 計數信號量

如果有100個停車位,可以停100輛車,每進去一輛車,車位的數量就要減一,當停車場停滿了 100 輛車的時候,再來的車就不能停進去了。這種場景就需要計數信號量來表示多個狀態。二進制信號量可以被認為是長度為 1 的隊列,而計數信號量則可以被認為長度大於 1 的隊列,信號量使用者依然不必關心存儲在隊列中的消息,只需關心隊列是否有消息即可。

7.1.3 互斥信號量

還是前面車位問題,只剩一個空車位,雖然員工車離得近,但是領導車來了,要優先安排給領導使用,這就是由地位決定。互斥信號量其實是特殊的二值信號量,由於其特有的優先級繼承機制從而使它更適用於簡單互鎖,也就是保護臨界資源。

優先級翻轉問題:假設有任務H,任務M和任務L三個任務,優先級逐次降低。低優先級的任務L搶先佔有資源,導致高優先級的任務H阻塞等待,此時再有中等優先級的任務M,它不需要該資源,且優先級高於任務L,它優先執行;之後再執行任務L,最後才執行任務H。看起來就是高優先級的任務反而不如低優先級的任務,即優先級翻轉。

改進型的互斥信號量具有優先級繼承機制,操作系統對獲取到臨界資源的任務提高其優先級為所有等待該資源的任務中的最高優先級。一旦任務釋放了該資源,就恢復到原來的優先級。

任務L先佔用資源,任務H申請不到資源會進入阻塞態,同時系統就會把當前正在使用資源的任務L的優先級臨時提高到與任務H優先級相同,即使任務M被喚醒了,因為它的優先級比任務H低,所以無法打斷任務L,因為任務L的優先級被臨時提升到 H;任務L使用完該資源,任務H優先級最高,將接着搶佔 CPU 的使用權,這樣保證任務H在任務M前優先執行。

上面的這些就是為了説明,二值信號量因為優先級翻轉,不能用於對臨界區的訪問。

7.1.4 遞歸互斥信號量

信號量是每獲取一次,可用信號量個數就會減少一個,釋放一次就增加一個。但是遞歸信號量則不同。對於已經獲取遞歸互斥量的任務可以重複獲取該遞歸互斥量,該任務擁有遞歸信號量的所有權。任務成功獲取幾次遞歸互斥量,就要返還幾次,在此之前遞歸互斥量都處於無效狀態,其他任務無法獲取,只有持有遞歸信號量的任務才能獲取與釋放。類似棧的效果。

7.2 二值信號量的應用

二值信號量是任務與任務間、任務與中斷間同步的重要手段。例如,任務A使用串口發出AT數據後,獲取二值信號量無效進入阻塞;

某個時間後,任務B中串口收到正確的回覆,釋放二值信號量。

任務A就立即從阻塞態中解除,進入就緒態,等待運行。這種機制用在模塊AT交互很合適。

7.3 計數信號量的應用

計數信號量可以用於資源管理,允許多個任務獲取信號量訪問共享資源。例如有公共資源車位3個,但是有多個任務要使用,這種場景就必須使用計數信號量。三個資源最多支持 3 個任務訪問,那麼第 4 個任務訪問的時候,會因為獲取不到信號量而進入阻塞。也就是第4個人無法佔用車位,必須前面有車離開。等到其中一個有任務(比如任務 1) 釋放掉該資源的時候,第 4 個任務才能獲取到信號量從而進行資源的訪問。其運作的機制類似下圖。

在這裏插入圖片描述

7.4 互斥信號量的應用

多任務環境下往往存在多個任務競爭同一臨界資源的應用場景,互斥量可被用於對臨界資源的保護從而實現獨佔式訪問。互斥量可以降低信號量存在的優先級翻轉問題帶來的影響。

比如有兩個任務需要對串口進行發送數據,其硬件資源只有一個,那麼兩個任務肯定不能同時發送,不然導致數據錯誤,那麼就可以用互斥量對串口資源進行保護,當一個任務正在使用串口的時候,另一個任務則無法使用串口,等到前一個任務使用串口完成後, 另外一個任務才能獲得串口的使用權。

另外需要注意的是互斥量不能在中斷服務函數中使用,因為其特有的優先級繼承機制只在任務起作用,在中斷的上下文環境毫無意義。

互斥信號量可以在多個任務之間進行資源保護,而臨界區只能是在同一個任務進行,但是其速度快。(個人理解)

7.5 信號量接口

所有信號量semaphore使用套路相近,都是創建creat、刪除delete、釋放give和獲取take四種;釋放和獲取支持任務級和中斷級FromISR,其中互斥量和遞歸互斥量不支持中斷。使用對應的信號量,需要在FreeRTOSConfig.h開啓對應的功能。

7.5.1 信號量創建

xSemaphoreCreateBinary()用於創建一個二值信號量,並返回一個句柄,默認二值信號量為空,在使用函數 xSemaphoreTake()獲取之前必須 先 調 用 函 數 xSemaphoreGive() 釋放後才可以獲取。

xSemaphoreCreateCounting()創建計數信號量。

1. #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )   

uxMaxCount 計數信號量的最大值,當達到這個值的時候,信號量不能再被釋放。uxInitialCount 創建計數信號量的初始值。

xSemaphoreCreateMutex()用於創建一個互斥量,並返回一個互斥量句柄,只能被同一個任務獲取一次,如果同一個任務想再次獲取則會失敗。

xSemaphoreCreateRecursiveMutex()用於創建一個遞歸互斥量,遞歸信號量可以被同一個任務獲取很多次,獲取多少次就需要釋放多少次。遞歸信號量與互斥量一樣,都實現了優先級繼承機制,可以減少優先級反轉的反生。

7.5.2 信號量刪除

vSemaphoreDelete()用於刪除一個信號量,包括二值信號量,計數信號量,互斥量和遞 歸互斥量。如果有任務阻塞在該信號量上,暫時不要刪除該信號量。傳入的參數為創建時返回的句柄。

7.5.3 信號量釋放

當信號量有效的時候,任務才能獲取信號量,信號量變得有效就是釋放信號量。每調用一次該函數就釋放一個信號量,注意釋放的次數,尤其是計數信號量。

xSemaphoreGive()是任務中釋放信號量的宏,可以用於二值信號量、計數信號量、互斥量的釋放,但不能釋放由函數xSemaphoreCreateRecursiveMutex()創建的遞歸互斥量,遞歸互斥信號量用xSemaphoreGiveRecursive()釋放。xSemaphoreGiveFromISR()帶中斷保護釋放一個信號量,被釋放的信號量可以是二值信號量和計數信號量,不能釋放互斥量和遞歸互斥量,因為互斥量和遞歸互斥量不可在中斷中使用,互斥量的優先級繼承機制只能在任務中起作用。

7.5.4 信號量獲取

與釋放信號量對應的是獲取信號量,當信號量有效的時候,任務才能獲取信號量,當任務獲取了某個信號量的時候,該信號量的可用個數就減一,當它減到0 的時候,任務就無法再獲取了,並且獲取的任務會進入阻塞態(如果設定了阻塞超時時間)。

xSemaphoreTake()函數用於獲取信號量,不帶中斷保護。獲取的信號量對象可以是二值信號量、計數信號量和互斥量,但是遞歸互斥量並不能使用它。

1. #define xSemaphoreTake( xSemaphore, xBlockTime )  

xSemaphore 信號量句柄 

xBlockTime 等待信號量可用的最大超時時間,單位為 tick 

獲取 成 功 則 返 回 pdTRUE ,在 指定的 超時 時間 中 沒 有 獲 取 成 功 則 返 回errQUEUE_EMPTY。

使用xSemaphoreTakeRecursive()獲取遞歸互斥量。xSemaphoreTakeFromISR()是獲取信號量的中斷版本,是一個不帶阻塞機制獲取信號量的函數,獲取對象必須由是已經創建的信號量,信號量類型可以是二值信號量和計數信號量,它與 xSemaphoreTake()函數不同,它不能用於獲取互斥量,因為互斥量不可以在中斷中使用,並且互斥量特有的優先級繼承機制只能在任務中起作用,而在中斷中毫無意義。

7.6 信號量使用注意點

1、建議合理使用信號量進行事件同步處理,減少對定時器的依賴。

2、使用前合理設定超時時間和依賴關係,避免多個任務互相等待對方釋放的信號量而死鎖。

八、 事件

8.1 事件的概念

信號量用於單個任務與任務或任務與中斷之間的同步,但有些任務可能與多個任務由關聯,此時信號量實現就比較麻煩,可以使用事件機制。

事件是一種實現任務間通信的機制,多任務環境下,任務、中斷之間往往需要同步操作,一個事件發生會告知等待中的任務,即形成一個任務與任務、中斷與任務間的同步。事件可以提供一對多、多對多的同步操作。一對多同步模型:一個任務等待多個事件的觸發,這種情況是比較常見的。

任務可以通過設置事件位來實現事件的觸發和等待操作。FreeRTOS 的事件僅用於同步,不提供數據傳輸功能。

8.2 事件的應用

在某些場合,可能需要多個事件發生了才能進行下一步操作。各個事件可分別發送或一起操作事件標誌組,而任務可以等待多個事件,任務僅對感興趣的事件進行關注。當有感興趣的事件發生時並且符合感興趣的條件,任務將被喚醒並進行後續的處理動作。

其機制類似一個全局變量,子任務使用特殊的接口函數對指定的位進行寫1或者清零,主任務阻塞等待該變量滿足設定的規則,則返回運行。

例如項目中的餵狗機制,多個任務,只要有一個任務發生異常,則主任務停止餵狗,等待被重啓。不使用事件機制,則3個任務定時向主master task發送消息,表明自身任務運行正常;同時master task定時查詢,是否收到3個任務的消息,如果全都收到表示正常,清除進入下一個定時檢查週期;如果其中一個未收到則表示對應任務異常,故意停止餵狗等待被重啓。

使用事件機制,則相對容易,3個任務定時設置對應的標誌位,master task只需要等待指定的事件位,超時就表示異常;不需要自身定時查詢,也省去了定時發消息。當然缺點是master task只能阻塞等待事件不能執行其他業務邏輯。

8.3 事件接口

xEventGroupCreate()用於創建一個事件組,vEventGroupDelete()刪除事件對象控制塊來釋放系統資源。

事件組置位,任務中使用 xEventGroupSetBits(),中斷中使用xEventGroupSetBitsFromISR();

xEventGroup 事件句柄。uxBitsToSet 指定事件中的事件標誌位。如設置 uxBitsToSet 為 0x09 則位 3和位 0 都需要被置位。返回調用 xEventGroupSetBits() 時事件組中的值。

事件組清除位,任務中使用xEventGroupClearBits(),中斷中使用 xEventGroupClearBitsFromISR(),都是用於清除事件組指定的位,如果在獲取事件的時候沒有將對應的標誌位清除,那麼就需要用這個函數來進行顯式清除。

xEventGroup 事件句柄。uxBitsToClear 指定事件組中的哪個位需要清除。如設置 uxBitsToSet 為 0x09則位 3和位 0 都需要被清除。

讀取事件標誌,任務中使用 xEventGroupGetBits(),中斷中使用xEventGroupGetBitsFromISR()。

重點是等待事件函數 xEventGroupWaitBits(),獲取任務感興趣的事件且支持等待超時機制,當且僅當任務等待的事件發生時,任務才能獲取到事件信息。否則任務將保持阻塞狀態以等待事件發生。當其它任務或中斷服務程序往其等待的事件設置對應的標誌位,該任務將自動由阻塞態轉為就緒態。

EventGroupWaitBits()用於獲取事件組中的一個或多個事件發生標誌,當要讀取的事件標誌位沒有被置位時,任務將進入阻塞等待狀態。要想使用該函數必 須 把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 )
  

參數

xEventGroup 事件句柄。 

  uxBitsToWaitFor 一個按位或的值,指定需要等待事件組中的哪些位置1。如需要等待 bits 0 and/or bit 1 and/or bit 2則 uxBitsToWaitFor 配置為 0x07(0111b)。

xClearOnExit pdTRUE:xEventGroupWaitBits() 等待到滿足任務喚醒的事件時,系統將清除由形參 uxBitsToWaitFor 指定的事件標誌位。pdFALSE:不會清除由形參 uxBitsToWaitFor 指定的事件標誌位。

xWaitForAllBits pdTRUE :當形參 uxBitsToWaitFor 指定的位都置位的時候,xEventGroupWaitBits()才滿足任務喚醒的條件,這也是“邏輯與”等待事件,並且在沒有超時的情況下返回對應的事件標誌位的值。pdFALSE:當形參 uxBitsToWaitFor 指定的位有其中任意一個置位的時候,這也是常説的“邏輯或”等待事件,在沒有超時的情況下 函數返回對應的事件標誌位的值。xTicksToWait 最大超時時間,單位為系統節拍週期

返回值

返回事件中的哪些事件標誌位被置位,返回值很可能並不是用户指定的事件位,需要對返回值進行 判斷再處理 。

其應用類似某個全局變量,等待事件的任務在設定的時間內,監控該變量某些位的值;該值由其他任務或中斷修改。

九、 任務通知

FreeRTOS 從 V8.2.0 版本開始提供任務通知這個功能,可以在一定場合下替代 FreeRTOS 的信號量,隊列、事件組等,但是使用也有侷限性。將宏定義 configUSE_TASK_NOTIFICATIONS 設置為 1才能開啓開功能。但該功能並不常用。

十、 內存管理

10.1 內存管理的概念

FreeRTOS 內存管理模塊管理用於系統中內存資源,它是操作系統的核心模塊之一。主要包括內存的初始化、分配以及釋放。一般不同的平台移植代碼,內存的動態申請和釋放接口需要替換。嵌入式實時操作系統中,一般不支持標準C庫中的 malloc()和 free(),其內存有限,隨着內存不斷被分配和釋放,整個系統內存區域會產生越來越多的碎片。

FreeRTOS提供了 5 種內存管理算法,源文件在Source\portable\MemMang 路徑下,使用的時候選擇其中一個。heap_1.c、heap_2.c 和 heap_4.c 這三種內存管理方案,內存堆實際上是一個很大的 數 組ucHeap。

heap_1.c內存管理方案簡單,它只能申請內存而不能進行內存釋放。有些嵌入式系統並不會經常動態申請與釋放內存,一般都是在系統啓動後就一直使用下去,永不刪除,適合這種方式。

heap_2.c 方案支持釋放申請的內存,但是它不能把相鄰的兩個小的內存塊合成一個大的內存塊,對於每次申請內存大小都比較固定的;但每次申請並不是固定內存大小的則會造成內存碎片。如下圖,隨着不斷的申請釋放,空閒空間會變成很多小片段。

heap_3.c 方案只是封裝了標準 C 庫中的 malloc()和 free()函數,由編譯器提供,需要通過編譯器或者啓動文件設置堆空間。

heap_4.c 方案是在heap_2.c 基礎上,對內存碎片進行了改進,能把相鄰的空閒的內存塊合併成一個更大的塊,這樣可以減少內存碎片。

heap_5.c 方案在實現動態內存分配時與 heap4.c 方案一樣,採用最佳匹配算法和合並算法,並且允許內存堆跨越多個非連續的內存區,也就是允許在不連續的內存堆中實現內存分配,比如做圖形顯示,可能芯片內部的 RAM 不足,額外擴展SDRAM,那這種內存管理方案則比較合適。

一般物聯網平台使用的是heap_4.c。

10.2 內存管理接口

不管其內部的管理如何實現的,對上層應用層的接口都是一樣的。

1. void *pvPortMallocsize_t xSize )//內存申請函數   
2. void vPortFreevoid *pv );          //內存釋放函數   
3. void vPortInitialiseBlocksvoid )//初始化內存堆函數   
4. size_t xPortGetFreeHeapSizevoid );    //獲取當前未分配的內存堆大小   
5. size_t xPortGetMinimumEverFreeHeapSizevoid )//獲取未分配的內存堆歷史最小值  

一般主要是使用內存申請和釋放兩個接口,用法和注意事項同malloc/free一樣,成對使用。內存釋放後儘量將指針設為NULL。

十一、 通用接口

一些常用接口進行説明。

11.1 臨界段

進入和退出臨界段的宏在 task.h 中定義,進入和退出臨界段的宏分中斷保護版本和非中斷版本,但最終都是通過開/關中斷來實現。主要用於對全局變量的控制,系統使用非常多,但實際項目中沒使用,因為全局變量的異常訪問時小概率問題,只是測試沒發現,理論上是存在問題的。

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 任務阻塞延時

vTaskDelay ()阻塞延時,任務調用該延時函數後會被剝離 CPU 使用權,進入阻塞狀態,直到延時結束。但是該函數不能用在中斷服務和定時回調函數。延時單位是tick。

11.3 獲取系統時鐘計數值

1. TickType_t xTaskGetTickCountvoid )  
2. TickType_t xTaskGetTickCountFromISRvoid )  

注意該接口分任務版和中斷版,該接口獲取的是tick計數值,需要結合系統時鐘頻率轉換成時間。

11.4 中斷回調函數

和其它平台不同,中斷回調中釋放中斷標記即可,freeRTOS中,中斷觸發後,可能某些阻塞的任務獲取了相關信號,需要立刻執行,因此中斷服務發送消息後,需要主動查詢阻塞任務的情況,執行任務切換動作。

1. static uint32_t ulExampleInterruptHandlervoid )  
2. 
{  
3.     BaseType_t xHigherPriorityTaskWoken;  
4. 
5.     xQueueSendToBackFromISR (xQueueRx,&cChar,&xHigherPriorityTaskWoken);  
6.     portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  
7. }  


免責聲明:本文內容由21ic獲得授權後發佈,版權歸原作者所有,本平台僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平台立場,如有問題,請聯繫我們,謝謝!

換一批

延伸閲讀

[新聞速遞] AWS發佈新一代Amazon Aurora Serverless

一個使SQL Server遷移到Amazon Aurora更方便的新功能,和一個幫助更多組織從SQL Server遷移到PostgreSQL的開源項目。 (1)Aurora Serverless v2可在不到一秒內擴展至支持數十萬...

關鍵字: 計算機 嵌入式

[大魚機器人] 知乎熱議:嵌入式開發中C++好用嗎?

觀點一 先説結論: 嵌入式、單片機裏面C++非常好使; C with class用來作大部分開發是完全可以勝任,如果用的好,能明顯改善你的代碼質量(嵌入式領域,個...

關鍵字: C語言 嵌入式

[程序喵大人] 新建一個空文件會佔用磁盤空間嗎?

今天我們來思考一個簡單的問題。在Linux下你用touch命令新建一個空文件: touch empty_file.txt 操作完成後,是否要消耗掉我們的一些磁盤空間?...

關鍵字: Linux 嵌入式

[程序喵大人] 愛了愛了,這篇寄存器講的有點意思

點擊藍色“程序員cxuan ”關注我喲 加個“星標”,歡迎來撩...

關鍵字: 寄存器 嵌入式

[大魚機器人] C語言指針,這可能是史上最幹最全的講解啦(附代碼)

點擊上方“大魚機器人”,選擇“置頂/星標公眾號” 福利乾貨,第一時間送達! 指針對於C來説太重要。然而,想要全面理解指針,除了要對C語言有熟...

關鍵字: C語言 嵌入式

技術子站

關閉