1. 程式人生 > >cortex-M3 的SVC、PendSV異常,與作業系統(ucos實時系統)

cortex-M3 的SVC、PendSV異常,與作業系統(ucos實時系統)

SVC異常是?
PendSV異常是?
ucos 任務切換時機?
ucos 如何滿足實時性(實現)?
ucos中,systick的優先順序?

SVC和PendSV

SVC(系統服務呼叫,亦簡稱系統呼叫)和PendSV(可懸起系統呼叫),它們多用於在作業系統之上的軟體開發中。

SVC:

SVC 用於產生系統函式的呼叫請求。
例如,作業系統不讓使用者程式直接訪問硬體,而是通過提供一些系統服務函式,使用者程式使用SVC 發出對系統服務函式的呼叫請求,以這種方法呼叫它們來間接訪問硬體。
因此,
當用戶程式想要控制特定的硬體時,它就會產生一個SVC 異常,
然後作業系統提供的SVC 異常服務例程得到執行,
它再呼叫相關的作業系統函式,
後者完成使用者程式請求的服務。
這種“提出要求——得到滿足”的方式,很好、很強大、很方便、很靈活、很能可持續發展。
首先,它使使用者程式從控制硬體的繁文縟節中解脫出來,而是由作業系統 負責控制具體的硬體。
第二,作業系統的程式碼可以經過充分的測試,從而能使系統更加健壯和可靠。
第三,它使使用者程式無需在特權級下執行,使用者程式無需承擔因誤操作而癱瘓整個系統的風險。
第四,通過SVC 的機制,還讓使用者程式變得與硬體無關,因此在開發應用程式時無需瞭解硬體的操作細節,從而簡化了開發的難度和繁瑣度,並且使應用程式跨硬體平臺移植成為可能。開發應用程式唯一需要知道的就是作業系統提供的應用程式設計介面(API),並且瞭解各個請求代號和引數表,然後就可以使用SVC 來提出要求了(事實上,為使用方便,作業系統往往會提供一層封皮,以使系統呼叫的形式看起來和普通的函式呼叫一致。各封皮函式會正確使用SVC指令來執行系統呼叫——譯者注)。
其實,嚴格地講,操作硬體的工作是由裝置驅動程式完成的,只是對應用程式來說,它們也是作業系統的一部分。如圖7.14 所示
這裡寫圖片描述


SVC 異常通過執行”SVC”指令來產生。該指令需要一個立即數,充當系統呼叫代號。SVC異常服務例程稍後會提取出此代號,從而解釋本次呼叫的具體要求,再呼叫相應的服務函式。例如,

SVC 0x3 ; 呼叫3 號系統服務

在SVC 服務例程執行後,上次執行的SVC 指令地址可以根據自動入棧的返回地址計算出。找到了SVC 指令後,就可以讀取該SVC 指令的機器碼,從機器碼中萃取出立即數,就獲知了請求執行的功能代號。如果使用者程式使用的是PSP,服務例程還需要先執行

MRS Rn,PSP

指令來獲取應用程式的堆疊指標。通過分析LR 的值,可以獲知在SVC 指令執行時,正在使用哪個堆疊。
由CM3 的中斷優先順序模型可知,你不能在SVC 服務例程中巢狀使用SVC 指令(事實上這樣做也沒意義),因為同優先順序的異常不能搶佔自身。這種作法會產生一個用法fault。同理,在NMI 服務例程中也不得使用SVC,否則將觸發硬fault。

PendSV:

另一個相關的異常是PendSV(可懸起的系統呼叫),它和SVC 協同使用。
一方面,SVC異常是必須立即得到響應的(若因優先順序不比當前正處理的高,或是其它原因使之無法立即響應,將上訪成硬fault——譯者注),應用程式執行SVC 時都是希望所需的請求立即得到響應。
另一方面,PendSV 則不同,它是可以像普通的中斷一樣被搶佔掛起的(不像SVC 那樣會上訪)。
作業系統 可以利用它“緩期執行”一個異常——直到其它重要的任務完成後才執行動作。

PendSV是什麼?
根據 權威指南。PendSV是為系統裝置而設的“可懸掛請求”(pendable request)。

  • 上下文切換 不能在中斷中進行,會導致中斷延期。為了解決這個問題,使用 PendSV。PendSV可以掛起,也就是等到別的 ISR結束後緩期執行。
  • 為了實現緩期執行PendSV,PendSV一定要被設定為最低優先順序的異常。

掛起PendSV 的方法是:軟體實現OSIntCtxSw()函式,向NVIC 的PendSV 懸起暫存器中寫1。

