爲什麼要使用事件標誌
事件標誌組是實現多任務同步的有效機制之一。也許有不理解的初學者會問採用事件標誌組多麻煩,
搞個全局變量不是更簡單?其實不然,在裸機編程時,使用全局變量的確比較方便,但是在加上 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 個。
FreeRTOS 中斷方式事件標誌組的實現
FreeRTOS 中斷方式事件標誌組的實現是指中斷函數和 FreeRTOS 任務之間使用事件標誌。 下面我們通過如下的框圖來說明一下 FreeRTOS 事件標誌的實現,讓大家有一個形象的認識。
任務 Task1 運行過程中調用函數 xEventGroupWaitBits,等待事件標誌位被設置,任務 Task1 由運行態進入到阻塞態。
Task1 阻塞的情況下,串口接收到數據進入到了串口中斷服務程序,在串口中斷服務程序中設置 Task1等待的事件標誌,任務 Task1 由阻塞態進入到就緒態,在調度器的作用下由就緒態又進入到運行態。
上面就是一個簡單的 FreeRTOS 中斷方式事件標誌通信過程。 實際應用中,中斷方式的消息機制要注意以下四個問題:
中斷函數的執行時間越短越好,防止其它低於這個中斷優先級的異常不能得到及時響應。
實際應用中,建議不要在中斷中實現消息處理,用戶可以在中斷服務程序裏面發送消息通知任務,在任務中實現消息處理,這樣可以有效地保證中斷服務程序的實時響應。同時此任務也需要設置爲高優先級,以便退出中斷函數後任務可以得到及時執行。
中斷服務程序中一定要調用專用於中斷的事件標誌設置函數,即以 FromISR 結尾的函數。
在操作系統中實現中斷服務程序與裸機編程的區別。
如果 FreeRTOS 工程的中斷函數中沒有調用 FreeRTOS 的事件標誌組 API 函數,與裸機編程是一樣的。
如果 FreeRTOS 工程的中斷函數中調用了 FreeRTOS 的事件標誌組的 API 函數,退出的時候要檢測是否有高優先級任務就緒,如果有就緒的,需要在退出中斷後進行任務切換,這點跟裸機編程稍有區別
事件標誌組 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 位變量,低 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);
}
}