說明

本文僅作為學習FreeRTOS的記錄文件,作為初學者肯定很多理解不對甚至錯誤的地方,望網友指正。

FreeRTOS是一個RTOS(實時作業系統)系統,支援搶佔式、合作式和時間片排程。適用於微處理器或小型微處理器的實時應用。

本文件使用的FreeRTOS版本:FreeRTOS Kernel V10.4.1

參考文件:《FreeRTOS_Reference_Manual_V10.0.0.pdf》《FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf》《STM32F4 FreeRTOS開發手冊_V1.1.pdf》

參考視訊:正點原子FreeRTOS手把手教學-基於STM32_嗶哩嗶哩_bilibili

9 訊號量

訊號量是作業系統重要的一部分,一般用來進行資源管理和任務同步,FreeRTOS訊號量分為二值訊號量、計數型訊號量、互斥訊號量和遞迴型訊號量。

9.1 二值訊號量

9.1.1 相關說明

二值訊號量通常用於互斥訪問或者同步。和互斥訊號量的差別:互斥訊號量有優先順序繼承,二值訊號量沒有優先順序繼承,所有二值訊號量適用於同步,而互斥訊號量適用於互斥訪問。

和佇列一樣,訊號量API函式允許設定一個阻塞時間,阻塞時間是當任務獲得訊號的時候由於訊號量無效從而導致任務進行阻塞態的最大時鐘節拍數,當訊號量有效時高優先順序任務就會解除阻塞狀態。

二值訊號量就是隻有一個佇列項的佇列,所以這個佇列要麼滿的,要麼空的。二值訊號量的工作過程如下:

(1)二值訊號量無效,任務阻塞等待訊號量

(2)中斷髮生,釋放了訊號量

(3)任務獲取訊號量成功,從阻塞狀態解除

(4)任務再次進入阻塞態,等待訊號量

完整的一次流程如下:

9.1.2 相關函式

(1)動態建立訊號量

函式原型:

#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphoreCreateBinary( void );

函式描述:建立一個二值訊號量,並返回訊號量控制代碼。每一個訊號量需要一個記憶體空間來存放訊號量狀態。這個函式的建立的訊號量空間由FreeRTOS自動分配。訊號量建立之後是空的,任務這時候是無法獲得的。

函式引數:

返回值:NULL:建立失敗。其他值:建立成功的二值訊號量的控制代碼

(2)靜態建立訊號量

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );

函式描述:建立一個二值訊號量,並返回訊號量控制代碼。每一個訊號量需要一個記憶體空間來存放訊號量狀態。這個函式的建立的訊號量空間由使用者指定。訊號量建立之後是空的,任務這時候是無法獲得的。

函式引數:pxSemaphoreBuffer:指向StaticSemaphore_t型別的變數,這個變數用來儲存訊號量的狀態。

返回值:NULL:建立失敗。其他值:建立成功的二值訊號量的控制代碼

(3)任務級釋放訊號量

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

函式描述:釋放訊號量之前,訊號量必須已經被建立了。

函式引數:xSemaphore:要釋放的訊號量控制代碼

返回值:pdPASS:訊號量釋放成功。pdFAIL:訊號量釋放失敗。

(4)中斷級釋放訊號量

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );

函式描述:中斷級釋放訊號量函式,在中斷處理函式中使用。

函式引數:xSemaphore:要釋放的訊號量控制代碼;

pxHigherPriorityTaskWoken:標記退出此函式是否進行任務切換,此值為pdTRUE的時候在退出中斷函式之前要進行一次任務切換。

返回值:pdPASS:訊號量釋放成功。errQUEUE_FULL:訊號量釋放失敗,訊號量已經被釋放了。

(5)任務級獲取訊號量

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );

函式描述:獲取訊號量之前,訊號量必須已經被建立了。

函式引數:xSemaphore:要獲取的訊號量控制代碼;

