探究為什么FreeRTOS 有些API不能在中斷服務函數中調用,轉而需要調用帶ISR的版本


  用了好久的FreeRTOS以前只是知道,如果在中斷服務程序中調用某一些FreeRTOS的API函數時需要注意,如果有ISR版本的一定要調用,末尾帶ISR的函數,並且要調用系統的API函數,中斷服務程序的中斷優先級不能高於配置宏(configMAX_SYSCALL_INTERRUPT_PRIORITY)的值這是為什么呢。剛好今天受台風影響只能在家里窩着,所以就想着趁有時間看看這一部分的內容,研究一下為什么,那么廢話不多說開干。

   找了幾個函數簡化一些安全檢查的內容再把一些宏函數替換后對比觀察了下內容如下:

TickType_t xTaskGetTickCount( void )
{
    TickType_t xTicks;
    {
        xTicks = xTickCount;
    }
    return xTicks;
}
/*-----------------------------------------------------------*/

TickType_t xTaskGetTickCountFromISR( void )
{
    TickType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

    uxSavedInterruptStatus = portTICK_TYPE_SET_INTERRUPT_MASK_FROM_ISR();
    {
        xReturn = xTickCount;
    }
    portTICK_TYPE_CLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

    return xReturn;
}

其中的函數

分別解析下

  • portASSERT_IF_INTERRUPT_PRIORITY_INVALID這是一個宏對應的真實函數為vPortValidateInterruptPriority,具體內容如下,這個函數解釋了為什么不能在優先級高於configMAX_SYSCALL_INTERRUPT_PRIORITY配置宏的中端中調用系統API函數了。
void vPortValidateInterruptPriority( void )
{
    uint32_t ulCurrentInterrupt;
    uint8_t ucCurrentPriority;
    //參考內核指南,這個命令是獲取當前的中斷號
    ulCurrentInterrupt = vPortGetIPSR();
    /*portFIRST_USER_INTERRUPT_NUMBER 是一個和芯片相關的用戶中斷號
    在M3、M4的芯片上就是15以后是外部中斷的中斷號所以這里配置成16
    判斷是不是在外部中斷中調用的API函數,如果是執行if里的內容*/
    if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
    {
        /*根據中斷服務函數的中斷號獲取當前中斷的優先級設置*/
        ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
        /*如果當前執行的中斷優先級數字小於配置ucMaxSysCallPriority這個值實際上是
        configMAX_SYSCALL_INTERRUPT_PRIORITY,也就是當前中斷優先級高於配置最高優先級
        斷言將會失敗,程序將停止在這里*/
        configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
    }
    /*當前中斷優先級分組組大於配置分組(也就是表示搶占優先級的位數少於配置)則斷言失敗,程序停止*/
    configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
}
  • portTICK_TYPE_SET_INTERRUPT_MASK_FROM_ISR 和 portTICK_TYPE_CLEAR_INTERRUPT_MASK_FROM_ISR

這兩個函數實際上是設置內核的中斷屏蔽寄存器(BASEPRI),但是在xTaskGetTickCountFromISR函數中就是這樣設計的,在另一個函數中也可以看出來,比如任務掛起恢復函數xTaskResumeFromISR同樣簡化后看

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
{
    BaseType_t xYieldRequired = pdFALSE;
    TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    UBaseType_t uxSavedInterruptStatus;

    configASSERT( xTaskToResume );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    //上面的內容同獲取當前時鍾SysTick相同,不說了
    /*portSET_INTERRUPT_MASK_FROM_ISR就是將中斷掩蔽寄存器配置成系統調用最高中斷優先級
    進而掩蔽中斷優先級低於系統調用最高優先級*/
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        /*任務是否是掛起態*/
        if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
        {
            /*調試代碼*/
            traceTASK_RESUME_FROM_ISR( pxTCB );

            /*調度器為運行態*/
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
                /*恢復的任務比中斷之前的任務優先級高,則將xYieldRequired設置,提示需要任務切換保證系統的實時性*/
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
                    xYieldRequired = pdTRUE;
                }
                else
                {
                    
                }
                /*任務的一些狀態等修改,並將任務加入就緒列表等待調度器調度*/
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
                /*調度器被暫停,此時只需要將任務加入掛起就緒列表,等任務調度器恢復后加入到就緒任務列表*/
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }
        }
        else
        {
        }
    }
    /*取消中斷屏蔽*/
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
    /*返回是否需要任務切換狀態*/
    return xYieldRequired;
}