NVIC_INT_CTRL   EQU     0xE000ED04   ; Interrupt control state register.
NVIC_PENDSVSET  EQU     0x10000000   ; Value to trigger PendSV exception.
OSIntCtxSw
    LDR     R0, =NVIC_INT_CTRL       ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

掛起後,如果優先順序不夠高,則將緩期等待執行。
PendSV 的典型使用場合是在上下文切換時(在不同任務之間切換)。

作業系統,上下文切換 例項:

場景假設:一個系統(按時間片輪轉排程的系統)中有兩個就緒的任務(A任務、B任務),
上下文切換被觸發的場合可以是:

  • 執行一個系統呼叫
  • 系統滴答定時器(SYSTICK)中斷,(輪轉排程中需要)

A、B兩個就緒任務,通過SysTick 異常啟動上下文切換。如圖7.15 所示。
這裡寫圖片描述
上圖是兩個任務輪轉排程的示意圖。
但若在產生SysTick 異常時正在響應一箇中斷,則SysTick 異常會搶佔其ISR。
在這種情況下,作業系統 不可以執行上下文切換,否則將使中斷請求被延遲,
而且在真實系統中延遲時間還往往不可預知——任何有一丁點實時要求的系統都決不能容忍這種事。
因此,在CM3 中也是嚴禁沒商量——如果作業系統 在某中斷活躍時嘗試切入執行緒模式,將觸犯用法fault 異常。
這裡寫圖片描述
為解決此問題,早期的作業系統 大多會在SysTick 異常中 檢測當前是否有中斷在活躍中,只有沒有任何中斷需要響應時,才執行上下文切換(切換期間無法響應中斷)。
然而,這種方法的弊端在於,
它可能把任務切換動作拖延很久(因為如果搶佔了IRQ,則本次SysTick 在執行後不得作上下文切換,只能等待下一次SysTick 異常),尤其是當某中斷源的頻率和SysTick 異常的頻率比較接近時,會發生“共振”。
現在好了,PendSV 來完美解決這個問題了(產生SysTick 異常時正在響應一箇中斷,SysTick 異常會搶佔其ISR。此時,作業系統 不可以執行上下文切換,否則將使中斷請求被延遲):
把PendSV 程式設計為最低優先順序的異常,PendSV 異常會自動延遲上下文切換的請求,直到其它的ISR 都完成了處理後才放行。
如果作業系統 檢測到某IRQ 正在活動並且被SysTick 搶佔,它將懸起一個PendSV 異常,以便緩期執行上下文切換。如圖7.17 所示
這裡寫圖片描述
流水賬記錄如下:
1. 任務 A 呼叫SVC 來請求任務切換(例如,等待某些工作完成)
2. OS 接收到請求,做好上下文切換的準備,並且pend 一個PendSV 異常。
3. 當 CPU 退出SVC 後,它立即進入PendSV,從而執行上下文切換。
4. 當 PendSV 執行完畢後,將返回到任務B,同時進入執行緒模式。
5. 發生了一箇中斷,並且中斷服務程式開始執行
6. 在 ISR 執行過程中,發生SysTick 異常,並且搶佔了該ISR。
7. OS 執行必要的操作,然後pend 起PendSV 異常以作好上下文切換的準備。
8. 當 SysTick 退出後,回到先前被搶佔的ISR 中,ISR 繼續執行
9. ISR 執行完畢並退出後,PendSV 服務例程開始執行,並且在裡面執行上下文切換
10. 當 PendSV 執行完畢後,回到任務A,同時系統再次進入執行緒模式。

其實,ucos中的實現,於此有些差異,但從結果上看,是一致的(如果systick搶佔了其他ISRs,不會在其中執行上下文切換。會等到全部的ISRs執行完畢後(期間一定是無任務排程的),才執行pendsv異常,完成上下文的切換。==差別在於生成pendsv異常的時機。)

ucos 關於 PendSV 異常的應用(上下文切換時機、怎樣滿足實時性):

在systick異常中,執行必要的任務維護更新工作,在退出時,考慮生成pensv異常。
當且僅當無中斷被搶佔時,生成pensv異常,並於pendsv異常中,完成上下文切換工作;
當每一箇中斷/異常處理函式中,均在退出時考慮生成pensv異常,則最終無中斷可執行時,pensv異常一定會生成,並且期間無任務排程。

這可能與上邊描述的不一致,但結果上來看,是一致的。
下邊以ucos系統的原始碼,進行大概的講解:

中斷/異常處理通用模板:

    OS_CPU_SR  cpu_sr; 
    OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR          */
    OSIntNesting++;
    OS_EXIT_CRITICAL();
    使用者處理程式碼;
    void  OSIntExit (void);//OSIntNesting--;以及可能的排程

systick異常實現(ucos心臟):

void  OS_CPU_SysTickHandler (void)
{
    OS_CPU_SR  cpu_sr;
    OS_ENTER_CRITICAL(); /* Tell uC/OS-II that we are starting an ISR */
    OSIntNesting++;
    OS_EXIT_CRITICAL();
    OSTimeTick();  /* Call uC/OS-II's OSTimeTick()       */

    OSIntExit();  /* Tell uC/OS-II that we are leaving the ISR */
}

void OSTimeTick (void)

void  OSTimeTick (void)
{
更新系統時間,OSTime++;
遍歷OSTCBList 任務控制塊連結串列(已經建立的任務),
    如果任務控制塊OSTCBDly非零,則減一;
    如果等於零,更新OSTCBStat(任務狀態)OSTCBStatPend(任務掛起狀態)成員;
    如果OSTCBStat等於OS_STAT_RDY(就緒狀態),則將任務放入就緒表中。
}

OSIntExit

/*$PAGE*/
/*
*********************************************************************************************************
*                                               EXIT ISR
*
* Description: This function is used to notify uC/OS-II that you have completed serviving an ISR.  When
*              the last nested ISR has completed, uC/OS-II will call the scheduler to determine whether
*              a new, high-priority task, is ready to run.
*
* Arguments  : none
*
* Returns    : none
*
* Notes      : 1) You MUST invoke OSIntEnter() and OSIntExit() in pair.  In other words, for every call
*                 to OSIntEnter() at the beginning of the ISR you MUST have a call to OSIntExit() at the
*                 end of the ISR.
*              2) Rescheduling is prevented when the scheduler is locked (see OS_SchedLock())
*********************************************************************************************************
*/

void  OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3                                /* Allocate storage for CPU status register */
    OS_CPU_SR  cpu_sr = 0;
#endif



    if (OSRunning == OS_TRUE) {
        OS_ENTER_CRITICAL();
        if (OSIntNesting > 0) {                            /* Prevent OSIntNesting from wrapping       */
            OSIntNesting--;
        }
        if (OSIntNesting == 0) {                           /* Reschedule only if all ISRs complete ... */
            if (OSLockNesting == 0) {                      /* ... and not locked.                      */
                OS_SchedNew();
                OSTCBHighRdy  = OSTCBPrioTbl[OSPrioHighRdy];
                if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy */

#if OS_TASK_PROFILE_EN > 0
                    OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task  */
#endif
                    OSCtxSwCtr++;                          /* Keep track of the number of ctx switches */
                    OSIntCtxSw();                          /* Perform interrupt level ctx switch       */
                }
            }
        }
        OS_EXIT_CRITICAL();
    }
}

我們在進入systick異常時,有執行OSIntNesting++;準備出來時,自然需要OSIntNesting–;
如果OSIntNesting不等於零,退出systick異常。
只有OSIntNesting等於零(無其他異常/中斷髮生)並且OSLockNesting等於零(無任務排程鎖),才執行OS_SchedNew()查就任務緒表中最高優先順序並返回,比較返回的優先順序是否為當前執行任務的優先順序,
僅不相等時,執行OSIntCtxSw()函式,生成pendsv異常。OSIntCtxSw()函式的實現見上邊篇幅。
pendsv異常OS_CPU_PendSVHandler,實現上下文的切換。這裡不做解釋。

ucos中,systick的優先順序?

PENDSV和SYSTICK屬於系統異常;
定時器中斷,串列埠中斷這些屬於外部中斷。
PENDSV和SYSTICK的中斷優先順序可以程式設計,
一般要把PENDSV的優先順序設定成最低(沒什麼好說的)。
但SYSTICK異常的優先順序:

  • 一般無需設定(高於外部中斷的優先順序),畢竟這是系統的時鐘源(ucos心臟);
  • 當然,也可根據專案需要(有些外部中斷,專案上要求務必實時),將SYSTICK優先順序設定與合適的位置。
  • 確實存在的普遍現象是,很多專案對於實時沒有很高的要求,乾脆將PENDSV和SYSTICK的優先順序都設定成OxFF。

都是最低優先順序,此時因為PENDSV在中斷向量表中排在SYSTICK前面,所以如果PENDSV,SYSTICK同時產生中斷,PENDSV優先中斷。

本文有些地方表述的確實有些怪異,見諒。
歡迎各位指正。