xTicksToWait:當任務無法獲取到訊號量,任務最大的保持阻塞的時間。如果為0,任務無法獲得訊號量時將立即返回。阻塞時間指的時時鐘滴答數,所以阻塞的時間大小取決於系統頻率,可以使用pdMS_TO_TICKS() 巨集來指定阻塞多少毫秒。如果為portMAX_DELAY,任務將一直等待。

返回值:pdPASS:訊號量獲取成功。pdFAIL:訊號量獲取失敗。

(6)中斷級獲取訊號量

函式原型:

#include “FreeRTOS.h”
#include “queue.h”
BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken );

函式描述:在中斷服務函式中獲取訊號量。

函式引數:xSemaphore:要獲取的訊號量控制代碼;

pxHigherPriorityTaskWoken:標記退出此函式後是否進行任務切換,此值為pdTRUE的時候在退出中斷函式之前要進行一次任務切換。

返回值:pdPASS:訊號量獲取成功。pdFAIL:訊號量獲取失敗。

9.1.3 操作實驗

實驗目的:使用二值訊號量完成任務間的同步

實驗設計:任務task00定時釋放二值訊號量,任務task01獲取二值訊號量,接收到訊號量就進行相應的動作。

測試程式碼:

/* task00 info */
configSTACK_DEPTH_TYPE Task00_STACK_SIZE = 5;
UBaseType_t Task00_Priority = 1;
TaskHandle_t Task00_xHandle; /* task01 info */
configSTACK_DEPTH_TYPE Task01_STACK_SIZE = 5;
UBaseType_t Task01_Priority = 2;
TaskHandle_t Task01_xHandle; //二值訊號量
SemaphoreHandle_t BinarySemaphore; void vTask00_Code(void *para)
{
static unsigned int cnt = 0;
BaseType_t err = pdTRUE;
for (;;)
{
PRINT(" task00 cnt %u...", cnt++);
err = xSemaphoreGive(BinarySemaphore);
if (err != pdTRUE)
PRINT("BinarySemaphore give failed!\n");
vTaskDelay(2000);
}
} void vTask01_Code(void *para)
{
static unsigned int cnt = 0;
BaseType_t err = pdTRUE;
for (;;)
{
xSemaphoreTake(BinarySemaphore, portMAX_DELAY);
PRINT(" task01 cnt %u...", cnt++);
vTaskDelay(500);
}
} void test_BinarySemaphore()
{
BinarySemaphore = xSemaphoreCreateBinary(); if (xTaskCreate(vTask00_Code, "task00 task",
Task00_STACK_SIZE, NULL, Task00_Priority,
&Task00_xHandle) != pdPASS)
{
PRINT("creat task00 failed!\n");
} if (xTaskCreate(vTask01_Code, "task01 task",
Task01_STACK_SIZE, NULL, Task01_Priority,
&Task01_xHandle) != pdPASS)
{
PRINT("creat task01 failed!\n");
}
} void creat_task(void)
{
test_BinarySemaphore();
// test_queue();
}

編譯,執行:

$ ./build/freertos-simulator
task00 cnt 0...
task01 cnt 0...
task00 cnt 1...
task01 cnt 1...
task00 cnt 2...
task01 cnt 2...

可以看出,任務task01等到了訊號量之後才會執行。

接著,將獲取訊號量函式xSemaphoreTake的阻塞時間改為0,也就是沒獲取到訊號量,立即返回。

void vTask01_Code(void *para)
{
static unsigned int cnt = 0;
BaseType_t err = pdTRUE;
for (;;)
{
xSemaphoreTake(BinarySemaphore, 0);
PRINT(" task01 cnt %u...", cnt++);
vTaskDelay(500);
}
}

編譯,執行:

$ ./build/freertos-simulator
task01 cnt 0...
task00 cnt 0...
task01 cnt 1...
task01 cnt 2...
task01 cnt 3...
task01 cnt 4...
task00 cnt 1...

可以看出,任務task01不會等待訊號量,而是繼續執行。

9.1.4 優先順序反轉