對比普通版的任務恢復函數

void vTaskResume( TaskHandle_t xTaskToResume )
{
    TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;
    configASSERT( xTaskToResume );
    if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) )
    {
        /*設置中斷掩蔽,同時將臨結嵌套深度增加1*/
        taskENTER_CRITICAL();
        {
            if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )
            {
                traceTASK_RESUME( pxTCB );

                /*  */
                ( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );

                /*任務的一些狀態等修改,並將任務加入就緒列表等待調度器調度*/
                if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
                {
                    /*懸起PenSVC中斷,退出臨結后啟動任務切換調度*/
                    taskYIELD_IF_USING_PREEMPTION();
                }
                else
                {
                    
                }
            }
            else
            {
                
            }
        }
        taskEXIT_CRITICAL();
    }
    else
    {
        
    }
}

對比兩個函數的實現方式,不同的地方時ISR結尾的函數不會觸發任務調度,而是返回需不需要任務調度,這是為什么呢????這就需要去看看,任務調度函數的實現了,這里就語言論述了。

     任務切換由可懸起中斷服務函數(PendSV)負責,這是一個匯編函數,其中主要做的事情是將,任務當前的執行狀態以PSP進行壓棧(保存現場),然后調用vTaskSwitchContext,找到就緒的其他任務(優先級相同或者高於當前任務)然后從任務堆棧中出棧--改寫寄存器(偷梁換柱-操作系統的巧妙之處),在返回時處理器會按寄存器中保存的返回地址直接返回,然后新的任務就運行了。這個過程通常是由滴答定時器,按照固定周期觸發的,具體的觸發過程由滴答定時器中斷服務函數置起中斷標志位,然后在滴答定時器退出PendSV中斷就會執行,進而任務切換,用一個圖來說明一下這個過程吧。但是我想不通一點,M3的內核是支持中斷咬未操作的,所以應該是下圖的這樣的一個過程。

