第十八章 machine.Timer類實驗 1)實驗平臺:正點原子DNK210開發板 2)章節摘自【正點原子】DNK210使用指南 - CanMV版 V1.0 3)購買鏈接:https://detail.tmall.com/item.htm?&id=782801398750 4)全套實驗源碼+手冊+ ...
在進行FreeRTOS任務切換的介紹前,我們先來瞭解一下SVC和PendSV。
SVC和PendSV
SVC(系統服務調用,亦簡稱系統調用)和 PendSV(可懸起系統調用),它們多用於在操作系統之上的軟體開發中。SVC用於產生系統函數調用的請求。操作系統不讓用戶直接訪問硬體,而是通過提供一些系統服務函數,用戶程式通過使用SVC發出對系統服務函數的調用請求,以系統服務函數間接的去訪問硬體。因此當用戶程式想要控制特定的硬體時,它就會產生一個SVC異常,然後操作系統提供的SVC異常服務常式得到執行,它再調用相關的操作系統函數,由它來完成用戶程式請求的服務。
PendSV(可懸起的系統調用),它和 SVC 協同使用。一方面,SVC異常是必須立即得到響應的(若因優先順序不比當前正處理的高,或是其它原因使之無法立即響應,將上訪成硬 fault),應用程式執行 SVC 時都是希望所需的請求立即得到響應。另一方面,PendSV 則不同,它是可以像普通的中斷一樣被懸起的(不像 SVC那樣會上訪)。OS 可以利用它“緩期執行”一個異常,直到其它重要的任務完成後才執行動作。懸起 PendSV 的方法是:手工往 NVIC 的 PendSV 懸起寄存器中寫 1。懸起後,如果優先順序不夠高,則將緩期等待執行。
PendSV 的典型使用場合是在上下文切換時(在不同任務之間切換)。例如,一個系統中有兩個就緒的任務,上下文切換被觸發的場合可以是:
1.執行一個系統調用
2.系統滴答定時器(SYSTICK)中斷
舉個簡單的例子來輔助理解。假設有這麼一個系統,裡面有兩個就緒的任務,並且通過 SysTick 異常啟動上下文切換。
在典型的RTOS中,任務的執行時間被分為多個時間片,每個任務執行相對應時間片。上圖的任務切換是在Systick中斷中完成的,每觸發一次Systick中斷就會進行任務切換。但若在產生Systick異常時正在響應一個中斷,則Systick異常會搶占其中斷。在這種情況下,OS不得執行上下文切換,否則將使中斷請求被延遲,,而且在真實系統中,延時時間往往是不可預知的,這對於實時操作系統來說是不能容忍的。由於存在中斷請求,ARM Cortex-M 不允許返回線程模式,因此,將會產生用法錯誤異常(Usage Fault)。
在一些 RTOS 的設計中,會通過判斷是否存在中斷請求,來決定是否進行任務切換。雖然可以通過檢查 xPSR 或 NVIC 中的中斷活躍寄存器來判斷是否存在中斷請求,但是這樣可能會影響系統的性能,甚至可能出現當某中斷源的頻率和 SysTick 異常的頻率比較接近時,會發生“共振”。中斷源在 SysTick 中斷前後不斷產生中斷請求,導致系統無法進行任務切換的情況。
使用PendSV就能解決這個問題,PendSV 異常會自動延遲上下文切換的請求,直到其它的 ISR 都完成了處理後才放行。為實現這個機制,需要把 PendSV 編程為最低優先順序的異常。如果 OS 檢測到某 IRQ 正在活動並且被 SysTick 搶占,它將懸起一個 PendSV 異常,以便緩期執行上下文切換。
1.任務A呼叫SVC來請求任務切換(例如,任務A正在等待某些工作完成)。
2.內核接到請求,做好上下文切換的準備,並且懸起一個PendSV異常。
3.當退出SVC後,立即進入PendSV,在PendSV中執行上下文切換。
4.當PendSV執行完畢後,返回線程模式,切換到執行任務B。
5.發生了一個中斷,開始執行中斷服務程式。
6.在中斷執行過程中,發生了Systick異常(用於內核時鐘節拍),並且搶占了該中斷。
7.操作系統執行必要的操作,如更新內部時間計數、遍歷延遲任務隊列等,然後掛起PendSV異常,準備進行任務切換。
8.當Systick退出後,回到先前被搶占的ISR中,ISR繼續執行。
9.當中斷執行完畢後,PendSV服務常式開始執行,完成任務切換。
10.當PendSV執行完畢後,回到任務A,同時系統再次進入線程模式。
FreeRTOS中的任務切換就是在PendSV中完成的。
PendSV 中斷服務函數
FreeRTOS 在 PendSV 的中斷中,完成任務切換,PendSV 的中斷服務函數由 FreeRTOS 編寫,將 PendSV 的中斷服務函數定義成函數 xPortPendSVHandler()。針 對 ARM Cortex-M3 和針對 ARM Cortex-M4 和 ARM Cortex-M7 內 核 的 函 數xPortPendSVHandler()稍有不同,其主要原因在於 ARM Cortex-M4 和 ARM Cortex-M7 內核具有浮點單元,因此在進行任務切換的時候,還需考慮是否保護和恢復浮點寄存器的值。針對 ARM Cortex-M3 內核的函數xPortPendSVHandler(),具體的代碼如下所示
__asm void xPortPendSVHandler( void )
{
/* 導入全局變數及函數 */
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* 8 位元組對齊 */
PRESERVE8
/* 從 PSP 寄存器(進程堆棧指針)中獲取當前任務的堆棧指針 */
mrs r0, psp
/* isb 指令用於確保指令的嚴格順序執行,以提高代碼的可靠性和正確性 */
isb
/* 將 pxCurrentTCB 的 地址值 載入到 r3 寄存器中,即指向當前運行任務控制塊的指針 */
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
/* 將 r3 寄存器中存儲的記憶體地址所對應的數據載入到 r2 寄存器中,即當前運行任務控制塊的首地址 */
ldr r2, [r3]
/* 將 R4~R11 入棧到當前運行任務的任務棧中 */
stmdb r0!, {r4-r11} /* Save the remaining registers. */
/* r0 的內容會被存儲到由 R2 寄存器指定的記憶體地址中
將當前任務的堆棧指針中的內容存到 R2. R2 指向的地址為此時的任務棧指針 */
str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
/* 將 r3 和 r14 保存到內核堆棧MSP中,然後設置 basepri 寄存器的值為
configMAX_SYSCALL_INTERRUPT_PRIORITY,屏蔽受 FreeRTOS 管理的所有中斷。
然後執行數據同步操作(dsb)和指令同步操作(isb),確保之前的操作已完成 */
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 跳轉到函數 vTaskSeitchContext
* 主要用於更新 pxCurrentTCB,
* 使其指向最高優先順序的就緒態任務
*/
bl vTaskSwitchContext
mov r0, #0
/* 將 basepri 寄存器的值恢復為 0,為 0 表示允許所有優先順序的中斷 */
msr basepri, r0
/* 這部分代碼從棧中彈出 r3 和 r14 的值,這兩個寄存器用於保存任務
控制塊指針和鏈接寄存器。然後,它載入當前任務控制塊中的棧頂指針
到 r0 中,接著將 r4-r11 的值從棧中彈出並存儲到相應的寄存器中,
最後使用 msr 指令將當前任務的棧指針存儲到 psp 寄存器中 */
/* 將 R3、R14 重新從 MSP 指向的棧中出棧
R3 為 pxCurrentTCB 的地址值,
pxCurrentTCB 已經在函數 vTaskSwitchContext 中更新為最高優先順序的就緒態任務
因此 r1 為 pxCurrentTCB 的值,即當前最高優先順序就緒態任務控制塊的首地址 */
ldmia sp!, {r3, r14}
ldr r1, [r3]
/* r0 為最高優先順序就緒態任務的任務棧指針 */
ldr r0, [r1] /* pxCurrentTCB中的第一項是任務棧棧頂 */
/* 從最高優先順序就緒態任務的任務棧中出棧 R4~R11 和 R14
註意:這裡出棧的 R14 為 EXC_RETURN,其保存了任務是否使用浮點單元的信息 */
ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
/* 使用 msr 指令將 r0 的值存儲到 psp 寄存器中,用作任務的棧指針 */
msr psp, r0
isb
/* 使用 bx 指令 跳轉到切換後的任務運行
執行此指令,CPU 會自動從 PSP 指向的任務棧中,
出棧 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,
接著 CPU 就跳轉到 PC 指向的代碼位置運行,
也就是任務上次切換時運行到的位置 */
bx r14
nop
}
所以從上面的代碼可以分析出,FreeRTOS在進行任務切換的時候首先會對當前任務的狀態信息進行入棧保存,保存到任務的任務棧中。然後調用vTaskSwitchContext函數更新當前最高優先順序任務的指向,然後從切換後的任務的任務棧中進行出棧,恢復新的任務的狀態信息,最後使用bx指令跳轉到切換後的任務執行。
從該函數中只看到了程式保存和恢復CPU信息中的部分寄存器信息(R4R11),這是因為硬體會自動入棧和出棧其他CPU寄存器信息。在任務運行的時候,CPU使用進程堆棧指針PSP最為棧空間來使用,也就是使用運行任務的任務棧。當Systick中斷發生時,在跳轉到Systick中斷服務函數運行前,硬體會自動除將R4R11寄存器的其他CPU寄存器入棧,因此就將任務切換前的部分信息保存到了對應任務的任務棧中,當退出PendSV時,會自動從棧空間中恢復這部分信息,以供任務正常運行。
可以通過結合下麵兩張圖來結合理解:
任務棧如下圖所示:
vTaskSwitchContext() 函數
void vTaskSwitchContext(void)
{
/* 任務調度器未在運行 */
if (uxSchedulerSuspended != (UBaseType_t)pdFALSE)
{
/* 調度程式當前掛起-不允許進行上下文切換,直接退出函數 */
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
#if (configGENERATE_RUN_TIME_STATS == 1)
{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);
#else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif
if (ulTotalRunTime > ulTaskSwitchedInTime)
{
pxCurrentTCB->ulRunTimeCounter += (ulTotalRunTime - ulTaskSwitchedInTime);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif /* configGENERATE_RUN_TIME_STATS */
taskCHECK_FOR_STACK_OVERFLOW();
/* 此函數用於將 pxCurrentTCB 更新為指向優先順序最高的就緒態任務 */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if (configUSE_NEWLIB_REENTRANT == 1)
{
_impure_ptr = &(pxCurrentTCB->xNewLib_reent);
}
#endif /* configUSE_NEWLIB_REENTRANT */
}
}
該函數內部通過調用巨集函數 taskSELECT_HIGHEST_PRIORITY_TASK(),來將pxCurrentTCB 設置為指向優先順序最高的就緒態任務。
taskSELECT_HIGHEST_PRIORITY_TASK()如下:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* Find the highest priority list that contains ready tasks. */ \
/* 查找就緒態任務列表中最高的任務優先順序 */ \
portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority); \
/* 此任務優先順序不能是最低的任務優先順序 */ \
configASSERT(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxTopPriority])) > 0); \
/* 讓 pxCurrentTCB 指向該任務優先順序就緒態任務列表中的任務 */ \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
}\
需要註意的是,FreeRTOS 提供了兩種從任務優先順序記錄中查找優先順序最高任務優先等級的方式,一種是由純 C 代碼實現的,這種方式適用於所有運行 FreeRTOS 的 MCU;另外一種方式則是使用了硬體計算前導零的指令,因此這種方式並不適用於所有運行 FreeRTOS 的 MCU,而僅適用於具有有相應硬體指令的 MCU。具體使用哪種方式,用戶可以在 FreeRTOSConfig.h 文件中進行配置。
使用此方法就限制了系統最大的優先順序數量不能超過 32,即最高優先等級為 31,不過對於絕大多數的應用場合,32 個任務優先順序等級已經足夠使用了。如果不使用次方法,理論上任務優先順序沒有上限。
軟體方式如下:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
/* Find the highest priority queue that contains ready tasks. */ \
while (listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority]))) \
{ \
configASSERT(uxTopPriority); \
--uxTopPriority; \
} \
\
/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \
the same priority get an equal share of the processor time. */ \
listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
uxTopReadyPriority = uxTopPriority; \
} /* taskSELECT_HIGHEST_PRIORITY_TASK */