1. FreeRTOS 消息隊列
1.1 簡介
? 隊列是 任務間通信的主要形式,可用于在任務之間以及中斷與任務之間傳遞消息。隊列在 FreeRTOS 中具有以下關鍵特點:
- 隊列默認采用 先進先出 FIFO 方式,也可以使用
xQueueSendToFront()
實現 LIFO。 - FreeRTOS 確保隊列操作是原子的,不會因為任務切換而導致數據損壞。
- 如果隊列 滿了(寫入時)或 空了(讀取時),任務 可以選擇阻塞(等待消息) 或 非阻塞(立即返回)。
- 高優先級任務更快地執行隊列操作,如果多個任務在相同優先級,按照 時間片輪轉 處理隊列。
- 消息大小固定,但可以存儲指向可變數據的指針。
1.2 隊列相關 API 函數介紹
FreeRTOS 隊列管理 API 速覽
1. 隊列的創建 API
API 函數 | 描述 |
---|---|
xQueueCreate() | 動態創建隊列 |
xQueueCreateStatic() | 靜態創建隊列 |
2. 隊列消息的寫入 API
API 函數 | 描述 |
---|---|
xQueueSend() | 往隊列尾部寫入消息 |
xQueueSendToBack() | 同 xQueueSend() ,往隊列尾部寫入消息 |
xQueueSendToFront() | 往隊列頭部寫入消息 |
xQueueOverwrite() | 覆寫隊列消息(僅限隊列長度為 1) |
xQueueSendFromISR() | 在中斷中往隊列尾部寫入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() ,往隊列尾部寫入 |
xQueueSendToFrontFromISR() | 在中斷中往隊列頭部寫入消息 |
xQueueOverwriteFromISR() | 在中斷中覆寫隊列消息(僅限隊列長度為 1) |
3. 隊列消息的讀取 API
API 函數 | 描述 |
---|---|
xQueueReceive() | 從隊列頭部讀取消息,并刪除消息 |
xQueuePeek() | 從隊列頭部讀取消息(但不刪除) |
xQueueReceiveFromISR() | 在中斷中從隊列頭部讀取消息,并刪除消息 |
xQueuePeekFromISR() | 在中斷中從隊列頭部讀取消息(但不刪除) |
1.3 實驗
- start_task:用來創建其他的 3 個任務。
- task1:當按鍵 key1 或 key2 按下,將鍵值拷貝到隊列 queue1(入隊);當按鍵 key3 按下,將傳輸大數據,這里拷貝大數據的地址到隊列 big_queue 中。
- task2:讀取隊列 queue1 中的消息(出隊),打印出接收到的鍵值。
- task3:從隊列 big_queue 讀取大數據地址,通過地址訪問大數據。
1 ) 創建隊列程序:
QueueHandle_t queue1; /* 小數據句柄 */
QueueHandle_t big_queue; /* 大數據句柄 */
char buff[100] = {"dioajdiaj fdahjk324hjkhfjksdahjk#$@!@#jfaskdfhjka"};void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建 queue1 隊列 */queue1 = xQueueCreate(2, sizeof(uint8_t));if (queue1 != NULL){printf("queue1 隊列創建成功\r\n");}else{printf("queue1 隊列創建失敗\r\n");}/* 創建 big_queue 隊列 */big_queue = xQueueCreate(1, sizeof(char *));if (big_queue != NULL){printf("big_queue 隊列創建成功\r\n");}else{printf("big_queue 隊列創建失敗\r\n");}/* 創建三個任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{uint8_t key_value = 0;char *buf;BaseType_t err = 0;buf = &buff[0];char task_info[200];while (1){if (key[0].flag == 1 || key[1].flag == 1){key_value = key[0].flag;err = xQueueSendToBack(queue1, &key_value, portMAX_DELAY);if (err != pdTRUE){printf("queue1 隊列發送失敗\r\n");}key[0].flag = 0;key[1].flag = 0;}else if (key[2].flag == 1){err = xQueueSendToBack(big_queue, &buf, portMAX_DELAY);if (err != pdTRUE){printf("big_queue 隊列發送失敗\r\n");}key[2].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{uint8_t key = 0;BaseType_t err = 0;while (1){err = xQueueReceive(queue1, &key, portMAX_DELAY);if (err != pdTRUE){printf("queue1 隊列讀取失敗\r\n");}else{printf("queue1 讀取隊列成功,數據:%d\r\n", key);}}
}
4 ) task3:
void task3(void *pvParameters)
{char *buf;BaseType_t err = 0;while (1){for (int i = 0; i < 16; i++){if (key[i].flag == 1){err = xQueueReceive(big_queue, &buf, portMAX_DELAY);if (err != pdTRUE){printf("big_queue 隊列讀取失敗\r\n");}else{printf("data:%s\r\n", buf);}}}}
}
2. FreeRTOS 隊列集
? 隊列集(Queue Set)是 FreeRTOS 中的一種數據結構,用于管理多個隊列。它提供了一種有效的方式,通過單個 API 調用來操作和訪問一組相關的隊列。在多任務系統中,任務之間可能需要共享數據,而這些數據可能存儲在不同的隊列中。隊列集的作用就是為了更方便地管理這些相關隊列,使得任務能夠輕松地訪問和處理多個隊列的數據。
2.1 隊列集相關 API 函數介紹
函數 | 描述 |
---|---|
xQueueCreateSet() | 創建隊列集,用于管理多個隊列或信號量 |
xQueueAddToSet() | 向隊列集添加隊列或信號量 |
xQueueRemoveFromSet() | 從隊列集中移除隊列或信號量 |
xQueueSelectFromSet() | 獲取隊列集中有消息的隊列(阻塞) |
xQueueSelectFromSetFromISR() | 在中斷中獲取隊列集中有消息的隊列(非阻塞) |
2.2 實驗
- start_task:用來創建其他 2 個任務,并創建隊列集、倆個隊列,將這倆個隊列添加到隊列集中。
- task1:用于掃描按鍵,當 KEY1 按下,往隊列1中寫入數據,當 KEY2 按下,往隊列2中寫入數據。
- task2:讀取隊列集中的消息,并打印。
1 ) 創建隊列集
QueueHandle_t queue1, queue2;
QueueSetHandle_t queue_set;void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建 queue1 隊列 */queue1 = xQueueCreate(1, sizeof(uint8_t));if (queue1 != NULL){printf("queue1 隊列創建成功\r\n");}else{printf("queue1 隊列創建失敗\r\n");}/* 創建 queue2 隊列 */queue2 = xQueueCreate(1, sizeof(char *));if (queue2 != NULL){printf("queue2 隊列創建成功\r\n");}else{printf("queue2 隊列創建失敗\r\n");}/* 創建 queue2 隊列 */queue_set = xQueueCreateSet(2);if (queue_set != NULL){printf("queue_set 隊列創建成功\r\n");}else{printf("queue_set 隊列創建失敗\r\n");}xQueueAddToSet(queue1, queue_set);xQueueAddToSet(queue2, queue_set);/* 創建任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{char *buf;char text[20];BaseType_t err = 0;while (1){if (key[0].flag == 1){sprintf((char *)text, "queue1 數據");buf = &text[0];err = xQueueSendToBack(queue1, &buf, portMAX_DELAY);if (err != pdTRUE){printf("queue1 隊列寫入數據錯誤\r\n");}key[0].flag = 0;}else if (key[1].flag == 1){sprintf((char *)text, "queue2 數據");buf = &text[0];err = xQueueSendToBack(queue2, &buf, portMAX_DELAY);if (err != pdTRUE){printf("queue2 隊列寫入數據錯誤\r\n");}key[1].flag = 0;}vTaskDelay(100);}
}
3 ) task2:
void task2(void *pvParameters)
{char *buf;QueueSetMemberHandle_t queue = NULL;while (1){queue = xQueueSelectFromSet(queue_set, portMAX_DELAY);if (queue == queue1){xQueueReceive(queue, &buf, portMAX_DELAY);printf("queue1 讀取到數據為: %s\r\n", buf);}if (queue == queue2){xQueueReceive(queue, &buf, portMAX_DELAY);printf("queue2 讀取到數據為: %s\r\n", buf);}vTaskDelay(100);}
}
3. FreeRTOS 信號量
3.1 簡介
信號量(Semaphore)是一種用于 任務同步 或 任務間資源互斥 的機制,主要用于:
- 任務同步:協調任務之間的執行順序,如等待某個事件發生后再執行任務。
- 互斥訪問:保護共享資源,防止多個任務同時訪問而引起競爭問題。
FreeRTOS 提供了 二值信號量、計數信號量 和 互斥信號量 三種類型:
信號量類型 | 作用 |
---|---|
二值信號量(Binary Semaphore) | 只能取 0 或 1,常用于任務同步或簡單的互斥訪問 |
計數信號量(Counting Semaphore) | 可取多個值,適用于多個資源的同步管理 |
互斥信號量(Mutex) | 具備 優先級繼承 機制,主要用于任務間的互斥訪問 |
3.2 常用 API 函數
API 函數 | 作用 |
---|---|
xSemaphoreCreateBinary() | 創建二值信號量(初始值為 0,需要 xSemaphoreGive() 釋放一次后才能使用) |
xSemaphoreCreateBinaryStatic() | 使用靜態方式創建二值信號量 |
xSemaphoreCreateCounting() | 創建計數信號量 |
xSemaphoreCreateMutex() | 創建互斥信號量 |
xSemaphoreGive() | 釋放信號量(+1) |
xSemaphoreTake() | 獲取信號量(-1),如果沒有可用信號量則阻塞 |
uxSemaphoreGetCount() | 獲取信號量的計數值 |
xSemaphoreGiveFromISR() | 在中斷中釋放信號量 |
xSemaphoreTakeFromISR() | 在中斷中獲取信號量 |
? 信號量 API 函數允許指定阻塞時間。 阻塞時間表示當一個任務試圖“獲取”信號量時,如果信號不是立即可用,那么該任務進入阻塞狀態的最大 “tick” 數。 如果多個任務在同一個信號量上阻塞,那么具有最高優先級的任務將在下次信號量可用時最先解除阻塞 。
在 FreeRTOS 中,信號量 實際上是一個特殊的隊列,本質上 創建信號量就是創建一個只存儲一個元素的隊列,區別在于:
- 二值信號量(Binary Semaphore):隊列長度固定為 1,每次只能存儲 一個項目,用于 任務同步。
- 計數信號量(Counting Semaphore):隊列長度可以大于 1,用于 多個資源的管理。
- 互斥信號量(Mutex):基于二值信號量,但增加了 優先級繼承 機制,用于資源互斥
對于二值信號量 FreeRTOS 通過 xQueueGenericCreate() 這個通用隊列創建函數來創建信號量,但是這個函數在 semphr.h 被宏定義為:
/* (UBaseType_t) 1 → 隊列長度 = 1(二值信號量只能存儲一個信號量) semSEMAPHORE_QUEUE_ITEM_LENGTH → 項目大小 = 0(不存儲實際數據,僅表示可用信號量) queueQUEUE_TYPE_BINARY_SEMAPHORE → 隊列類型 = 信號量隊列 */ #define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
信號量的作用本質是管理這個隊列是否為空。
而計數信號量創建函數等效為:
/* uxMaxCount → 隊列長度 (計數信號量可以為任何整數) queueSEMAPHORE_QUEUE_ITEM_LENGTH → 項目大小 = 0(不存儲實際數據,僅表示可用信號量) queueQUEUE_TYPE_BINARY_SEMAPHORE → 隊列類型 = 信號量隊列 */ xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );
互斥信號量創建函數等效為:
/* uxMutexLength 1 → 隊列長度 = 1(互斥信號量只能存儲一個信號量) uxMutexSize → 項目大小 = 0(不存儲實際數據,僅表示可用信號量) queueQUEUE_TYPE_MUTEX → 隊列類型 = 信號量隊列 */ xQueueGenericCreate( uxMutexLength, uxMutexSize, queueQUEUE_TYPE_MUTEX )
3.3 實驗
3.3.1 二值信號量實驗
- start_task:用來創建其他的 2 個任務。
- task1:用于按鍵掃描,當檢測到按鍵 KEY1 被按下時,釋放二值信號量。
- task2:獲取二值信號量,當成功獲取后打印提示信息。
1 ) 創建二值信號量:
QueueHandle_t semaphore_handle;void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建信號量 */semaphore_handle = xSemaphoreCreateBinary();if (semaphore_handle != NULL){printf("二值信號量創建成功\r\n");}else{printf("二值信號量創建失敗\r\n");}/* 創建任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{BaseType_t err;while (1){if (key[0].flag == 1){err = xSemaphoreGive(semaphore_handle);if (err != pdTRUE){printf("釋放信號量失敗\r\n");}led[0].state = !led[0].state;HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, (GPIO_PinState)led[0].state);key[0].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{BaseType_t err = 0;while (1){err = xSemaphoreTake(semaphore_handle,portMAX_DELAY);if (err != pdTRUE){printf("獲取信號量失敗\r\n");}else{printf("成功獲取信號量\r\n");}}
}
3.3 .2 計數信號量實驗
- start_task:用來創建其他的 2 個任務。
- task1:用于按鍵掃描,當檢測到按鍵 KEY1 被按下時,釋放計數型信號量。
- task2:每過一秒獲取一次計數型信號量,當成功獲取后打印信號量計數值。
1 ) 創建計數信號量:
QueueHandle_t semaphore_handle;void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建計數信號量 */UBaseType_t uxMaxCount = 4;UBaseType_t uxInitialCount = 0;semaphore_handle = xSemaphoreCreateCounting(uxMaxCount,uxInitialCount);if (semaphore_handle != NULL){printf("計數信號量創建成功\r\n");}else{printf("計數信號量創建失敗\r\n");}/* 創建任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{BaseType_t err;while (1){if (key[0].flag == 1){err = xSemaphoreGive(semaphore_handle);if (err != pdTRUE){printf("釋放信號量失敗\r\n");}else{printf("釋放信號量\r\n");}led[0].state = !led[0].state;HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, (GPIO_PinState)led[0].state);key[0].flag = 0;}}
}
3 ) task2:
void task2(void *pvParameters)
{BaseType_t err = 0;while (1){if(uxSemaphoreGetCount(semaphore_handle) != 0){err = xSemaphoreTake(semaphore_handle,portMAX_DELAY);if (err != pdTRUE){printf("獲取信號量失敗\r\n");}else{printf("成功獲取信號量\r\n");}}else{printf("無空閑資源分配\r\n");}vTaskDelay(1000);}
}
3.4 任務優先級翻轉 bug 問題
? 優先級翻轉是一個在實時系統中可能出現的問題,特別是在多任務環境中。該問題指的是一個較低優先級的任務阻塞了一個較高優先級任務的執行,從而導致高優先級任務無法及時完成。
典型的優先級翻轉場景如下:
- 任務 A(高優先級):擁有高優先級,需要訪問共享資源,比如一個關鍵數據結構。
- 任務 B(低優先級):擁有低優先級,目前正在訪問該共享資源。
- 任務 C(中優先級):位于任務 A 和任務 B 之間,具有介于兩者之間的優先級。
具體流程如下:
(1)任務 A 開始執行,但由于任務 B 正在訪問共享資源,任務 A 被阻塞等待。
(2)任務 C 獲得執行權,由于優先級高于任務 B,它可以搶占任務 B。
(3)任務 C 執行完成后,任務 B 被解除阻塞,開始執行,完成后釋放了共享資源。
(4)任務 A 重新獲取執行權,繼續執行。
? 這個過程中,任務 A 因為資源被占用而被阻塞,而任務 B 卻被中優先級的任務 C 搶占,導致任務 B 無法及時完成。這種情況稱為優先級翻轉,因為任務 C 的介入翻轉了高優先級任務 A 的執行順序。
實驗模擬
模擬優先級翻轉,觀察對搶占式內核的影響:
- start_task:用來創建其他的 3 個任務。
- task1:低優先級任務,同高優先級一樣的操作,不同的是低優先級任務占用信號
- task2:中等優先級任務,簡單的應用任務。
- task3:高優先級任務,會獲取二值信號量,獲取成功以后打印提示信息,處理完后釋放信號量。
1 ) 創建隊列程序:
QueueHandle_t semaphore_handle;/*** @brief : 啟動任務函數,創建三個任務,并刪除自己** @param pvParameters*/
void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建計數信號量 */UBaseType_t uxMaxCount = 1;UBaseType_t uxInitialCount = 1;semaphore_handle = xSemaphoreCreateCounting(uxMaxCount, uxInitialCount);if (semaphore_handle != NULL){printf("計數信號量創建成功\r\n");}else{printf("計數信號量創建失敗\r\n");}/* 創建三個任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{while (1){printf("低優先級 Task1 獲取信號量\r\n");xSemaphoreTake(semaphore_handle, portMAX_DELAY);printf("低優先級 Task1 正在運行\r\n");HAL_Delay(3000);printf("低優先級 Task1 釋放信號量\r\n");xSemaphoreGive(semaphore_handle);vTaskDelay(1000);}
}
3 ) task2:
void task2(void *pvParameters)
{while (1){printf("中優先級的 Task2 正在執行\r\n");HAL_Delay(1500);printf("Task2 執行完成一次.....\r\n");vTaskDelay(1000);}
}
4 ) task3:
void task3(void *pvParameters)
{while (1){printf("高優先級 Task3 獲取信號量\r\n");xSemaphoreTake(semaphore_handle, portMAX_DELAY);printf("高優先級 Task3 正在運行\r\n");HAL_Delay(1000);printf("高優先級 Task3 釋放信號量\r\n");xSemaphoreGive(semaphore_handle);vTaskDelay(1000);}
}
5 ) 實驗結果
可以觀察到 Task1 獲取信號量后,Task2 通過搶占 Task1 實現了翻轉優先級,實現了優于 Task3 的執行優先。
3.5 互斥信號量
? 互斥信號量是包含優先級繼承機制的二進制信號量。二進制信號量能更好實現同步(任務間或任務與中斷之間), 而互斥信號量有助于更好實現簡單互斥(即相互排斥)。優先級繼承是一種解決實時系統中任務調度引起的優先級翻轉問題的機制。在具體的任務調度中,當一個高優先級任務等待一個低優先級任務所持有的資源時,系統會提升低優先級任務的優先級,以避免高優先級任務長時間等待的情況。
互斥信號量的獲取和釋放函數與二值信號量的相應函數相似,但有一個重要的區別:互斥信號量不支持在中斷服務程序中直接調用。注意,當創建互斥信號量時,系統會自動進行一次信號量的釋放操作。
通過互斥信號量來解決優先級翻轉實驗
1 ) 將二值信號量改為互斥信號量:
semaphore_handle = xSemaphoreCreateMutex();if (semaphore_handle != NULL){printf("互斥信號量創建成功\r\n");}else{printf("互斥信號量創建失敗\r\n");}
2 ) 實驗結果:
可以觀察到 Task2 只能在 Task3 信號量釋放的后才可以搶占運行,不會發生任務優先級的翻轉(在 Task1 釋放信號量后 Task2 立馬搶占執行,而是 Task3 立刻占用信號量,開始執行 Task3 再執行 Task2)
4. FreeRTOS 事件標志組
4.1 簡介
? 事件標志組(Event Groups)是 FreeRTOS 提供的一種輕量級任務間同步機制,允許任務或中斷通過 設置或清除 “事件標志(Event Bits)” 來實現 事件通知、同步和狀態監控。事件標志組類似于 二進制標志位集合,每個標志位可以單獨操作,多個任務可以等待多個標志位滿足特定條件后再執行。
事件標志組特點
- 每個事件標志組是一個 8/24 位獨立的二進制標志位(Event Bits)。
- 任務可以等待多個事件標志位,并指定所有滿足或任意一個滿足時觸發任務執行。
- 事件標志位可由任務或中斷設置/清除,支持
ISR
操作。 - 比隊列(Queue)和信號量(Semaphore)更高效,適用于簡單事件同步。
事件標志組的適用場景
應用場景 | 適合事件標志組 | 適合隊列或信號量 |
---|---|---|
多任務同步 | ?? 多任務需要等待某些事件發生 | ? |
中斷通知任務 | ?? 事件觸發后可直接通知多個任務 | ?? 但隊列或信號量只能通知一個任務 |
狀態監測 | ?? 可使用不同事件位表示不同狀態 | ? |
任務間數據傳輸 | ? | ?? 隊列適合傳輸數據 |
? 事件標志組和信號量都是 FreeRTOS 中用于任務同步和通信的機制,但它們適用于不同的場景,主要區別如下:
對比項 | 事件標志組(Event Flags Group) | 信號量(Semaphore) |
---|---|---|
用途 | 用于任務間事件通知和同步,標志位代表某個事件狀態 | 用于任務間資源控制和同步,確保安全訪問共享資源 |
狀態表示 | 每個標志位只有 已設置/未設置 兩種狀態 | 信號量是一個計數器,可用于 計數、互斥、同步 |
任務等待 | 任務可等待 多個事件同時滿足 或 任意一個事件發生 | 任務等待信號量計數變為 非零,然后繼續執行 |
適用場景 | 適用于任務間 事件同步,如 數據準備完成、狀態變更通知 | 適用于 資源訪問控制、同步、互斥,防止多個任務同時訪問共享資源 |
信號傳遞 | 可存儲多個事件狀態,即使任務不在等待,事件狀態仍然保留 | 不可存儲狀態,如果任務未在等待,信號量會直接丟失 |
示例 | 任務 A 設置事件標志 → 任務 B 等待該事件標志并繼續執行 | 任務 A 釋放信號量 → 任務 B 獲取信號量并訪問共享資源 |
4.2 事件標志組和事件位數據類型
? 可以將事件組視為一個二進制標志集合,其中的每一位表示一個事件。任務可以設置(Set)、清(Clear)或等待(Wait)某些特定的事件位,從而實現任務間的 同步與信號觸發。事件組的大小(即支持的 事件位數)受 FreeRTOS 配置項的影響,由 configUSE_16_BIT_TICKS
控制:
#define configUSE_16_BIT_TICKS 1 // 事件組內可用標志數位為8
#define configUSE_16_BIT_TICKS 0 // 事件組內可用標志數位為24
? 在 FreeRTOS 中,事件組的最大位數 由 EventBits_t
變量的大小決定,而 EventBits_t
的大小受 TickType_t
(用于計時的變量類型)影響。所以最高為32位,但是 FreeRTOS 保留了 8 位 供系統內部使用
4.3 事件標志組相關 API 函數介紹
API 函數 | 功能 |
---|---|
xEventGroupCreate() | 創建事件標志組 |
xEventGroupCreateStatic() | 使用靜態地創建事件標志組 |
xEventGroupSetBits() | 設置一個或多個事件標志位 |
xEventGroupClearBits() | 清除一個或多個事件標志位 |
xEventGroupWaitBits() | 等待一個或多個事件標志位被置位 |
xEventGroupGetBits() | 獲取當前事件標志狀態 |
xEventGroupSetBitsFromISR() | 在中斷中設置事件標志位 |
vEventGroupDelete() | 刪除事件標志組 |
xEventGroupSync() | 多個任務同步等待所有事件標志 |
4.4 實驗
- start_task:用來創建其他 3 個任務,并創建事件標志組。
- task1:讀取按鍵按下鍵值,根據不同鍵值將事件標志組相應事件位置一,模擬事件發生。
- task2:同時等待事件標志組中的多個事件位,當這些事件位都置 1 的話就執行相應的處理。
- task3:等待事件標志組中的多個任一事件位,當這些事件位任意置 1 的話就執行相應的處理。
1 ) 創建事件標志組程序:
EventGroupHandle_t event_group;/*** @brief : 啟動任務函數,創建三個任務,并刪除自己** @param pvParameters*/
void task_start(void *pvParameters)
{/* 進入臨界區:保護臨界區里的代碼不會被打斷 */taskENTER_CRITICAL();/* 創建事件標志組 */event_group = xEventGroupCreate();if (event_group == NULL){printf("事件標志組創建失敗\r\n");}else{printf("事件標志組創建成功\r\n");}/* 創建任務 */xTaskCreate((TaskFunction_t)task1,(char *)"task1",(configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK1_PRIORITY,(TaskHandle_t *)&Task1_Handler);xTaskCreate((TaskFunction_t)task2,(char *)"task2",(configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK2_PRIORITY,(TaskHandle_t *)&Task2_Handler);xTaskCreate((TaskFunction_t)task3,(char *)"task3",(configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,(void *)NULL,(UBaseType_t)TASK3_PRIORITY,(TaskHandle_t *)&Task3_Handler);/* 啟動任務只需要執行一次即可,用完就刪除自己 */vTaskDelete(NULL);/* 退出臨界區 */taskEXIT_CRITICAL();
}
2 ) task1:
void task1(void *pvParameters)
{while (1){if (key[0].flag == 1){xEventGroupSetBits(event_group, 0x01 << 0);printf("事件0置位\r\n");key[0].flag = 0;}else if (key[1].flag == 1){xEventGroupSetBits(event_group, 0x01 << 1);printf("事件1置位\r\n");key[1].flag = 0;}else if (key[2].flag == 1){xEventGroupSetBits(event_group, 0x01 << 2);printf("事件2置位\r\n");key[2].flag = 0;}else if (key[3].flag == 1){xEventGroupSetBits(event_group, 0x01 << 3);printf("事件3置位\r\n");key[3].flag = 0;}vTaskDelay(100);}
}
3 ) task2:
void task2(void *pvParameters)
{while (1){xEventGroupWaitBits(event_group, 1 << 0 | 1 << 1, pdTRUE, pdTRUE, portMAX_DELAY);printf("事件0和1同時有效,執行task2...\r\n");vTaskDelay(100);}
}
4 ) task3:
void task3(void *pvParameters)
{while (1){xEventGroupWaitBits(event_group, 1 << 2 | 1 << 3, pdTRUE, pdFALSE, portMAX_DELAY);printf("事件2和3任一有效,執行task3...\r\n");vTaskDelay(100);}
}