以下轉載自安富萊電子: http://forum.armfly.com/forum.php
為什么要使用事件標志
事件標志組是實現多任務同步的有效機制之一。也許有不理解的初學者會問采用事件標志組多麻煩,
搞個全局變量不是更簡單?其實不然,在裸機編程時,使用全局變量的確比較方便,但是在加上 RTOS 后
就是另一種情況了。 使用全局變量相比事件標志組主要有如下三個問題:
使用事件標志組可以讓 RTOS 內核有效地管理任務,而全局變量是無法做到的,任務的超時等機制需
要用戶自己去實現。
使用了全局變量就要防止多任務的訪問沖突,而使用事件標志組則處理好了這個問題,用戶無需擔心。
使用事件標志組可以有效地解決中斷服務程序和任務之間的同步問題。
FreeRTOS 任務間事件標志組的實現
任務間事件標志組的實現是指各個任務之間使用事件標志組實現任務的通信或者同步機制。
下面我們來說說 FreeRTOS 中事件標志的實現,根據用戶在 FreeRTOSConfig.h 文件中的配置:
#define configUSE_16_BIT_TICKS 1
配置宏定義 configUSE_16_BIT_TICKS 為 1 時,每創建一個事件標志組,用戶可以使用的事件標志是
8 個。
#define configUSE_16_BIT_TICKS 0
配置宏定義 configUSE_16_BIT_TICKS 為 0 時,每創建一個事件標志組,用戶可以使用的事件標志是24 個。
上面說的 8 個和 24 個事件標志應該怎么理解呢?其實就是定義了一個 16 位變量,僅使用了低 8bit
或者定義了一個 32 位變量,僅使用了低 24bit。每一個 bit 用 0 和 1 兩種狀態來代表事件標志。 反映到
FreeRTOS 上就是將事件標志存儲到了 EventBits_t 類型的變量中, 這個變量又是怎么回事呢?定義如下:
由上面定義可以看出,TickType_t 數據類型可以是 16 位數或者 32 位數,這樣就跟上面剛剛說的
configUSE_16_BIT_TICKS 宏定義呼應上了。教程配套的例子都是配置宏定義 configUSE_16_BIT_TICKS
為 0, 即用戶每創建一個事件標志組, 有 24 個標志可以設置。 如下圖所示,這里僅使用 bit0, bit1 和 bit2。
下面我們通過如下的框圖來說明一下 FreeRTOS 事件標志的實現,讓大家有一個形象的認識。
FreeRTOS 中斷方式事件標志組的實現
FreeRTOS 中斷方式事件標志組的實現是指中斷函數和 FreeRTOS 任務之間使用事件標志。 下面我們
通過如下的框圖來說明一下 FreeRTOS 事件標志的實現,讓大家有一個形象的認識。
任務 Task1 運行過程中調用函數 xEventGroupWaitBits,等待事件標志位被設置,任務 Task1 由運
行態進入到阻塞態。
Task1 阻塞的情況下,串口接收到數據進入到了串口中斷服務程序,在串口中斷服務程序中設置 Task1
等待的事件標志,任務 Task1 由阻塞態進入到就緒態,在調度器的作用下由就緒態又進入到運行態。
上面就是一個簡單的 FreeRTOS 中斷方式事件標志通信過程。 實際應用中,中斷方式的消息機制要注意以
下四個問題:
中斷函數的執行時間越短越好,防止其它低於這個中斷優先級的異常不能得到及時響應。
實際應用中,建議不要在中斷中實現消息處理,用戶可以在中斷服務程序里面發送消息通知任務,在
任務中實現消息處理,這樣可以有效地保證中斷服務程序的實時響應。同時此任務也需要設置為高優
先級,以便退出中斷函數后任務可以得到及時執行。
中斷服務程序中一定要調用專用於中斷的事件標志設置函數,即以 FromISR 結尾的函數。
在操作系統中實現中斷服務程序與裸機編程的區別。
如果 FreeRTOS 工程的中斷函數中沒有調用 FreeRTOS 的事件標志組 API 函數,與裸機編程是
一樣的。
如果 FreeRTOS 工程的中斷函數中調用了 FreeRTOS 的事件標志組的 API 函數,退出的時候要
檢測是否有高優先級任務就緒,如果有就緒的,需要在退出中斷后進行任務切換,這點跟裸機編
程稍有區別,詳見 實驗例程說明(中斷方式):
另外強烈推薦用戶將 Cortex-M3 內核的 STM32F103 和 Cortex-M4 內核的 STM32F407, F429
的 NVIC 優先級分組設置為 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);這樣中
斷優先級的管理將非常方便。
用戶要在 FreeRTOS 多任務開啟前就設置好優先級分組,一旦設置好切記不可再修改。
事件標志組 API 函數
使用如下 11 個函數可以實現 FreeRTOS 的事件標志組:
xEventGroupCreate()
xEventGroupCreateStatic()
vEventGroupDelete()
xEventGroupWaitBits()
xEventGroupSetBits()
xEventGroupSetBitsFromISR()
xEventGroupClearBits()
xEventGroupClearBitsFromISR()
xEventGroupGetBits()
xEventGroupGetBitsFromISR()
這里我們重點的說以下 4 個函數:
xEventGroupCreate()
xEventGroupWaitBits()
xEventGroupSetBits()
xEventGroupSetBitsFromISR()
函數 xEventGroupCreate
函數原型:
EventGroupHandle_t xEventGroupCreate( void );
函數描述:
函數 xEventGroupCreate 用於創建事件標志組。
返回值,如果創建成功, 此函數返回事件標志組的句柄,如果 FreeRTOSConfig.h 文件中定義的 heap
空間不足會返回 NULL
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 30 * 1024 ) )
函數 xEventGroupSetBits
函數原型:
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, /* 事件標志組句柄 */
const EventBits_t uxBitsToSet ); /* 事件標志位設置 */
函數描述:
函數 xEventGroupSetBits 用於設置指定的事件標志位為 1。
第 1 個參數是事件標志組句柄。
第 2 個參數表示 24 個可設置的事件標志位,EventBits_t 是定義的 32 位變量,
低 24 位用於事件標志設置。變量 uxBitsToSet 的低 24 位的某個位設置為 1,那么被設置的
事件標志組的相應位就設置為 1。 變量 uxBitsToSet 設置為 0 的位對事件標志相應位沒有影響。比
如設置變量 uxBitsToSet = 0x0003 就表示將事件標志的位 0 和位 1 設置為 1,其余位沒有變化。
返回當前的事件標志組數值。
使用這個函數要注意以下問題:
1. 使用前一定要保證事件標志組已經通過函數 xEventGroupCreate 創建了。
2. 此函數是用於任務代碼中調用的,故不可以在中斷服務程序中調用此函數,中斷服務程序中使用的是
xEventGroupSetBitsFromISR
3. 用戶通過參數 uxBitsToSet 設置的標志位並不一定會保留到此函數的返回值中,下面舉兩種情況:
a. 如果設置一個位導致等待該位的任務離開阻塞(注意是離開阻塞態,即使沒有進入運行態,只要離開阻塞態即可)狀態,則該位可能會被自動清除(請參閱xEventGroupWaitBits()的xClearBitOnExit參數)。
b. 調用此函數的任務是一個低優先級任務,通過此函數設置了事件標志后,讓一個等待此事件標志
的高優先級任務就緒了,會立即切換到高優先級任務去執行,相應的事件標志位會被函數
xEventGroupWaitBits 清除掉,等從高優先級任務返回到低優先級任務后,函數xEventGroupSetBits 的返回值已經被修改。
函數 xEventGroupSetBitsFromISR
函數原型:
BaseType_t xEventGroupSetBitsFromISR(
EventGroupHandle_t xEventGroup, /* 事件標志組句柄 */
const EventBits_t uxBitsToSet, /* 事件標志位設置 */
BaseType_t *pxHigherPriorityTaskWoken ); /* 高優先級任務是否被喚醒的狀態保存 */
函數描述:
函數 xEventGroupSetBitsFromISR,用於設置指定的事件標志位為 1。(中斷方式)
第 1 個參數是事件標志組句柄。
第 2 個參數表示 24 個可設置的事件標志位,EventBits_t 是定義的 32 位變量(詳解 18.1.2 小節說
明),低 24 位用於事件標志設置。變量 uxBitsToSet 的低 24 位的某個位設置為 1,那么被設置的
事件標志組的相應位就設置為 1。 變量 uxBitsToSet 設置為 0 的位對事件標志相應位沒有影響。比
如設置變量 uxBitsToSet = 0x0003 就表示將事件標志的位 0 和位 1 設置為 1,其余位沒有變化。
第 3 個參數用於保存是否有高優先級任務准備就緒。如果函數執行完畢后,此參數的數值是 pdTRUE,
說明有高優先級任務要執行,否則沒有。
返回值,如果消息成功發送給 daemon 任務(就是 FreeRTOS 的定時器任務)返回 pdPASS,否則
返回 pdFAIL,另外 daemon 任務中的消息隊列滿了也會返回 pdFAIL。
使用這個函數要注意以下問題:
1. 使用前一定要保證事件標志已經通過函數 xEventGroupCreate 創建了。同時要在 FreeRTOSConfig.h
文件中使能如下三個宏定義:
#define INCLUDE_xEventGroupSetBitFromISR 1
#define configUSE_TIMERS 1
#define INCLUDE_xTimerPendFunctionCall 1
2. 函數 xEventGroupSetBitsFromISR 是用於中斷服務程序中調用的,故不可以在任務代碼中調用此函
數,任務代碼中使用的是 xEventGroupSetBits。
3. 函數 xEventGroupSetBitsFromISR 對事件標志組的操作是不確定性操作,因為不知道當前有多少個
任務在等待此事件標志。而 FreeRTOS 不允許在中斷服務程序和臨界段中執行不確定性操作。 為了不
在中斷服務程序中執行,就通過此函數給 FreeRTOS 的 daemon 任務(就是 FreeRTOS 的定時器任
務)發送消息,在 daemon 任務中執行事件標志的置位操作。 同時也為了不在臨界段中執行此不確定
操作,將臨界段改成由調度鎖來完成。這樣不確定性操作在中斷服務程序和臨界段中執行的問題就都
得到解決了。
4. 由於函數 xEventGroupSetBitsFromISR 對事件標志的置位操作是在 daemon 任務里面執行的,如果
想讓置位操作立即生效,即讓等此事件標志的任務能夠得到及時執行,需要設置 daemon 任務的優先
級高於使用此事件標志組的所有其它任務。
5. 通過下面的使用舉例重點看一下函數 xEventGroupSetBitsFromISR 第三個參數的規范用法,初學者務
必要注意。
函數 xEventGroupWaitBits
函數原型:
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup, /* 事件標志組句柄 */
const EventBits_t uxBitsToWaitFor, /* 等待被設置的事件標志位 */
const BaseType_t xClearOnExit, /* 選擇是否清零被置位的事件標志位 */
const BaseType_t xWaitForAllBits, /* 選擇是否等待所有標志位都被設置 */
TickType_t xTicksToWait ); /* 設置等待時間 */
函數描述:
函數 xEventGroupWaitBits 等待事件標志被設置。
第 1 個參數是事件標志組句柄。
第 2 個參數表示等待 24 個事件標志位中的指定標志,EventBits_t 是定義的 32 位變量(詳解 18.1.2
小節說明),低 24 位用於事件標志設置。比如設置變量 uxBitsToWaitFor = 0x0003 就表示等待事
件標志的位 0 和位 1 設置為 1。 此參數切不可設置為 0。
第 3 個參數選擇是否清除已經被置位的事件標志,如果這個參數設置為 pdTRUE,且函數
xEventGroupWaitBits 在參數 xTicksToWait 設置的溢出時間內返回,那么相應被設置的事件標志
位會被清零。 如果這個參數設置為 pdFALSE,對已經被設置的事件標志位沒有影響。
第 4 個參數選擇是否等待所有的標志位都被設置,如果這個參數設置為 pdTRUE,要等待第 2 個參
數 uxBitsToWaitFor 所指定的標志位全部被置 1,函數才可以返回。當然,超出了在參數
xTicksToWait 設置的溢出時間也是會返回的。如果這個參數設置為 pdFALSE,第 2 個參數
uxBitsToWaitFor 所指定的任何標志位被置 1,函數都會返回,超出溢出時間也會返回。
第 5 個參數設置等待時間,單位時鍾節拍周期。 如果設置為 portMAX_DELAY,表示永久等待。
返回值,由於設置的時間超時或者指定的事件標志位被置 1,導致函數退出時返回的事件標志組數值。
使用這個函數要注意以下問題:
1. 此函數切不可在中斷服務程序中調用。
2. 這里再着重說明下這個函數的返回值,通過返回值用戶可以檢測是哪個事件標志位被置 1 了。
如果由於設置的等待時間超時,函數的返回值可能會有部分事件標志位被置 1。
如果由於指定的事件標志位被置1而返回, 並且設置了這個函數的參數xClearOnExit為pdTRUE,
那么此函數的返回值是清零前的事件標志組數值。
另外,調用此函數的任務在離開阻塞狀態到退出函數 xEventGroupWaitBits 之間這段時間,如果一個
高優先級的任務搶占執行了,並且修改了事件標志位,那么此函數的返回值會跟當前的事件標志組數
值不同 。
實驗分析場:
static void AppTaskCreate(void) { xTaskCreate(vTaskWork, /* 任務函數 */ "vTaskWork", /* 任務名 */ 512, /* 任務棧大小,單位word,也就是4字節 */ NULL, /* 任務參數 */ 1, /* 任務優先級*/ &xHandleTaskWork ); /* 任務句柄 */ xTaskCreate( vTaskLed1, /* 任務函數 */ "vTaskLed1", /* 任務名 */ 512, /* 任務棧大小,單位word,也就是4字節 */ NULL, /* 任務參數 */ 2, /* 任務優先級*/ &xHandleTaskLED1); /* 任務句柄 */ xTaskCreate( vTaskBeep, /* 任務函數 */ "vTaskBeep", /* 任務名 */ 512, /* 任務棧大小,單位word,也就是4字節 */ NULL, /* 任務參數 */ 3, /* 任務優先級*/ &xHandleTaskBeep ); /* 任務句柄 */ }
/********************************************************************************* * @ 函數名 : vTaskBeep * @ 功能說明: Beep 任務 * @ 參數 : pvParameters,當任務創建的時候傳進來,可以沒有 * @ 返回值 : 無 ********************************************************************************/ void vTaskBeep(void *pvParameters) { EventBits_t uxBits; const TickType_t xTicksToWait = 5000; /* 最大延遲100ms */ while(1) { /* 等K1按鍵按下設置bit0和K2按鍵按下設置bit1 */ uxBits = xEventGroupWaitBits(xCreatedEventGroup, /* 事件標志組句柄 */ BIT_ALL, /* 等待bit0和bit1被設置 */ pdTRUE, /* 退出前bit0和bit1被清除,這里是bit0和bit1都被設置才表示“退出”*/ pdTRUE, /* 設置為pdTRUE表示等待bit1和bit0都被設置*/ xTicksToWait); /* 等待延遲時間 */ if((uxBits & BIT_ALL) == BIT_ALL) { /* 接收到bit1和bit0都被設置的消息 */ printf("接收到bit0和bit1都被設置的消息\r\n"); } else { /* 超時,另外注意僅接收到一個按鍵按下的消息時,變量uxBits的相應bit也是被設置的 */ BEEP_TOGGLE; } } }
/***按鍵處理任務***/ static void vTaskWork(void *pvParameters) { EventBits_t uxBits; while(1) { if (key1_flag==1) { key1_flag=0; /* 設置事件標志組的bit0 */ uxBits = xEventGroupSetBits(xCreatedEventGroup, BIT_0); if((uxBits & BIT_0) != 0) { printf("K1鍵按下,事件標志的bit0被設置\r\n"); } else { printf("K1鍵按下,事件標志的bit0被清除,說明任務vTaskBeep已經接受到bit0和bit1被設置的情況\r\n"); } } if(key2_flag==1) { key2_flag=0; uxBits = xEventGroupSetBits(xCreatedEventGroup, BIT_1); if((uxBits & BIT_1) != 0) { printf("K2鍵按下,事件標志的bit1被設置\r\n"); } else { printf("K2鍵按下,事件標志的bit1被清除,說明任務vTaskBeep已經接受到bit0和bit1被設置的情況\r\n"); } } vTaskDelay(20); } }
創建的任務,按鍵處理優先級低於事件等待的Beep任務,先按下K1,再按K2,打印如下:
第一個輸出毫無疑問,第二行,由於事件等待Beep優先級大於按鍵處理,所以當K2按下之后,調度器首先回到高優先級的任務Beep,打印出此時K1,K2都被按下以致bit0和bit1被置位的消息,在Beep任務中調用xEventGroupWaitBits函數后,這兩個置為1的位bit1和bit0會被清零,此時,調度器再次回到低優先級的按鍵處理任務時,xEventGroupSetBits的返回值已經更新成清零值,故第三行打印清除的消息。
現在,我們把按鍵處理的優先級設置成為高於Beep任務的,打印輸出如下:
第一個輸出也毫無疑問,按下K1,bit0被置位,當我按下K2的時候,此時調度器 不會馬上返回低優先級的Beep任務,而會繼續執行自身(此實驗設置按鍵處理最高優先級)直到被阻塞,所以會有第二行的打印,但是,注意,第二行按下K2的打印卻依舊顯示的是被清除了,因為在Beep任務中使用了事件等待,而K2按下的時候,freertos操作系統會知道等待兩個按鍵按下的事件已經觸發了,此時,在按鍵處理任務中,xEventGroupSetBits的返回值,也不是當前獲取的置位值了,而是經過xEventGroupSetBits函數自動清零之后的值,所以第二行打印的是清零消息,第三行打印都被置位,為什么不是清零?因為此時的xEventGroupWaitBits返回值是清零前的事件標志組數值。
可能你覺得有點奇怪, xEventGroupSetBits函數本就是置位信息的功能,居然還要受xEventGroupWaitBits函數和調用形式影響,哪怕調用xEventGroupWaitBits函數的任務優先級還是低於我們的按鍵任務的(但是事件標志的設置讓低優先級的任務離開了阻塞態(只要離開了阻塞態,返回值就會更新),在就緒態,只是被高優先級任務搶占了),但是,正是因為這樣,我們真正實時傳遞了事件信息啊。試想,要是我的兩個按鍵事件都已經觸發了,而我在按鍵處理任務中還不能立即知道,這樣的實時性顯然是不滿足需求的。就連裸機中,我們通過中斷改變一個元素的值,一定是中斷改變之后,這個值在被任何使用的時候都已經更新,所以,作為實時操作系統,freertos這樣的行為也就可以理解了。
中斷方式不再演示,只是需要在中斷服務函數中使用xEventGroupSetBitsFromISR,使用方式如下:
/* ********************************************************************************************************* * 函 數 名: TIM_CallBack1和TIM_CallBack2 * 功能說明: 定時器中斷的回調函數,此函數被bsp_StartHardTimer所調用。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void TIM_CallBack1(void) { BaseType_t xResult; BaseType_t xHigherPriorityTaskWoken = pdFALSE; /* 向任務vTaskBeep發送事件標志 */ xResult = xEventGroupSetBitsFromISR(xCreatedEventGroup, /* 事件標志組句柄 */ BIT_0 , /* 設置bit0 */ &xHigherPriorityTaskWoken ); /* 消息被成功發出 */ if( xResult != pdFAIL ) { /* 如果xHigherPriorityTaskWoken = pdTRUE,那么退出中斷后切到當前最高優先級任務執行 */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }