FreeRTOS信號量
信號量是操作系統總重要的一部分,信號量一般用來進行資源管理和任務同步,FreeRTOS中信號量又分為二值信號量、計數型信號量、互斥信號量和遞歸互斥信號量。不同的信號量其應用場景不同,但是有些場景是可以互換着使用的。
信號量簡介
信號量常常用於控制對共享資源的訪問和任務同步。舉一個很常見的例子,某個停車場有100個停車位,這100個停車位大家都可以使用,對於大家說這100個停車位就是共享資源。假設現在這個停車場正常運行,你要把車停到這個停車場肯定要先看一下現在停了多少車了?還有沒有停車位?當前停車數量就是一個信號量,具體的停車數量就是這個信號量值。當這個值到100的時候,說明停車場滿了。停車場滿的時候你可以等一會兒看看有沒有其他的車開出停車場,當有車看出停車場的時候,停車數量就會減一,也就是說信號量減一,此時你就可以把車停進去了,你把車停進去以后,停車數量就會加一,也就是信號量加一。這就是一個典型的使用信號量進行共享資源管理的案例。在這個案例中,使用的就是計數型信號量。在看另一個案例:使用公共電話。我們知道一次只能一個人使用電話,這個時候,公共電話只可能有兩個狀態:使用或未使用,如果用電話的這兩個狀態作為信號量的話,那么這個就是二值信號量。
信號量用於控制共享資源訪問的場景相當於一個上鎖機制,代碼只有獲得了這個鎖的鑰匙才能夠執行。
上面我們講了信號量在共享資源訪問中的使用,信號量的另一個重要的應用場合就是任務同步,用於任務與任務或中斷與任務之間的同步。在執行中斷服務函數的時候就可以通過向任務發送信號量來通知任務它所期待的事件發生了,當退出中斷服務函數以后,在任務調度器的調度下同步的任務就會執行。在編寫中斷服務函數的時候,我們都知道一定要快進快出,中斷服務函數里面不能有太多的代碼,否則的話會影響中斷的實時性。裸機編寫中斷服務函數的時候一般都只是在中斷服務函數中打個標記,然后在其他的地方根據標記來做具體的處理過程。在使用RTOS系統的時候,我們就可以借助信號量完成此功能,當中斷發生的時候就釋放信號量,如果獲取到信號量就說明中斷發生了,那么開始完成相應的處理,這樣做的好處就是中斷執行時間非常短。這個例子就是中斷與任務之間使用信號量來完成同步,當然,任務與任務之間也可以使用信號量來完成同步。
FreeRTOS中還有一些其他特殊類型的信號量,比如互斥信號量和遞歸互斥信號量。
二值信號量
二值信號量簡介
二值信號量通常用於互斥訪問或同步,二值信號量與互斥信號量非常類似,但是還是有一些細微的差別,互斥信號量擁有優先級繼承機制,二值信號量沒有優先級繼承。因此二值信號量更適合用於同步(任務與任務或任務與中斷的同步),而互斥信號量適合用於簡單的互斥訪問。
和隊列一樣,信號量API函數允許設置一個阻塞時間,阻塞時間是當任務獲取信號量的時候,由於信號量無效而導致任務進入阻塞態的最大時鍾節拍數。如果多個任務同時阻塞在同一個信號量上的話,那么優先級最高的那個任務優先獲得信號量,這樣當當信號量有效的時候,高優先級的任務就會解除阻塞狀態。
二值信號量其實就是一個只有一個隊列項的隊列,這個特殊的隊列要么是滿的,要么是空的,這正要就是二值。任務和中斷使用這個特殊隊列不同在乎隊列中存的是什么消息,只需要知道這個隊列是滿的還是空的。可以利用這個機制來完成任務與中斷之間的同步。
在使用應用中通常會使用一個任務來處理MCU的某個外設,比如網絡應用中,一般最簡單的方法就是使用一個任務去輪詢地查詢MCU的ETH(網絡相關外設,如STM32的以太網MAC)外設是否有數據,當有數據的時候就處理這個網絡數據。這樣使用輪詢的方式是很浪費CPU資源的,而且也阻止了其他任務的運行。最理想的方法就是當沒有網絡數據的時候,網絡任務就進入阻塞態,把CPU讓給其他的任務,當有數據的時候網絡任務才去執行。現在使用二值信號量就可以實現這樣的功能,任務通過獲取信號量來判斷是否有網絡數據,沒有的話就進入阻塞態,而網絡中斷服務函數(大多數的網絡外設都與中斷功能,比如STM32的MAC專用DMA中斷,通過中斷可以判斷是否接收到數據)通過釋放信號量來通知任務以太網外設接收到了網絡數據,網絡任務可以去提取處理了。網絡任務只是在一直獲取二值信號量,它不會釋放信號量,而中斷服務函數是一直在釋放信號量,它不會獲取信號量。在中斷服務函數中發送信號量可以使用函數 xSemaphoreGiveFromISR() ,也可以使用任務通知功能來替他二值信號量,而且使用任務通知的話,速度更塊,代碼量更少。
使用二值信號量來完成中斷與任務同步的這個機制中,任務優先級確保了外設能夠得到及時的處理,這樣做相當於推遲了中斷處理過程。也可以使用隊列替代二值信號量,在外設事件的中斷服務函數中獲取相關數據,並將相關數據通過隊列發送給任務。如果隊列無效的話,任務就進入阻塞態,直到隊列中有數據,任務接收到數據以后就開始相關的處理過程。
下面幾個步驟演示了二值信號量的工作過程。
1. 二值信號量無效
在上圖中任務Task通過函數 xSemaphoreTake() 獲取信號量,但是此時二值信號量無效,所以任務Task進入阻塞態。
2. 中斷釋放信號量
此時中斷發生了,在中斷服務函數中通過函數 xSemaphoreGiveFromISR() 釋放信號量,因此信號量變為有效。
3. 任務獲取信號量成功
由於信號量已經有效了,所以任務Task獲取信號量成功,任務從阻塞態解除,開始執行相關處理過程。
4. 任務再次進入阻塞態
由於任務函數一般都是一個大循環,所以在任務做完相關的處理以后就會再次調用 xSemaphoreTask() 獲取信號量。在執行完第三步以后,二值信號量就已經變為無效了,所以任務將再次進入阻塞態,和第一步一樣,直到中斷再次發生並且調用函數 xSemaphoreGiveFromISR() 釋放信號量。
創建二值信號量
和隊列一樣,想要使用二值信號量就必須先創建二值信號量,二值信號量創建函數如下表:
函數 | 描述 |
vSemaphoreCreateBinary() | 動態創建二值信號量,這個是老版本FreeRTOS中使用的創建二值信號量的API函數。 |
xSemaphoreCreateBinary() | 動態創建二值信號量,新版FreeRTOS使用此函數創建二值信號量。 |
xSemaphoreCreateBinaryStatic() | 靜態創建二值信號量。 |
1. 函數 vSemaphoreCreateBinary()
此函數是老版本FreeRTOS中創建二值信號量函數,新版本已經不再使用了,新版本的FreeRTOS使用 xSemaphoreCreateBinary() 來替代此函數,這里還保留這個函數是為了兼容哪些基於老版本FreeRTOS而做的應用層代碼。此函數是個宏,具體創建過程是由函數 xQueueGenericCreate()
來完成的,在文件 semphr.h 中有如下定義:
void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
參數:
xSemaphrore:保存創建成功的二值信號量句柄。
返回值:
NULL:二值信號量創建失敗。
其他值:二值信號量創建成功。
2. 函數xSemaphoreCreateBinary()
此函數是vSemaphoreCreateBinary()的新版本,新版本中FreeRTOS中統一用此函數來創建二值信號量。使用此函數創建二值信號量的話,信號量所需RAM是由FreeRTOS的內存管理部分來動態分配的。次函數創建好的二值信號量默認是空的,也就是說剛剛創建好的二值信號量量使用函數xSemephoreTask()是獲取不到的。vSemaphoreCreateBinary()也是個宏,具體創建過程是由函數xQueueGenericCreate()來完成的,其函數原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary( void )
參數:
無。
返回值:
NULL:二值信號量創建失敗。
其他值:創建成功的二值信號量的句柄。
3. 函數 xSemephroeCreateBinaryStatic()
此函數也是創建二值信號量的,只不過使用次函數創建二值信號量的話信號量所需要的RAM需要由用戶來分配,此函數是個宏,具體創建過程是通過函數xQueueGenericCreateStatic()來完成的,函數原型如下:
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
參數:
pxSemaphoreBuffer:此參數指向一個StaticSemaphore_t類型的變量,用來保存信號量結構體。
返回值:
NULL:二值信號量創建失敗。
其他值:創建成功的二值信號量句柄。
二值信號量創建過程分析
上一小節講了三個用於二值信號量創建的函數,兩個動態的創建函數和一個靜態的創建函數。本節就來分析一下這兩個動態的創建函數,靜態創建函數和動態類似,就不做分析了。首先看一下老版本的二值信號量動態創建函數 vSemaphoreCreateBinary(),函數代碼如下:
1 #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 2 #define vSemaphoreCreateBinary( xSemaphore ) \ 3 { \ 4 ( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, \ 5 semSEMAPHORE_QUEUE_ITEM_LENGTH, \ 6 queueQUEUE_TYPE_BINARY_SEMAPHORE ); \ 7 if( ( xSemaphore ) != NULL ) \ 8 { \ 9 ( void ) xSemaphoreGive( ( xSemaphore ) ); \ 10 } \ 11 } 12 #endif
第4~6行:上面說了二值信號量是在隊列的基礎上實現的,所以創建二值信號量就是創建隊列的過程。這里使用函數xQueueGenericCreate()創建了一個隊列,隊列長度為1,隊列項長度為0,隊列類型為queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信號量。
第9行:當二值信號量創建成功以后立即調用函數 xSemaphoreGive()釋放二值信號量,此時新創建的二值信號量有效。
再來看一下新版本的二值信號量創建函數 xSemaphoreCreateBianry(),函數代碼如下:
1 #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 2 #define xSemaphoreCreateBinary() \ 3 xQueueGenericCreate( ( UBaseType_t ) 1, \ 4 semSEMAPHORE_QUEUE_ITEM_LENGTH, \ 5 queueQUEUE_TYPE_BINARY_SEMAPHORE ) 6 #endif
可以看出新版本的二值信號量創建函數也是使用函數 xQueueGenericCreate()來創建一個類型為queueQUEUE_TYPE_BINARY_SEMAPHORE、長度為1、隊列項長度為0的隊列。這一步和老版本的二值信號量創建函數一樣,唯一不同的就是新版本的函數在成功創建二值信號量以后不會立即調用函數xSemephoreGive()釋放二值信號量。也就是說新版函數創建的二值信號量默認是無效的,而老版本是有效的。
大家注意看,創建的隊列是個沒有存儲區的隊列,前面說了使用隊列是否為空來表示二值信號量,而隊列是否為空可以通過隊列結構體的成員變量uxMessageWaiting來判斷。
釋放信號量
釋放信號量的函數有兩個:
函數 | 描述 |
xSemaphoreGive() | 任務級信號量釋放函數 |
xSemaphoreGiveFromISR() | 中斷級信號量釋放函數 |
同隊列一樣,釋放信號量也分為任務級和中斷級。還有,不管是二值信號量、計數型信號量還是互斥信號量,它們都使用上表中的函數釋放信號量,遞歸互斥信號量有專用的釋放函數。
1. 函數 xSemaphoreGive ()
此函數用於釋放二值信號量、計數型信號量或互斥信號量,此函數是一個宏,真正釋放信號量的過程由函數 xQueueGenericSend()來完成。函數原型如下:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
參數:
xSemaphore:要釋放的信號量句柄。
返回值:
pdPASS:釋放信號量成功。
errQUEUE_FULL:釋放信號量失敗。
我們在來看一下函數 xSemaphoreGive()的具體內容,此函數在文件semphr.h中有如下定義:
1 #define xSemaphoreGive( xSemaphore ) \ 2 xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \ 3 NULL, \ 4 semGIVE_BLOCK_TIME, \ 5 queueSEND_TO_BACK )
可以看出任務級釋放信號量就是想隊列發送消息的過程,只是這里並沒有發送具體的消息,阻塞時間為0(宏 semGIVE_BLOCK_TIME為0),入隊方式采用后向入隊。入隊的時候隊列結構體成員變量uxMessageWaiting會加一,對於二值信號量通過判斷uxMessageWaiting就可以知道信號量是否有效了。uxMessageWaiting為1的話,說明二值信號量有效,為0就無效。如果隊列滿的話就返回錯誤值errQUEUE_FULL,提示隊列滿,入隊失敗。
2. 函數 xSemaphoreGiveFromISR()
此函數用於在中斷中釋放信號量,此函數智能用來釋放二值信號量和計數型信號量,絕對不能用來在中斷服務函數中釋放互斥信號量!此函數是一個宏,真正執行的是函數 xQueueGiveGromISR(),此函數原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken )
參數:
xSemaphore:要釋放的信號量句柄。
pxHigherPriorityTaskWoken:標記退出次函數以后是否進行任務切換,這個變量的值是由這個函數來設置的,用戶不能進行設置,用於只需要提供一個變量來保存這個值就行了。當此值為pdTRUE的時候,在退出中斷服務函數之前一定要進行一次任務切換。
返回值:
pdPASS:釋放信號量成功。
errQUEUE_FULL:釋放信號量失敗。
在中斷中釋放信號量真正使用的事函數xQueueGiveFromISR(),此函數和中斷級通用入隊函數xQueueGenericSendFromISR()及其類似!只是針對信號量做了微小的改動。函數xSemaphoreGiveFromISR()不能用於在中斷中釋放互斥信號量,以為互斥信號量涉及到優先級繼承的問題,而中斷不屬於任務,沒法處理中斷優先級繼承。
獲取信號量
獲取信號量也有兩個函數:
函數 | 描述 |
xSemaphoreTake() | 任務級獲取信號量函數 |
xSemaphoreTakeFromISR() | 中斷級獲取信號量函數 |
同釋放信號量的API一樣,不管是二值信號量、計數型信號量還是互斥信號量,它們都使用上表中的函數獲取信號量。
1. 函數 xSemaphoreTake()
此函數用於獲取二值信號量、計數型信號量或互斥信號量。此函數是一個宏,真正獲取信號量的過程由函數xQueueGenericReceive()來完成。函數原型如下:
1 BaseType_t xSemaphoreTake( 2 SemaphoreHandle_t xSemaphore, 3 TickType_t xBlockTime 4 )
參數:
xSemaphore:要獲取的信號量句柄。
xBlockTime:阻塞時間
返回值:
pdTRUE:獲取信號量成功。
pdFALSE:超時,獲取信號量失敗。
在來看一下函數xSemaphoreTake()的具體內容,此函數在文件semphr.h中有如下定義:
#define xSemaphoreTake( xSemaphore, xBlockTime ) \ xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ),\ NULL,\ ( xBlockTime ), \ pdFALSE )
獲取信號量的過程其實就是讀取隊列的過程,只是這里並不是為了讀取隊列中的消息。xQueueGenericReceive(),如果隊列為空並且阻塞時間為0的話就立即返回errQUEUE_EMPTY,表示隊列空。如果隊列為空並且阻塞時間不為0的話就將任務添加到延時列表中。如果隊列不為空的話就從隊列中讀取數據(獲取信號量不執行這一步),數據讀取完成以后還需要將隊列結構體變量uxMessageWaiting減一,然后解除某些因為入隊而阻塞的任務,最后返回pdPASS表示出隊成功。互斥信號量涉及到優先級繼承,處理方式不同。
2. 函數 xSemaphoreTaskFromISR()
此函數用於在中斷服務函數中獲取信號量,此函數用於獲取二值信號量和計數型信號量,絕對不能使用此函數來獲取互斥信號量!此函數是一個宏,真正執行的是函數xQueueReceiveFromISR(),此函數原型如下:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken )
參數:
xSemaphore:要獲取的信號量句柄。
pxHigherPriorityTaskWoken:標記退出此函數以后是否進行任務切換,這個變量的值由這個函數來設置,用戶不進行設置,用戶只需要提供一個變量來保存這個值就行了。當此值為判斷TRUE的時候在退出中斷服務函數之前一定要進行一次任務切換。
返回值:
pdPASS:獲取信號量成功。
pdFALSE:獲取信號量失敗。
在中斷中獲取信號量真正使用的是函數xQueueReceiveFromISR(),這個函數就是中斷級出隊函數。當隊列不為空的時候就拷貝隊列中的數據(用於信號量的時候不需要這一步),然后將隊列結構體中的成員變量uxMessageWaiting減一,如果有任務因為入隊而阻塞的話就解除阻塞態,當解除阻塞的任務擁有更高優先級的話就將參數pxHigherPriorityTaskWoken設置為pdTRUE,最后返回pdPASS表示出隊成功。如果隊列為空的話就直接返回pdFAIL表示出隊失敗。
二值信號量操作實驗
二值信號量的使用就是同步,完成任務與任務或中斷與任務之間的同步。大多數情況下都是中斷與任務之間的同步。本實驗室學習使用二值信號量來完成中斷與任務之間的同步。任務與任務之間的同步。
本實驗設計三個任務:start_task、task1_task、DataProcess_task。
start_task:用來創建其他2個任務。
task1_task:獲取按鍵鍵值,釋放信號量。用於任務與任務之間同步
DataProcess_task:
實驗中還創建了一個二值信號量BinarySemaphore,用於完成串口中斷和任務DataProcess_task之間的同步。
相關代碼:
#define START_TASK_PRIO 1 // 任務優先級 #define START_STK_SIZE 128 // 任務堆棧大小 TaskHandle_t StartTask_Handler; // 任務句柄 void start_task(void *pvParameters); // 任務函數 #define LED0_TASK_PRIO 2 // 任務優先級 #define LED0_STK_SIZE 50 // 任務堆棧大小 TaskHandle_t LED0Task_Handler; // 任務句柄 void led0_task(void *pvParameters); // 任務函數 #define DATAPROCESS_TASK_PRIO 3 // 任務優先級 #define DATAPROCESS_STK_SIZE 50 // 任務堆棧大小 TaskHandle_t DataProcessTask_Handler; // 任務句柄 void DataProcess_task(void *pvParameters); // 任務函數 SemaphoreHandle_t BinarySemaphore = NULL; // 二值信號量句柄
main函數
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4 delay_init(); //延時函數初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED KEY_Init(); // 初始化按鍵 //創建開始任務 xTaskCreate((TaskFunction_t )start_task, //任務函數 (const char* )"start_task", //任務名稱 (uint16_t )START_STK_SIZE, //任務堆棧大小 (void* )NULL, //傳遞給任務函數的參數 (UBaseType_t )START_TASK_PRIO, //任務優先級 (TaskHandle_t* )&StartTask_Handler); //任務句柄 vTaskStartScheduler(); //開啟任務調度 }
任務函數
//開始任務任務函數 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //進入臨界區 // 創建二值信號量 BinarySemaphore = xSemaphoreCreateBinary(); if(BinarySemaphore == NULL) { printf("Binary Sem Create Failed!\r\n"); } //創建LED0任務 xTaskCreate((TaskFunction_t )led0_task, (const char* )"led0_task", (uint16_t )LED0_STK_SIZE, (void* )NULL, (UBaseType_t )LED0_TASK_PRIO, (TaskHandle_t* )&LED0Task_Handler); //創建DataProcess任務 xTaskCreate((TaskFunction_t )DataProcess_task, (const char* )"DataProcess_task", (uint16_t )DATAPROCESS_STK_SIZE, (void* )NULL, (UBaseType_t )DATAPROCESS_TASK_PRIO, (TaskHandle_t* )&DataProcessTask_Handler); vTaskDelete(StartTask_Handler); //刪除開始任務 taskEXIT_CRITICAL(); //退出臨界區 } //LED0任務函數 void led0_task(void *pvParameters) { u8 key=0; while(1) { key = KEY_Scan(0); if((BinarySemaphore!=NULL) && (key==KEY1_PRES)) { xSemaphoreGive( BinarySemaphore ); // 發送信號量 任務與任務同步 } vTaskDelay(10); } } //DataProcess任務函數 void DataProcess_task(void *pvParameters) { u8 count = 0; BaseType_t xHigherPriorityTaskWoken; BaseType_t err; while(1) { count ++; if(BinarySemaphore!=NULL) { // err = xSemaphoreTake( BinarySemaphore, 1000 ); // 一直等待 任務與任務同步 // if(err == pdTRUE) // { // printf("KEY1_PRESS! count:%d\r\n",count); // } err = xSemaphoreTakeFromISR( BinarySemaphore, &xHigherPriorityTaskWoken ); // 中斷與任務同步 if(err == pdTRUE) { printf("recv: %s\r\n",USART_RX_BUF); memset(USART_RX_BUF,0,USART_REC_LEN); USART_RX_STA = 0; }else { vTaskDelay(10); } } } }
中斷服務函數:
extern SemaphoreHandle_t BinarySemaphore ; // 二值信號量句柄 void USART1_IRQHandler(void) //串口1中斷服務程序 { u8 Res; BaseType_t xHigherPriorityTaskWoken = pdFALSE; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中斷(接收到的數據必須是0x0d 0x0a結尾) { Res =USART_ReceiveData(USART1); //讀取接收到的數據 if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收錯誤,重新開始 else USART_RX_STA|=0x8000; //接收完成了 } else //還沒收到0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收數據錯誤,重新開始接收 } } } } if((BinarySemaphore!=NULL) && (USART_RX_STA&0x8000)) { xSemaphoreGiveFromISR( BinarySemaphore, &xHigherPriorityTaskWoken ); // 發送中斷二值信號量 中斷與任務同步 portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }
注意:串口中斷初始化優先級設置,必須在FreeRTOS能管理的優先級范圍內5~15。
計數型信號量
計數型信號量簡介
有些資料中也將計數型信號量叫做數值信號量,二值信號量相當於長度為1的隊列,那么計數型信號量就是長度大於1的隊列。同二值信號量一樣,用戶不需要關系隊列中存儲了什么數據,只需要關系隊列是否為空即可。計數型信號量通常用於如下兩個場合:
1. 事件計數
在這個場合中,每次事件發生的時候就在事件處理函數中釋放信號量(增加信號量計數值),其他任務會獲取信號量(信號量計數值減一,信號量值就是隊列結構體成員變量uxMessageWaiting)來處理事件。在這種場合中創建的計數型信號量的初始值為0。
2. 資源管理
在這個場合中,信號量的值代表當前資源的可用數量,比如停車場當前剩余的停車位數量。一個任務要想獲取資源的使用權,首先必須獲取信號量,信號量獲取成功以后信號量值就會減一。當信號量值為0的時候說明沒有資源了。當一個任務使用完資源以后一定要釋放信號量,釋放信號量以后信號量就會加一。在這個場合中創建的計數型信號量初始值應該是資源的數量,比如停車場移動有100個停車位,那么創建信號量的時候信號量值就應該是資源的數量。比如停車場一共有100個停車位,那么創建信號量的時候信號量值就應該初始化為100。
創建計數型信號量
FreeRTOS提供了兩個計數型信號量創建函數:
函數 | 描述 |
xSemaphoreCreateCounting() | 使用動態方法創建計數型信號量 |
xSemaphoreCreateCountingStatic() | 使用靜態方法創建計數型信號量 |
1. 函數 xSemaphoreCreateCounting()
此函數用於創建一個計數型信號量,所需要的內存通過動態內存管理方法分配。此函數本質是一個宏,
真正完成信號量創建的函數是 xQueueCreateCountingSemaphore(),此函數原型如下:
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount )
參數:
uxMaxCount:計數信號量最大計數值,當信號量值等於此值得時候釋放信號量就會失敗。
uxInitialCount:計數信號量初始值。
返回值:
NULL:計數型信號量創建失敗。
其他值:計數型信號量創建成功,返回計數型信號量句柄。
2. 函數 xSemaphoreCreateCountingStatic()
此函數也是用來創建計數型信號量的,使用此函數創建計數型信號量的時候所需要的內存由用戶分配。此函數也是一個宏,真正執行的是函數是 xQueueCreateCountingSemaphoreStatic(),函數原型如下:
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer )
參數:
uxMaxCount:計數型信號量最大計數值,當信號量值等於此值得時候釋放信號量就會失敗。
uxInitialCount:計數信號量初始值。
pxSemaphoreBuffer:指向一個StaticSemaphore_t類型的變量,用來保存信號量結構體。
返回值:
NULL:計數型信號量創建失敗。
其他值:計數型信號量創建成功,返回計數型信號量句柄。
計數型信號量創建過程分析
這里只分析動態創建計數型信號量函數 xSemaphoreCreateCounting(),此函數是個宏,定義如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \ xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) ) #endif
可以看出,真正干事的是函數 xQueueCreateCountingSemaphore(),此函數在文件queue.c中有如下定義:
1 QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount ) 2 { 3 QueueHandle_t xHandle; 4 5 configASSERT( uxMaxCount != 0 ); 6 configASSERT( uxInitialCount <= uxMaxCount ); 7 8 xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE ); 9 10 if( xHandle != NULL ) 11 { 12 ( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount; 13 14 traceCREATE_COUNTING_SEMAPHORE(); 15 } 16 else 17 { 18 traceCREATE_COUNTING_SEMAPHORE_FAILED(); 19 } 20 21 return xHandle; 22 }
第8行:計數型信號量也是在隊列的基礎上實現的,所以需要調用函數 xQueueGenericCreate()創建一個隊列,隊列的長度為uxMaxCount,隊列項長度為queueSEMAPHORE_QUEUE_ITEM_LENGTH(此宏為0),隊列的類型為 queueQUEUE_TYPE_COUNTING_SEMAPHORE,表示是個計數型信號量。
第12行:隊列結構體成員變量uxMessageWaiting用於計數型信號量的計數,根據計數型信號量的初始值來設置uxMessageWaiting。
計數型信號量的釋放和獲取與二值信號量相同。
實驗
計數型信號量一般用於事件計數和資源管理,計數型信號量在這個場景中的使用方法基本一樣。這是使用計數型信號量在事件計數中的使用。
本實驗設計三個任務:start_task、task1_task、DataProcess_task。
start_task:用來創建其他2個任務
task1_task:獲取按鍵KEY1后就釋放信號量
DataProcess_task:獲取信號量,並打印信號量的值。
任務設置
//任務優先級 #define START_TASK_PRIO 1 //任務堆棧大小 #define START_STK_SIZE 128 //任務句柄 TaskHandle_t StartTask_Handler; //任務函數 void start_task(void *pvParameters); //任務優先級 #define TASK1_TASK_PRIO 2 //任務堆棧大小 #define TASK1_STK_SIZE 50 //任務句柄 TaskHandle_t Task1Task_Handler; //任務函數 void task1_task(void *pvParameters); //任務優先級 #define DATAPROCESS_TASK_PRIO 3 //任務堆棧大小 #define DATAPROCESS_STK_SIZE 50 //任務句柄 TaskHandle_t DataProcessTask_Handler; //任務函數 void DataProcess_task(void *pvParameters);
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4 delay_init(); //延時函數初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED KEY_Init(); // 初始化按鍵 SemaphoreCount = xSemaphoreCreateCounting( 255, 0 ); // 創建計數型信號量 if(SemaphoreCount == NULL) { printf(" SemaphoreCount Created Failed!\r\n "); } //創建開始任務 xTaskCreate((TaskFunction_t )start_task, //任務函數 (const char* )"start_task", //任務名稱 (uint16_t )START_STK_SIZE, //任務堆棧大小 (void* )NULL, //傳遞給任務函數的參數 (UBaseType_t )START_TASK_PRIO, //任務優先級 (TaskHandle_t* )&StartTask_Handler); //任務句柄 vTaskStartScheduler(); //開啟任務調度 }
任務函數
//開始任務任務函數 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //進入臨界區 //創建LED0任務 xTaskCreate((TaskFunction_t )task1_task, (const char* )"task1_task", (uint16_t )TASK1_STK_SIZE, (void* )NULL, (UBaseType_t )TASK1_TASK_PRIO, (TaskHandle_t* )&Task1Task_Handler); //創建LED1任務 xTaskCreate((TaskFunction_t )DataProcess_task, (const char* )"DataProcess_task", (uint16_t )DATAPROCESS_STK_SIZE, (void* )NULL, (UBaseType_t )DATAPROCESS_TASK_PRIO, (TaskHandle_t* )&DataProcessTask_Handler); vTaskDelete(StartTask_Handler); //刪除開始任務 taskEXIT_CRITICAL(); //退出臨界區 } // TASK1任務函數 void task1_task(void *pvParameters) { u8 key = 0; while(1) { key = KEY_Scan(0); if((SemaphoreCount != NULL) && (key)) { if(key == KEY1_PRES) { xSemaphoreGive(SemaphoreCount); // 釋放信號量 LED0=~LED0; } }else{ vTaskDelay(10); } } } //DataProcess任務函數 void DataProcess_task(void *pvParameters) { UBaseType_t countVal = 0; while(1) { if(SemaphoreCount != NULL) { xSemaphoreTake(SemaphoreCount,portMAX_DELAY); // 死等 countVal = uxSemaphoreGetCount(SemaphoreCount); printf("countVal = %d\r\n",(uint8_t)countVal); }else{ vTaskDelay(10); } LED1=~LED1; vTaskDelay(1000); } }
注意:但第一次按鍵時,輸出的countVal=0。因為在xSemaphoreTask()中有uxMessageWaiting-1的操作。
使用計數型信號量,首先創建。調用函數xSemaphoreCreateCounting()創建一個計數型信號量CountSemaphore。計數型信號量最大值設置為255,由於本實驗中計數型信號量用於事件計數,所以信號量的初始值設置為255,如果計數型信號量用於資源管理的話,那么事件計數型信號量的初始值就應該根據資源的實際數量來設置。
如果按鍵KEY按下,表示事件發生了,就調用函數xSemaphoreGive()釋放信號量SemaphoreCount.
調用函數uxSemaphoreGetCount()獲取信號量SemaphoreCount的信號量值。釋放信號量的話信號量值就會加一。函數uxSemaphoreGetCount()是來獲取信號量值得,這個函數是個宏,是對函數uxQueueMessageWaiting()的一個簡單封裝,其實就是返回隊列結構體成員變量uxMessageWaiting的值。
優先級翻轉
在使用二值信號量的時候會遇到很常見的一個問題:優先級翻轉。優先級翻轉在可剝奪內核中是非常常見的,在實時系統中不允許出現這種現象,這樣會破壞任務的預期順序,可能會導致嚴重的后果。下圖是一個優先級翻轉的例子:
(1)任務H和任務M處於掛起狀態,等待某一事件的發生,任務L正在運行。
(2)某一時刻任務L想要訪問共享資源,在此之前它必須先獲得對應資源的信號量。
(3)任務L獲得信號量並開始使用該共享資源。
(4)由於任務H優先級高,它等待的時間發生后便剝奪了任務L的CPU使用權。
(5)任務H開始運行。
(6)任務H運行過程中也要使用任務L正在使用着的資源,由於該資源的信號量還被L占用着,任務H只能進入掛起狀態,等待任務L釋放該信號量。
(7)任務L繼續運行。
(8)由於任務M優先級高於任務L,當任務M等待的事件發生后,任務M剝奪了任務L的CPU使用權。
(9)任務M處理該處理的事。
(10)任務M執行完畢后,將CPU使用權歸還給任務L。
(11)任務L繼續執行。
(12)最終任務L完成所有的工作並釋放了信號量,到此為止,由於實時內核知道有個高優先級的任務正在等待這個信號量,故內核做任務切換。
(13)任務H得到該信號量並接着運行。
在這種情況下,任務H的優先級實際上降到了任務L的優先級水平。因為任務H要一直等待任務L釋放其占用的那個共享資源。由於任務M剝奪了任務L的CPU使用權,使得任務H的情況更加惡化,這樣就相當於任務M的優先級高於任務H,導致優先級翻轉。
優先級翻轉實驗
在使用二值信號量的時候會存在優先級翻轉的問題,本實驗模擬實現優先級翻轉,觀察優先級翻轉對搶占式內核的影響。
設計4個任務:start_task、high_task、middle_task、low_task,這四個任務的功能如下:
start_task:用來創建其他3個任務。
hight_task:高優先級任務,會獲取二值信號量,獲取成功以后會進行相應的處理,處理完成以后會釋放二值信號量。
middle_task:中等優先級任務,一個簡單的應用任務。
low_task:低優先級任務,和高優先級任務一樣,會獲取二值信號量,獲取成功以后會進行相應的處理,不過不同之處在於低優先級任務占用二值信號量的時間要久一點(軟件模擬占用)。
創建一個二值信號量BinarySemaphore,高優先級和低優先級這兩個任務會使用這個二值信號量。
任務設置:
//任務優先級 #define START_TASK_PRIO 1 //任務堆棧大小 #define START_STK_SIZE 128 //任務句柄 TaskHandle_t StartTask_Handler; //任務函數 void start_task(void *pvParameters); //任務優先級 #define HIGH_TASK_PRIO 4 //任務堆棧大小 #define HIGH_STK_SIZE 50 //任務句柄 TaskHandle_t HIGHTask_Handler; //任務函數 void high_task(void *pvParameters); //任務優先級 #define MIDDLE_TASK_PRIO 3 //任務堆棧大小 #define MIDDLE_STK_SIZE 50 //任務句柄 TaskHandle_t MIDDLETask_Handler; //任務函數 void middle_task(void *pvParameters); //任務優先級 #define LOW_TASK_PRIO 2 //任務堆棧大小 #define LOW_STK_SIZE 50 //任務句柄 TaskHandle_t LOWTask_Handler; //任務函數 void low_task(void *pvParameters); SemaphoreHandle_t BinarySemaphore = NULL; // 二值信號量
main() 函數
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4 delay_init(); //延時函數初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED //創建開始任務 xTaskCreate((TaskFunction_t )start_task, //任務函數 (const char* )"start_task", //任務名稱 (uint16_t )START_STK_SIZE, //任務堆棧大小 (void* )NULL, //傳遞給任務函數的參數 (UBaseType_t )START_TASK_PRIO, //任務優先級 (TaskHandle_t* )&StartTask_Handler); //任務句柄 vTaskStartScheduler(); //開啟任務調度 }
任務函數:
//開始任務任務函數 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //進入臨界區 BinarySemaphore = xSemaphoreCreateBinary(); // 創建二值信號量 if(BinarySemaphore == NULL) { printf("BinarySemaphore Created Failed! \r\n"); }else { xSemaphoreGive(BinarySemaphore); // 釋放二值信號量 } //創建HIGH任務 xTaskCreate((TaskFunction_t )high_task, (const char* )"high_task", (uint16_t )HIGH_STK_SIZE, (void* )NULL, (UBaseType_t )HIGH_TASK_PRIO, (TaskHandle_t* )&HIGHTask_Handler); //創建MIDDLE任務 xTaskCreate((TaskFunction_t )middle_task, (const char* )"middle_task", (uint16_t )MIDDLE_STK_SIZE, (void* )NULL, (UBaseType_t )MIDDLE_TASK_PRIO, (TaskHandle_t* )&MIDDLETask_Handler); //創建LOW任務 xTaskCreate((TaskFunction_t )low_task, (const char* )"low_task", (uint16_t )LOW_STK_SIZE, (void* )NULL, (UBaseType_t )LOW_TASK_PRIO, (TaskHandle_t* )&LOWTask_Handler); vTaskDelete(StartTask_Handler); //刪除開始任務 taskEXIT_CRITICAL(); //退出臨界區 } //HIGH任務函數 void high_task(void *pvParameters) { while(1) { printf("high_task pending! \t\r\n"); // 任務掛起 xSemaphoreTake(BinarySemaphore,portMAX_DELAY); // 獲取二值信號量 printf("high_task running! \t\r\n"); // 任務運行 xSemaphoreGive(BinarySemaphore); // 釋放二值信號量 vTaskDelay(500); } } //MIDDLE任務函數 void middle_task(void *pvParameters) { while(1) { printf("middle_task running! \t\r\n"); vTaskDelay(1000); } } //LOW任務函數 void low_task(void *pvParameters) { u32 i = 0; while(1) { xSemaphoreTake(BinarySemaphore,portMAX_DELAY); // 獲取二值信號量 printf("low_task running! \t\r\n"); // 任務運行 for(i=0;i<2500000;i++) { taskYIELD(); // 發起任務調度 } xSemaphoreGive(BinarySemaphore); // 釋放二值信號量 vTaskDelay(1000); } }
打印輸出結果:
high_task pending! 和 high_task running!之間出現 middle_task running!
高優先級任務執行過程中,中等優先級多次運行,發生優先級翻轉。
當一個地優先級任務和一個高優先級任務同時使用同一個信號量,而系統中還有其他中等優先級任務時。如果低優先級獲得了信號量,那么高優先級任務就會處於等待狀態,但是,中等優先級任務可以打斷低優先級任務而先於高優先級任務運行(此時高優先級的任務在等待信號量,所以不能運行),這就出現了優先級翻轉現象。
優先級翻轉問題很嚴重,可以使用互斥信號量。
互斥信號量
互斥信號量簡介
互斥信號量其實就是一個擁有優先級繼承的二值信號量,在同步的應用中(任務與任務或中斷與任務之間的同步)二值信號量最合適。互斥信號量適合用於那些需要互斥訪問的應用中。在互斥訪問中互斥信號量相當於一個鑰匙,當任務想要使用資源的時候就必須先獲得這個鑰匙,但使用完資源以后就必須歸還這個鑰匙,這樣其他的任務就可以拿着這個鑰匙去使用資源。
互斥信號量使用和二值信號量具有相同的API操作函數,所以互斥信號量也可以設置阻塞時間,不同於二值信號量的事互斥信號量具有優先級繼承的特性。當一個互斥信號量正在別一個低優先級的任務使用,而此時有個高優先級的任務也嘗試獲取這個互斥信號量的話就會被阻塞。不過這個高優先級任務會將低優先級的任務的優先級提升到與自己相同的優先級,這個過程就是優先級繼承。優先級繼承盡可能地降低了高優先級任務處於阻塞態的時間,並且將已出現的“優先級翻轉”的影響降到最低。
優先級繼承並不能完全消除優先級翻轉,它只是盡可能地降低優先級翻轉帶來的影響。硬實時應用應該在設計之初就要避免優先級翻轉發生。互斥信號量不能用於中斷服務函數中,原因如下:
1. 互斥信號量有優先級繼承的機制,所以只能用在任務中,不能用於中斷服務函數。
2. 中斷服務函數中不能因為要等待互斥信號量而設置阻塞時間而進入阻塞態
創建互斥信號量
FreeRTOS提供了兩個互斥信號量創建函數:
函數 | 描述 |
xSemaphoreCreateMutex() | 使用動態方法創建互斥信號量 |
xSemaphoreCreateMutexStatic() | 使用靜態方法創建互斥信號量 |
1. 函數 xSemaphoreCreateMutex()
此函數用於創建一個互斥信號量,所需要的內存通過動態內存管理方法分配。此函數本質是一個宏,真正完成信號量創建的是函數xQueueCreateMutex(),此函數原型如下:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
參數:
無。
返回值:
NULL:互斥信號量創建失敗。
其他值:創建成功的互斥信號量的句柄。
2. 函數 xSemaphoreCreateMutexStatic()
此函數也是創建互斥信號量的,只不過使用次函數創建互斥信號量的話,信號量所需要的RAM需要由用戶來分配,此函數是個宏,具體創建過程是通過函數 xQueueCreateMutexStatic() 來完成的,函數原型如下:
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
參數:
pxMutexBuffer:此參數指向一個StaticSemaphore_t類型的變量,用來保存信號量結構體。
返回值:
NULL:互斥信號量創建失敗。
其他值:創建成功的互斥信號量的句柄。
互斥信號量創建過程分析
這里只分析動態創建互斥信號量函數 xSemaphoreCreateMutex(),此函數是個宏,定義如下:
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
可以看出,真正執行的是函數xQueueCreateMutex(),此函數在queue.c中有如下定義:
1 QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ) 2 { 3 Queue_t *pxNewQueue; 4 const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0; 5 6 pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType ); 7 prvInitialiseMutex( pxNewQueue ); 8 9 return pxNewQueue; 10 }
第6行:調用函數xQueueGenericCreate()創建一個隊列,隊列長度為1,隊列項長度為0,隊列類型為參數ucQueueType。由於本函數是創建互斥信號量,所以參數ucQueueType為queueQUEUE_TYPE_MUTEX。
第7行:調用函數prvInitialiseMutex()初始化互斥信號量。
函數prvInitialiseMutex()代碼如下:
static void prvInitialiseMutex( Queue_t *pxNewQueue ) { if( pxNewQueue != NULL ) { /* The queue create function will set all the queue structure members correctly for a generic queue, but this function is creating a mutex. Overwrite those members that need to be set differently - in particular the information required for priority inheritance. */ pxNewQueue->pxMutexHolder = NULL; pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX; /* In case this is a recursive mutex. */ pxNewQueue->u.uxRecursiveCallCount = 0; traceCREATE_MUTEX( pxNewQueue ); /* Start with the semaphore in the expected state. */ ( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK ); } else { traceCREATE_MUTEX_FAILED(); } }
實驗
將上面優先級翻轉的程序中的二值信號量改為互斥信號量
任務設置:
//任務優先級 #define START_TASK_PRIO 1 //任務堆棧大小 #define START_STK_SIZE 128 //任務句柄 TaskHandle_t StartTask_Handler; //任務函數 void start_task(void *pvParameters); //任務優先級 #define HIGH_TASK_PRIO 4 //任務堆棧大小 #define HIGH_STK_SIZE 50 //任務句柄 TaskHandle_t HIGHTask_Handler; //任務函數 void high_task(void *pvParameters); //任務優先級 #define MIDDLE_TASK_PRIO 3 //任務堆棧大小 #define MIDDLE_STK_SIZE 50 //任務句柄 TaskHandle_t MIDDLETask_Handler; //任務函數 void middle_task(void *pvParameters); //任務優先級 #define LOW_TASK_PRIO 2 //任務堆棧大小 #define LOW_STK_SIZE 50 //任務句柄 TaskHandle_t LOWTask_Handler; //任務函數 void low_task(void *pvParameters); SemaphoreHandle_t MutexSemaphore = NULL; // 互斥信號量句柄
main() 函數:
int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//設置系統中斷優先級分組4 delay_init(); //延時函數初始化 uart_init(115200); //初始化串口 LED_Init(); //初始化LED //創建開始任務 xTaskCreate((TaskFunction_t )start_task, //任務函數 (const char* )"start_task", //任務名稱 (uint16_t )START_STK_SIZE, //任務堆棧大小 (void* )NULL, //傳遞給任務函數的參數 (UBaseType_t )START_TASK_PRIO, //任務優先級 (TaskHandle_t* )&StartTask_Handler); //任務句柄 vTaskStartScheduler(); //開啟任務調度 }
任務函數:
//開始任務任務函數 void start_task(void *pvParameters) { taskENTER_CRITICAL(); //進入臨界區 MutexSemaphore = xSemaphoreCreateMutex(); // 創建互斥信號量 if(MutexSemaphore == NULL) { printf("BinarySemaphore Created Failed! \r\n"); }else { xSemaphoreGive(MutexSemaphore); // 釋放二值信號量 } //創建HIGH任務 xTaskCreate((TaskFunction_t )high_task, (const char* )"high_task", (uint16_t )HIGH_STK_SIZE, (void* )NULL, (UBaseType_t )HIGH_TASK_PRIO, (TaskHandle_t* )&HIGHTask_Handler); //創建MIDDLE任務 xTaskCreate((TaskFunction_t )middle_task, (const char* )"middle_task", (uint16_t )MIDDLE_STK_SIZE, (void* )NULL, (UBaseType_t )MIDDLE_TASK_PRIO, (TaskHandle_t* )&MIDDLETask_Handler); //創建LOW任務 xTaskCreate((TaskFunction_t )low_task, (const char* )"low_task", (uint16_t )LOW_STK_SIZE, (void* )NULL, (UBaseType_t )LOW_TASK_PRIO, (TaskHandle_t* )&LOWTask_Handler); vTaskDelete(StartTask_Handler); //刪除開始任務 taskEXIT_CRITICAL(); //退出臨界區 } //HIGH任務函數 void high_task(void *pvParameters) { while(1) { vTaskDelay(500); printf("high_task pending! \t\r\n"); // 任務掛起 xSemaphoreTake(MutexSemaphore,portMAX_DELAY); // 獲取二值信號量 printf("high_task running! \t\r\n"); // 任務運行 xSemaphoreGive(MutexSemaphore); // 釋放二值信號量 vTaskDelay(500); } } //MIDDLE任務函數 void middle_task(void *pvParameters) { while(1) { printf("middle_task running! \t\r\n"); vTaskDelay(1000); } } //LOW任務函數 void low_task(void *pvParameters) { u32 i = 0; while(1) { xSemaphoreTake(MutexSemaphore,portMAX_DELAY); // 獲取二值信號量 printf("low_task running! \t\r\n"); // 任務運行 for(i=0;i<2500000;i++) { taskYIELD(); // 發起任務調度 } xSemaphoreGive(MutexSemaphore); // 釋放二值信號量 vTaskDelay(1000); } }
實驗現象:
在high_task pending!和 high_task_running!之間沒有其他低優先級的任務運行。