FreeRTOS進階之任務通知示例完全解析
前言
在FreeRTOS版本V8.2.0中推出了全新的功能:任務通知。在大多數(shù)情況下,任務通知可以替代二進制信號量、計數(shù)信號量、事件組,可以替代長度為1的隊列(可以保存一個32位整數(shù)或指針值),并且任務通知速度更快、使用的RAM更少!我在FreeRTOS任務通知一文中介紹了任務通知如何使用以及局限性,今天我們將分析任務通知的實現(xiàn)源碼,看一下任務通知是如何做到效率與RAM消耗雙贏的。
在FreeRTOS信號量分析一文中我們已經知道,F(xiàn)reeRTOS的信號量是使用隊列機制實現(xiàn)的,數(shù)據(jù)結構也完全是隊列的那一套。而任務通知則不同,它的數(shù)據(jù)結構嵌在任務TCB(任務控制塊,見FreeRTOS進階之任務創(chuàng)建中的,并且數(shù)據(jù)結構十分簡單,涉及到任務TCB的兩個字段,我們將它單獨列出來:
volatile uint32_t ulNotifiedValue; /*任務通知值*/ volatile uint8_t ucNotifyState; /*任務通知狀態(tài),標識任務是否在等待通知等*/
這兩個字段占用5字節(jié)RAM(本文都是在32位系統(tǒng)下討論),而一個隊列數(shù)據(jù)結構至少占用76字節(jié)RAM!這不是同一數(shù)量級的,所以任務通知在RAM消耗上完勝。
在分析隊列和信號量的文章中,我們知道在使用隊列、信號量前,必須先創(chuàng)建隊列和信號量,目的是為了創(chuàng)建隊列數(shù)據(jù)結構。比如使用API函數(shù)xQueueCreate()創(chuàng)建隊列,用API函數(shù)xSemaphoreCreateBinary()創(chuàng)建二進制信號量等等。再來看任務通知,由于任務通知的數(shù)據(jù)結構包含在任務TCB中,只要任務存在,任務通知數(shù)據(jù)結構就已經創(chuàng)建完畢,可以直接使用!在易用性上,任務通知再次獲勝。
要想了解任務通知在性能上占優(yōu)的原因,就要分析源代碼了。
只有任務可以等待通知,中斷服務函數(shù)中不可以。如果等待的通知無效,任務會進入阻塞狀態(tài),我們可以將等待通知的任務看作是消費者;其它任務和中斷可以向等待通知的任務發(fā)送通知,發(fā)送通知的任務和中斷服務函數(shù)可以認為是生產者。處于阻塞的消費者得到通知后會再次進入就緒態(tài)。
任務通知API函數(shù)主要有兩類,一類發(fā)送通知,一類等待通知。發(fā)送通知API函數(shù)可以用于任務和中斷服務函數(shù),等待通知API函數(shù)只能用在任務中。
1.發(fā)送通知
我們先看一下發(fā)送通知API函數(shù)。這類函數(shù)比較多,有6個。但仔細分析會發(fā)現(xiàn)它們只能完成3種操作,每種操作有兩個API函數(shù),分別為帶中斷保護版本和不帶中斷保護版本。FreeRTOS將API細分為帶中斷保護版本和不帶中斷保護版本是為了節(jié)省中斷服務程序處理時間,提升性能。
和信號量類似,大多數(shù)發(fā)送通知API接口也是由宏實現(xiàn)的,如表1-1所示。
表1-1:發(fā)送通知API函數(shù)與實際調用函數(shù)列表
1.1 xTaskGenericNotify()
不帶中斷保護的發(fā)送通知API函數(shù)實際都是調用函數(shù)xTaskGenericNotify()實現(xiàn)的,我們看一下這個函數(shù)原型:
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue )
xTaskToNotify
:被通知的任務句柄。
ulValue
:更新的通知值
eAction
:枚舉類型,指明更新通知值的方法,枚舉變量成員以及作用見表1-2所示。
pulPreviousNotifyValue
:回傳未被更新的任務通知值。如果不需要回傳未被更新的任務通知值,這里設置為NULL。
表1-2:eNotifyAction枚舉成員以及作用
與入隊操作相比較,發(fā)送通知API函數(shù)顯得非常簡單,整理后的源碼如下所示:
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue ) { TCB_t * pxTCB; BaseType_t xReturn = pdPASS; uint8_t ucOriginalNotifyState; configASSERT( xTaskToNotify ); pxTCB = ( TCB_t * ) xTaskToNotify; taskENTER_CRITICAL(); { if( pulPreviousNotificationValue != NULL ) { /* 回傳更新前的通知值*/ *pulPreviousNotificationValue = pxTCB->ulNotifiedValue; } ucOriginalNotifyState = pxTCB->ucNotifyState; pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; switch( eAction ) { case eSetBits : pxTCB->ulNotifiedValue |= ulValue; break; case eIncrement : ( pxTCB->ulNotifiedValue )++; break; case eSetValueWithOverwrite : pxTCB->ulNotifiedValue = ulValue; break; case eSetValueWithoutOverwrite : if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) { pxTCB->ulNotifiedValue = ulValue; } else { /* 上次的通知值還未取走,本次通知值丟棄 */ xReturn = pdFAIL; } break; case eNoAction: /* 不需要更新通知值*/ break; } traceTASK_NOTIFY(); /* 如果被通知的任務因為等待通知而阻塞,現(xiàn)在將它解除阻塞 */ if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { /* 如果被通知的任務優(yōu)先級高于當前任務,則觸發(fā)PendSV中斷,退出臨界區(qū)后進行上下文切換T*/ taskYIELD_IF_USING_PREEMPTION(); } } } taskEXIT_CRITICAL(); return xReturn; }
函數(shù)的功能可以概括為:按照指定的方法更新通知值,如果被通知的任務處于阻塞狀態(tài),則將它解除阻塞,解除阻塞任務的優(yōu)先級如果大于當前任務的優(yōu)先級,則觸發(fā)一次任務切換。
與釋放信號量API函數(shù)相比,本函數(shù)少了很多調用子函數(shù)開銷、少了很多判斷、少了對事件列表的操作等等,確實是比釋放信號量的實現(xiàn)要簡潔的多。這也是有原因的,因為任務通知有它自己的局限性,并不能完全代替信號量。比如一個任務只能阻塞到一個通知上,如想要實現(xiàn)多個任務阻塞到同一個事件上,只能使用信號量了。也正是因為這種局限性,使得任務通知實現(xiàn)起來簡單高效,并且大多數(shù)情況下,任務通知的方法就已經能解決問題了。
1.2 vTaskNotifyGiveFromISR()
這個API函數(shù)是vTaskNotifyGive()的帶中斷保護版本,是專門設計用來在某些情況下代替二進制信號量和計數(shù)信號量的。函數(shù)也很簡單,我們直接看源碼,源碼經過整理和注釋,以方便理解。
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken ) { TCB_t * pxTCB; uint8_t ucOriginalNotifyState; UBaseType_t uxSavedInterruptStatus; pxTCB = ( TCB_t * ) xTaskToNotify; uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { ucOriginalNotifyState = pxTCB->ucNotifyState; pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; /* 通知值加1,相當于釋放了一個信號量 */ ( pxTCB->ulNotifiedValue )++; /* 如果目標任務因為等待通知而阻塞,現(xiàn)在將它解除阻塞*/ if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { /* 如果調度器正常,將任務放入就緒列表,否則放入掛起就緒列表 */ if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); } else { vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) ); } if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { /* 如果解除阻塞的任務優(yōu)先級大于當前任務優(yōu)先級,則設置上下文切換標識,等退出函數(shù)后手動切換上下文,或者在系統(tǒng)節(jié)拍中斷服務程序中自動切換上下文*/ if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; /* 設置手動切換標志 */ } else { xYieldPending = pdTRUE; /* 設置自動切換標志 */ } } } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); }
1.3 xTaskGenericNotifyFromISR()
如表1-1所示,帶中斷保護版本的API函數(shù)xTaskNotifyFromISR()和xTaskNotifyAndQueryFromISR()都是宏定義,真正被調用的函數(shù)為xTaskGenericNotifyFromISR()。
這個函數(shù)用于在中斷在中發(fā)送通知,與不帶中斷保護的API函數(shù)xTaskGenericNotify()非常相似,只是增加了一些中斷保護措施,我們直接看源碼。通用源碼經過整理和注釋,以方便理解。
BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue, BaseType_t *pxHigherPriorityTaskWoken ) { TCB_t * pxTCB; uint8_t ucOriginalNotifyState; BaseType_t xReturn = pdPASS; UBaseType_t uxSavedInterruptStatus; pxTCB = ( TCB_t * ) xTaskToNotify; uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if( pulPreviousNotificationValue != NULL ) { /* 回傳更新前的通知值 */ *pulPreviousNotificationValue = pxTCB->ulNotifiedValue; } ucOriginalNotifyState = pxTCB->ucNotifyState; pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; /* 根據(jù)參數(shù)設置通知值 */ switch( eAction ) { case eSetBits : pxTCB->ulNotifiedValue |= ulValue; break; case eIncrement : ( pxTCB->ulNotifiedValue )++; break; case eSetValueWithOverwrite : pxTCB->ulNotifiedValue = ulValue; break; case eSetValueWithoutOverwrite : if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED ) { pxTCB->ulNotifiedValue = ulValue; } else { /* 上次的通知值還未取走,本次通知值丟棄 */ xReturn = pdFAIL; } break; case eNoAction : /* 不需要更新通知值*/ break; } traceTASK_NOTIFY_FROM_ISR(); /* 如果被通知的任務因為等待通知而阻塞,現(xiàn)在將它解除阻塞 */ if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) { /* 如果調度器正常,將任務放入就緒列表,否則放入掛起就緒列表 */ if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { ( void ) uxListRemove( &( pxTCB->xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); } else { vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) ); } if( pxTCB->uxPriority > pxCurrentTCB->uxPriority ) { /* 如果解除阻塞的任務優(yōu)先級大于當前任務優(yōu)先級,則設置上下文切換標識,等退出函數(shù)后手動切換上下文,或者在系統(tǒng)節(jié)拍中斷服務程序中自動切換上下文*/ if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; /* 設置手動切換標志 */ } else { xYieldPending = pdTRUE; /* 設置自動切換標志 */ } } } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn; }
2.等待通知
等待通知API函數(shù)只能用在任務中,沒有帶中斷保護版本,因此只有兩個API函數(shù):
ulTaskNotifyTake()和xTaskNotifyWait ()
前者是為代替二進制信號量和計數(shù)信號量而專門設計的,它和發(fā)送通知API函數(shù)xTaskNotifyGive()、vTaskNotifyGiveFromISR()配合使用;
后者是全功能版的等待通知,可以根據(jù)不同的參數(shù)實現(xiàn)輕量級二進制信號量、計數(shù)信號量、事件組和長度為1的隊列。
等待通知API函數(shù)都帶有最大阻塞時間參數(shù),當任務因為等待通知而進入阻塞時,用來規(guī)定最大阻塞時間。
2.1 ulTaskNotifyTake()
這個API函數(shù)用于實現(xiàn)輕量級的二進制信號量和計數(shù)信號量,源碼如下所示。它有兩個參數(shù),如果第一個參數(shù)xClearCountOnExit設置為pdTRUE,則用來實現(xiàn)二進制信號量,函數(shù)退出時將通知值清零;如果第一個參數(shù)設置為pdFALSE,則用來實現(xiàn)計數(shù)信號量,函數(shù)退出時,將通知值減一。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ) { uint32_t ulReturn; taskENTER_CRITICAL(); { /* 僅當通知值為0,才進行阻塞操作*/ if( pxCurrentTCB->ulNotifiedValue == 0UL ) { /* 設置標志,表示當前任務等待一個通知*/ pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; if( xTicksToWait > ( TickType_t ) 0 ) { /* 將任務加入延時列表 */ prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE ); traceTASK_NOTIFY_TAKE_BLOCK(); /* 觸發(fā)PendSV中斷,等到退出臨界區(qū)時立即執(zhí)行任務切換 */ portYIELD_WITHIN_API(); } } } taskEXIT_CRITICAL(); /* 到這里說明其它任務或中斷向這個任務發(fā)送了通知,或者任務阻塞超時,現(xiàn)在繼續(xù)處理*/ taskENTER_CRITICAL(); { traceTASK_NOTIFY_TAKE(); ulReturn = pxCurrentTCB->ulNotifiedValue; if( ulReturn != 0UL ) { if( xClearCountOnExit != pdFALSE ) { pxCurrentTCB->ulNotifiedValue = 0UL; } else { pxCurrentTCB->ulNotifiedValue = ulReturn - 1; } } /* 設置標志,表示不需要等待通知 */ pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; } taskEXIT_CRITICAL(); return ulReturn; /* 如果返回值為0,說明是任務阻塞超時了 */ }
與獲取二進制信號量和獲取計數(shù)信號量函數(shù)相比,本函數(shù)少了很多調用子函數(shù)開銷、少了很多判斷、少了事件列表處理、少了隊列上鎖與解鎖處理等等,因此本函數(shù)相對效率很高。
2.2 xTaskNotifyWait()
這個函數(shù)用于實現(xiàn)全功能版的等待通知,根據(jù)參數(shù)的不同,可以靈活的用于實現(xiàn)輕量級的隊列、二進制信號量、計數(shù)信號量和事件組功能,函數(shù)原型為:
BaseType_t xTaskNotifyWait( uint32_tulBitsToClearOnEntry, uint32_tulBitsToClearOnExit, uint32_t*pulNotificationValue, TickType_txTicksToWait );
ulBitsToClearOnEntry
:在使用通知之前,先將任務的通知值與參數(shù)ulBitsToClearOnEntry的按位取反值按位與操作。設置參數(shù)ulBitsToClearOnEntry為0xFFFFFFFF(ULONG_MAX),表示清零任務通知值。
ulBitsToClearOnExit
:在函數(shù)xTaskNotifyWait()退出前,將任務的通知值與參數(shù)ulBitsToClearOnExit的按位取反值按位與操作。設置參數(shù)ulBitsToClearOnExit為0xFFFFFFFF(ULONG_MAX),表示清零任務通知值。
pulNotificationValue
:用于向外回傳任務的通知值。這個通知值在參數(shù)ulBitsToClearOnExit起作用前將通知值拷貝到*pulNotificationValue中。如果不需要返回任務的通知值,這里設置成NULL。
xTicksToWait
:因等待通知而進入阻塞狀態(tài)的最大時間。時間單位為系統(tǒng)節(jié)拍周期。宏pdMS_TO_TICKS用于將指定的毫秒時間轉化為相應的系統(tǒng)節(jié)拍數(shù)。
這個函數(shù)的實現(xiàn)和ulTaskNotifyTake()有很多相通之處,我將整個流程以注釋形式置于源碼中,源碼如下所示:
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait ) { BaseType_t xReturn; taskENTER_CRITICAL(); { /* 只有任務沒有等待通知,才會將任務阻塞 */ if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED ) { /* 使用任務通知值之前,先將參數(shù)ulBitsToClearOnEntryClear取反后與任務通知值位與.可以用這種方法在使用任務通知值之前,將通知值的某些或全部位清零 */ pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry; /* 設置任務狀態(tài)標識:等待通知 */ pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION; if( xTicksToWait > ( TickType_t ) 0 ) { /* 阻塞當前任務 */ prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE ); traceTASK_NOTIFY_WAIT_BLOCK(); /* 觸發(fā)PendSV中斷,等到退出臨界區(qū)后,執(zhí)行任務切換 */ portYIELD_WITHIN_API(); } } } taskEXIT_CRITICAL(); /* 到這里說明其它任務或中斷向這個任務發(fā)送了通知,或者任務阻塞超時,現(xiàn)在繼續(xù)處理*/ taskENTER_CRITICAL(); { traceTASK_NOTIFY_WAIT(); if( pulNotificationValue != NULL ) { /* 輸出當前通知值,通過指針參數(shù)傳遞*/ *pulNotificationValue = pxCurrentTCB->ulNotifiedValue; } /* 判斷是否是因為任務阻塞超時 */ if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION ) { /* 沒有收到任務通知,是阻塞超時 */ xReturn = pdFALSE; } else { /* 收到任務值,先將參數(shù)ulBitsToClearOnExit取反后與通知值位與,用于在退出函數(shù)前,將通知值的某些或者全部位清零. */ pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit; xReturn = pdTRUE; } /* 更改任務通知狀態(tài),解除任務通知等待 */ pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION; } taskEXIT_CRITICAL(); return xReturn; }
縱觀整個任務通知的實現(xiàn),可以發(fā)現(xiàn)它比隊列、信號量相比要簡單很多。它可以實現(xiàn)輕量級的隊列、二進制信號量、計數(shù)信號量和事件組,并且使用更方便、更節(jié)省RAM、更高效。FreeRTOS的作者做過測試,在同一平臺下,使用使用GCC編譯器、-o2優(yōu)化級別,相比使用信號量解除任務阻塞,使用任務通知可以快45%!這個性能的提升是巨大的。
我們分析過信號量的源碼,今天又分析了任務通知的源碼,這使得我們知道,之所以有這么大的性能提升,一方面緣于任務通知數(shù)據(jù)結構簡單、實現(xiàn)簡潔;
另一方面也跟FreeRTOS的信號量機制臃腫、效率低下有關。因為信號量的實現(xiàn)全部是使用隊列機制,并沒有為信號量做專門優(yōu)化。
此外,著重說明一下任務通知并不能完全代替隊列、二進制信號量、計數(shù)信號量和事件組,任務通知有自己的局限性,我們就以它的局限性來結束本文:
只能有一個任務接收通知事件。
接收通知的任務可以因為等待通知而進入阻塞狀態(tài),但是發(fā)送通知的任務即便不能立即完成發(fā)送通知,也不能進入阻塞狀態(tài)。
以上就是FreeRTOS進階任務通知示例分析的詳細內容,更多關于FreeRTOS任務通知分析的資料請關注腳本之家其它相關文章!
相關文章
FreeRTOS實時操作系統(tǒng)Cortex-M內核使用注意事項
這篇文章主要為大家介紹了FreeRTOS實時操作系統(tǒng)Cortex-M內核使用注意事項,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04FreeRTOS進階系統(tǒng)節(jié)拍時鐘示例的完全解析
這篇文章主要為大家介紹了FreeRTOS進階系統(tǒng)節(jié)拍時鐘示例的完全解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04