使用二值訊號量會出現優先順序反轉的問題,優先順序反轉在可剝奪核心中是常見的,但在實時系統中不允許出現這種現象。

上述圖中,高任務H會晚於低優先順序任務M執行,這就發生了優先順序反轉。

優先順序反轉實驗設計:

實驗設計:建立三個任務,高優先順序任務獲取二值訊號量,獲取成功後進行相應的處理,處理完之後釋放訊號量;中優先順序任務簡單執行;低優先順序任務和高優先順序任務一樣,會獲取二值訊號量,獲取成功後進行相應處理。

測試程式碼:

configSTACK_DEPTH_TYPE TaskLow_STACK_SIZE = 5;
UBaseType_t TaskLow_Priority = 2;
TaskHandle_t TaskLow_xHandle; configSTACK_DEPTH_TYPE TaskMiddle_STACK_SIZE = 5;
UBaseType_t TaskMiddle_Priority = 3;
TaskHandle_t TaskMiddle_xHandle; configSTACK_DEPTH_TYPE TaskHigh_STACK_SIZE = 5;
UBaseType_t TaskHigh_Priority = 4;
TaskHandle_t TaskHigh_xHandle; //二值訊號量
SemaphoreHandle_t BinarySemaphore; void vTaskLow_Code(void *para)
{
static unsigned int times = 0;
BaseType_t err = pdTRUE;
for (;;)
{
xSemaphoreTake(BinarySemaphore, portMAX_DELAY);
PRINT(" low task running");
for (times = 0; times < 20000000; times++)
taskYIELD();
err = xSemaphoreGive(BinarySemaphore);
if (err != pdTRUE)
PRINT("BinarySemaphore give failed!");
vTaskDelay(1000);
}
} void vTaskMiddle_Code(void *para)
{
for (;;)
{
PRINT(" task middle running");
vTaskDelay(1000);
}
} void vTaskHigh_Code(void *para)
{
for (;;)
{
vTaskDelay(500);
PRINT(" task high Pend Sem");
xSemaphoreTake(BinarySemaphore, portMAX_DELAY);
PRINT(" task high running!");
xSemaphoreGive(BinarySemaphore);
vTaskDelay(500);
}
} void test_BinarySemaphore()
{
taskENTER_CRITICAL();
BinarySemaphore = xSemaphoreCreateBinary();
if (BinarySemaphore != NULL)
xSemaphoreGive(BinarySemaphore); if (xTaskCreate(vTaskLow_Code, "taskLow task",
TaskLow_STACK_SIZE, NULL, TaskLow_Priority,
&TaskLow_xHandle) != pdPASS)
{
PRINT("creat taskLow failed!\n");
} if (xTaskCreate(vTaskMiddle_Code, "taskMiddle task",
TaskMiddle_STACK_SIZE, NULL, TaskMiddle_Priority,
&TaskMiddle_xHandle) != pdPASS)
{
PRINT("creat taskMiddle failed!\n");
} if (xTaskCreate(vTaskHigh_Code, "taskHigh task",
TaskHigh_STACK_SIZE, NULL, TaskHigh_Priority,
&TaskHigh_xHandle) != pdPASS)
{
PRINT("creat taskHigh failed!\n");
}
taskEXIT_CRITICAL();
} void creat_task(void)
{
test_BinarySemaphore();
}

編譯、執行:

$ ./build/freertos-simulator
task middle running
low task running
task high Pend Sem
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task middle running
task high running!
task middle running
task high Pend Sem
task high running!
low task running
task middle running

可以看出了優先順序反轉,中優先順序任務比高優先順序任務先執行。

9.2 計數型訊號量

計數型訊號量也叫數值型訊號量,其實質是長度大於1的佇列。

主要用於兩個場景:

1、事件計數:在這個場景中,每次事件發生時就在事件處理函式中釋放訊號量,其他任務獲取訊號量來處理事件。這種場合計數型訊號量初始計數值為0。