但是當任務A在運行用到了寄存器R8但是,當滴答定時器中斷來臨時,一部分寄存器被硬件自動壓棧保存(不包括R8)①,然后開始執行滴答定時器中斷服務函數,一堆處理完成后懸起PendSV中斷,這時當滴答定時器准備退出時難道會按中斷咬尾的方式直接進行PendSV中斷服務函數。從源碼看出來PendSV服務函數,會將剩下的寄存器R4-R11等繼續入棧(用psp),棧指針沒有問題,但是,R4-R11可能已經在滴答定時器終端服務程序中被使用過,所以他不是之前的任務堆棧的狀態,這樣壓棧沒有問題嗎??哎想不通,難道這里不會按中斷咬尾處理,先出棧在進PendSV中斷,希望知道的人指導一下,不勝感激。。。。(2019-08-10)。后來我找來了吃灰已久的開發板移植了FreeRTOS進行仿真測試,測試發現實際的效果就是后面的處理方式(沒有發生中斷咬尾)具體原因是什么尚不得知,如果有知道的大佬麻煩留言指導下。(2019-08-17 14:34:43)這個問題先放下繼續探究主題。

  如果在中斷中不調用ISR結尾或者在優先級高於系統調用優先級的中端中調用系統API函數會發生什么,為什么不能這么使用,通過查看多個類似的ISR結尾的函數和普通版本的區別發現,就是在中斷中可調用的函數會增加設置中斷掩蔽寄存器的操作,增加這一操作對於芯片而言就是防止一些低於configMAX_SYSCALL_INTERRUPT_PRIORITY優先級(在M4上就是優先級數字大於這個值)且高於當前中斷優先級(中斷優先級數字小於當前中斷優先級)的中斷打斷當前中斷中正在執行的系統調用。但是對於操作系統軟件而言會有什么問題呢,從FreeRTOS的用戶指導手冊發現,ISR結尾的類似的函數都應該在中斷中調用,並且一般情況下(uxQueueMessagesWaiting 除外)這類函數都會設置中斷掩蔽寄存器,同時有時還返回是否需要立刻執行任務切換。又根據給出的使用實例系統調用返回后還是在中斷服務函數中,此時需要手動檢查是否需要開始一次任務切換,而非ISR結尾的函數則直接在API函數中調用portYIELD()觸發一次任務調度,這就奇怪了,好像除了調用位置之外沒有什么區別,最后多方查詢發現這一種解釋是最可能的答案,這是為了保證操作系統的實時性,因為如果有更高優先級的任務就緒了就要讓高優先級的任務最快的速度運行起來,其次是在普通任務中調用的portYIELD函數和中斷中調用的portYIELD函數的實現不同因此需要區別對待。以上兩種答案好像都無法合理解釋我心中的疑慮。最后找到資料有這樣一句話“freeRTOS支持中斷嵌套,低於configMAX_SYSCALL_INTERRUPT_PRIORITY優先級的中斷里才允許調用FreeRTOS 的API函數,而優先級高於這個值的中斷則可以像前后台系統下一樣正常運行,但是這些中斷函數不能調用系統API函數”,然后豁然開朗,因為操作系統為了保證操作系統內核的運行穩定性,保證API 執行重要的過程都是原子操作,這樣就不會存在系統運行紊亂,比如如果在中斷中釋放信號量,但是這個中斷又會被其他低於configMAX_SYSCALL_INTERRUPT_PRIORITY但是高於當前中斷的中斷程序打斷,此時系統的運行過程就出現了紊亂,可能會導致任務優先級翻轉的問題。因此需要將系統的API相關重要操作“原子”化,從而避免系統運行紊亂。FreeRTOS是支持中斷嵌套的,分幾種情況分析,不全,意思到就行。

  1. 任務A正在執行中,發生了中斷優先級為“中等”的中斷M然后發生中斷優先級為“高等”的中斷H(M和H都高於configMAX_SYSCALL_INTERRUPT_PRIORITY)

此時發生中斷M,此時的處理過程和前后台中斷過程相同,由硬件自動入棧,然后中斷執行,然后又來了中斷H,此時使用MSP將M的執行現場保存,然后開始運行H中斷,完成后POP后接着運行M中斷服務程序。

  1. 滴答定時器懸起了PendSV中斷后發生了新的中斷Q(優先級高於PendSV)。

    如果此中斷在PendSV壓棧完成后尚未取指,則次中斷會按晚到中斷處理先運行Q然后運行完后再以中斷咬尾的方式運行PendSV,如果PendSV已經取指則按正常嵌套處理;如果新發生的中斷優先級低於configMAX_SYSCALL_INTERRUPT_PRIORITY並且它還調用了系統 API函數如果此時PendSV中斷函數已經完成了中斷掩蔽寄存器的設置,則先執行PendSV后執行這個中斷Q,反之中斷Q會先執行(嵌套)然后執行PendSV。這里如果這個后來的中斷Q服務歷程調用的API會使某個高優先級的任務就緒的話,那么兩種情況下的任務調度結果是不同的,差了一個系統時間片。

最后糾自己的①處的理解錯誤,R8具體壓不壓棧是由編譯器做掉了,如果你發生中斷以前用到了R8就會執行壓棧操作,否則不會但是這個過程確實令人費解的是,中斷是隨機來的,代碼編譯時,已經確定是壓棧還是不壓棧了,也就是說不想函數調用之間有明顯的關系可以進行判斷。最后再次糾正自己,一次(手動狗頭),具體用不用高位寄存器是根據中斷服務函數來判斷的,然后就不用管中斷之前是否使用過了。Ok到此我覺得自己的問題解決了,這個帖子前后經歷了一個周算是完成了,然后突然發現今天自己邁向25歲了,耳邊響起了許巍的歌..........

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM