FreeRTOS内核详解(1) —— 临界段保护原理
什么是臨界段
臨界段用一句話概括就是一段在執行的時候不能被中斷的代碼段。 在 FreeRTOS 里面,這個臨界段最常出現的就是對全局變量的操作,由于不同任務間可以切換運行,當一個任務在訪問某個全局變量時,這時其他任務切入,改變了該全局變量,再回到上個任務時訪問該變量已經不是當時的值,這種情況可能會導致不可意料的后果。
FreeRTOS 源碼中就有多處臨界段的處理,跟 FreeRTOS 一樣,uCOS-II 和 uCOS-III 源碼中都是有臨界段的,而 RTX 的源碼中不存在臨界段。另外,除了 FreeRTOS 操作系統源碼所帶的臨界段以外,用戶寫應用的時候也有臨界段的問題,比如以下兩種:
讀取或者修改變量(尤其是任務間通信的全局變量)的代碼。
調用公共函數的代碼,特別是不可重入函數,如果多個任務都訪問這個函數,結果是可想而知的。
注意:由于臨界段中需要關閉中斷,因此會影響某些緊急中斷的處理速度,同時由于臨界段中關閉了任務調度,也會影響系統實時性,總之臨界段的執行時間要求越短越好。
Cortex-M 內核快速關中斷指令
為了快速地開關中斷, Cortex-M 內核專門設置了一條 CPS 指令,有 4 種用法
CPSID I ;PRIMASK=1 ;關中斷 CPSIE I ;PRIMASK=0 ;開中斷 CPSID F ;FAULTMASK=1 ;關異常 CPSIE F ;FAULTMASK=0 ;開異常PRIMASK 和 FAULTMAST 是 Cortex-M 內核 里面三個中斷屏蔽寄存器中的兩個,還有一個是 BASEPRI,有關這三個寄存器的詳細用法
在 FreeRTOS 中,對中斷的開和關是通過操作 BASEPRI 寄存器來實現的,即大于等于 BASEPRI 的值的中斷會被屏蔽,小于 BASEPRI 的值的中斷則不會被屏蔽,不受FreeRTOS 管理。用戶可以設置 BASEPRI 的值來選擇性的給一些非常緊急的中斷留一條后路。
進出臨界段源碼分析
進入和退出臨界段的宏在 task.h 中定義
#define taskENTER_CRITICAL() portENTER_CRITICAL() #define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR() #define taskEXIT_CRITICAL() portEXIT_CRITICAL() #define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )進入和退出臨界段的宏分中斷保護版本和非中斷版本, 但最終都是通過開/關中斷來實現。有關開/光中斷的底層代碼我們已經講解,那么接下來的退出和進入臨界段的代碼配套注釋來理解即可。
進入臨界段
進入臨界段,不帶中斷保護版本且不能嵌套的代碼實現
1. 不帶中斷保護版本
/* ==========進入臨界段, 不帶中斷保護版本,不能嵌套=============== */ /* 在 task.h 中定義 */ #define taskENTER_CRITICAL() portENTER_CRITICAL()/* 在 portmacro.h 中定義 */ #define portENTER_CRITICAL() vPortEnterCritical()/* 在 port.c 中定義 */ void vPortEnterCritical( void ) {portDISABLE_INTERRUPTS();uxCriticalNesting++; if ( uxCriticalNesting == 1 ) {configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );} }/* 在 portmacro.h 中定義 */ #define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()/* 在 portmacro.h 中定義 */ static portFORCE_INLINE void vPortRaiseBASEPRI( void ) {uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;__asm{msr basepri, ulNewBASEPRIdsbisb} }//FreeRTOS_config.h中 #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //中斷最低優先級 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 10 //系統可管理的最高中斷優先級 #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) ) #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )uxCriticalNesting 是在 port.c 中定義的靜態變量,表示臨界段嵌套計數器 , 默 認 初 始 化 為 0xaaaaaaaa , 在 調 度 器 啟 動 時 會 被 重 新 初 始 化 為 0 :vTaskStartScheduler()->xPortStartScheduler()->uxCriticalNesting = 0。
如果 uxCriticalNesting 等于 1,即一層嵌套,要確保當前沒有中斷活躍,即內核外設 SCB 中的中斷和控制寄存器 SCB_ICSR 的低 8 位要等于 0。
uxCriticalNesting這個變量比較重要,用于臨界段的嵌套計數。初學的同學也許會問這里直接的開關中斷不就可以了嗎,為什么還要做一個嵌套計數呢?主要是因為直接的開關中斷方式不支持在開關中斷之間的代碼里再次執行開關中斷的嵌套處理,假如當前們的代碼是關閉中斷的,嵌套了一個含有開關中斷的臨界區代碼后,退出時中斷就成開的了,這樣就出問題了。通過嵌套計數就有效地防止了用戶嵌套調用函數taskENTER_CRITICAL 和 taskEXIT_CRITICAL 時出錯。
2. 帶中斷保護版本
/* ==========進入臨界段,帶中斷保護版本,可以嵌套=============== */ /* 在 task.h 中定義 */ #define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()/* 在 portmacro.h 中定義 */ #define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()/* 在 portmacro.h 中定義 */ static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) {uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;__asm{mrs ulReturn, baseprimsr basepri, ulNewBASEPRIdsbisb}return ulReturn; }通過上面的源碼實現可以看出,FreeRTOS 的開關全局中斷是通過操作寄存器 basepri 實現的。初學的同學也許會問,這里怎么沒有中斷嵌套計數了呢?是的,這里換了另外一種實現方法,通過保存和恢復寄存器 basepri 的數值就可以實現嵌套使用。如果大家研究過 uCOS-II 或者 III 的源碼,跟這里的實現方式是一樣的。
/*===========================================可屏蔽的中斷優先級配置====================================================*/ /** 用于配置STM32的特殊寄存器basepri寄存器的值,用于屏蔽中斷,當大于basepri值的優先級的中斷將被全部屏蔽。basepri只有4bit有效,* 默認只為0,即全部中斷都沒有被屏蔽。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY配置為:5,意思就是中斷優先級大于5的中斷都被屏蔽。* 當把配置好的優先級寫到寄存器的時候,是按照8bit來寫的,所以真正寫的時候需要經過轉換,公式為:* ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff),其中的priority就是我們配置的真正的優先級。經過這個公式之后得到的是下面的這個宏:* configMAX_SYSCALL_INTERRUPT_PRIORITY** 在FreeRTOS中,關中斷是通過配置basepri寄存器來實現的,關掉的中斷由配置的basepri的值決定,小于basepri值的* 中斷FreeRTOS是關不掉的,這樣做的好處是系統設計者可以人為的控制那些非常重要的中斷不能被關閉,在緊要的關頭必須被響應。* 而在UCOS中,關中斷是通過控制PRIMASK來實現的,PRIMASK是一個單1的二進制位,寫1則除能除了NMI和硬 fault的所有中斷。當UCOS關閉* 中斷之后,即使是你在系統中設計的非常緊急的中斷來了都不能馬上響應,這加大了中斷延遲的時間,如果是性命攸關的場合,那后果估計挺嚴重。* 相比UCOS的關中斷的設計,FreeRTOS的設計則顯得人性化很多。**/ #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2 #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )退出臨界段
退出臨界段,不帶中斷保護版本且不能嵌套的代碼實現
1. 不帶中斷保護的版本
/* ==========退出臨界段,不帶中斷保護版本,不能嵌套=============== */ /* 在 task.h 中定義 */ #define taskEXIT_CRITICAL() portEXIT_CRITICAL()/* 在 portmacro.h 中定義 */ #define portEXIT_CRITICAL() vPortExitCritical()/* 在 port.c 中定義 */ void vPortExitCritical( void ) {configASSERT( uxCriticalNesting );uxCriticalNesting--;if ( uxCriticalNesting == 0 ){portENABLE_INTERRUPTS();} }/* 在 portmacro.h 中定義 */ #define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )/* 在 portmacro.h 中定義 */ static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) {__asm{msr basepri, ulBASEPRI} }2. 帶中斷保護的版本
/* ==========退出臨界段,帶中斷保護版本,可以嵌套=============== */ /* 在 task.h 中定義 */ #define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )/* 在 portmacro.h 中定義 */ #define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)/* 在 portmacro.h 中定義 */ static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI ) {__asm{msr basepri, ulBASEPRI} }應用
在 FreeRTOS 中,對臨界段的保護出現在兩種場合,一種是在中斷場合一種是在非中斷場合
/* 中斷場合*/ {uint32_t ulReturn;/* 進入臨界段 */ulReturn = taskENTER_CRITICAL_FROM_ISR();/* 臨界段代碼 *//* 退出臨界段 */taskEXIT_CRITICAL_FROM_ISR( ulReturn ); }/* 非中斷場合 */ {/* 進入臨界段 */taskENTER_CRITICAL();/* 臨界段代碼 *//* 退出臨界段*/taskEXIT_CRITICAL(); }參考鳴謝:
https://www.itdaan.com/blog/2017/07/21/24a1a4f2e6d7df68fbedf353e7096082.html
總結
以上是生活随笔為你收集整理的FreeRTOS内核详解(1) —— 临界段保护原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何查看一个现有的keil工程之前由什么
- 下一篇: 【手把手带你学nRF52832/nRF5