2、資源管理:在這個場景中,訊號量代表當前可用的資源數量。一個任務想要獲取資源的使用權,必須先獲得訊號量,訊號量獲取成功訊號量就會減1,當訊號量為0時就沒有訊號量了。當一個任務使用完訊號量之後要釋放訊號量。這個場景中,訊號量的初始值就是資源的數量。

9.2.1 相關函式

(1)動態建立函式

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );

函式描述:建立一個計數型訊號量,返回訊號量的控制代碼。訊號量的記憶體空間由系統指定。

函式引數:uxMaxCount:計數訊號量的最大計數值,當訊號量值等於這個值的時候釋放訊號量就會失敗。

uxInitialCount:計數訊號量初始值。

返回值:NULL:計數訊號量建立失敗;其他值:計數訊號量建立成功,返回計數訊號量控制代碼。

(2)靜態建立函式

函式原型:

#include “FreeRTOS.h”
#include “semphr.h”
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t pxSempahoreBuffer );

函式描述:建立一個計數型訊號量,返回訊號量的控制代碼。訊號量的記憶體空間由使用者指定。

函式引數:uxMaxCount:計數訊號量的最大計數值,當訊號量值等於這個值的時候釋放訊號量就會失敗。

uxInitialCount:計數訊號量初始值。

pxSempahoreBuffer:指向StaticSemaphore_t型別的變數,用於儲存訊號量結構體。

返回值:NULL:計數訊號量建立失敗;其他值:計數訊號量建立成功,返回計數訊號量控制代碼。

(2)釋放和獲取函式

釋放和獲取函式和二值訊號量的一樣,參見9.1.2小節。

9.2.2 操作實驗

實驗目的:學習計數型訊號量的使用方法。

實驗設計:主函式中建立一個計數型訊號量,計數值為10,初始化計數值為0,然後建立兩個任務,任務task00釋放訊號量,任務task01獲取訊號量。

測試程式碼:

/* task00 info */
configSTACK_DEPTH_TYPE Task00_STACK_SIZE = 5;
UBaseType_t Task00_Priority = 1;
TaskHandle_t Task00_xHandle; /* task01 info */
configSTACK_DEPTH_TYPE Task01_STACK_SIZE = 5;
UBaseType_t Task01_Priority = 3;
TaskHandle_t Task01_xHandle; //計數型訊號量
SemaphoreHandle_t CountSemaphore; void vTask00_Code(void *para)
{
static unsigned int cnt = 0;
BaseType_t err = pdTRUE;
for (;;)
{
PRINT(" task00 cnt %u...", cnt++);
err = xSemaphoreGive(CountSemaphore);
if (err != pdTRUE)
PRINT("BinarySemaphore give failed!\n");
vTaskDelay(200);
if (cnt == 4)
vTaskDelete(Task00_xHandle);
}
} void vTask01_Code(void *para)
{
static unsigned int cnt = 0;
BaseType_t err = pdTRUE;
for (;;)
{
xSemaphoreTake(CountSemaphore, portMAX_DELAY);
PRINT(" task01 cnt %u...", cnt++);
vTaskDelay(1000);
}
} void test_CountSemaphore()
{
CountSemaphore = xSemaphoreCreateCounting(10, 0); if (xTaskCreate(vTask00_Code, "task00 task",
Task00_STACK_SIZE, NULL, Task00_Priority,
&Task00_xHandle) != pdPASS)
{
PRINT("creat task00 failed!\n");
} if (xTaskCreate(vTask01_Code, "task01 task",
Task01_STACK_SIZE, NULL, Task01_Priority,
&Task01_xHandle) != pdPASS)
{
PRINT("creat task01 failed!\n");
}
} void creat_task(void)
{
test_CountSemaphore();
}

編譯、執行:

$ ./build/freertos-simulator
task00 cnt 0...
task01 cnt 0...
task00 cnt 1...
task00 cnt 2...
task00 cnt 3...
task01 cnt 1...
task01 cnt 2...
task01 cnt 3...

可以看出,共釋放了4個訊號量,任務task1執行了4次。