大家晚上好,我是杰杰,最近挺忙的,好久沒有更新了,今天周末就吐血更新一下吧!
前言
FreeRTOS是一個是實時內核,任務是程式執行的最小單位,也是調度器處理的基本單位,移植了FreeRTOS,則避免不了對任務的管理,在多個任務運行的時候,任務切換顯得尤為重要,而任務切換的效率會決定了系統的穩定性與效率,
FreeRTOS的任務切換是干嘛的呢,rtos的實際是永遠運行的是具有最高優先級的運行態任務,而那些之前在就緒態的任務怎么變成運行態使其得以運行呢,這就是我們FreeRTOS任務切換要做的事情,它要做的是找到最高優先級的就緒態任務,并且讓它獲得cpu的使用權,這樣,它就能從就緒態變成運行態,這樣子,整個系統的實時性就會很好,回應也會很好,而不會讓程式阻塞卡死,
要知道怎么實作任務切換,那就要知道任務切換的機制,在不同的cpu(mcu)中,觸發的方式可能會不一樣,現在是以Cortex-M3為例來講講任務的切換,為了大家能看懂本文,我就拋轉引玉一下,參考《Cortex-M3權威指南-中文版》的部分陳述句(如涉及侵權,請聯系杰杰洗掉)
SVC 和 PendSV
SVC(系統服務呼叫,亦簡稱系統呼叫)和 PendSV(Pended System Call,可懸起系統呼叫),它們多用于在作業系統之上的軟體開發中,SVC 用于產生系統函式的呼叫請求,例如,作業系統不讓用戶程式直接訪問硬體,而是通過提供一些系統服務函式,用戶程式使用 SVC 發出對系統服務函式的呼叫請求,以這種方法呼叫它們來間接訪問硬體,因此,當用戶程式想要控制特定的硬體時,它就會產生一個 SVC 例外,然后作業系統提供的 SVC 例外服務例程得到執行,它再呼叫相關的作業系統函式,后者完成用戶程式請求的服務,
另一個相關的例外是 PendSV(可懸起的系統呼叫),它和 SVC 協同使用,一方面,SVC例外是必須立即得到回應的(若因優先級不比當前正處理的高,或是其它原因使之無法立即回應,將發生硬 fault——譯者注),應用程式執行 SVC 時都是希望所需的請求立即得到回應,另一方面,PendSV 則不同,它是可以像普通的中斷一樣被懸起的(不像 SVC 那樣),OS 可以利用它“緩期執行”一個例外——直到其它重要的任務完成后才執行動作,懸起 PendSV 的方法是:手工往 NVIC 的 PendSV 懸起暫存器中寫 1,懸起后,如果優先級不夠高,則將緩期等待執行,
如果一個發生的例外不能被即刻回應,就稱它被“懸起”(pending),不過,少數 fault例外是不允許被懸起的,一個例外被懸起的原因,可能是系統當前正在執行一個更高優先級例外的服務例程,或者因相關掩蔽位的設定導致該例外被除能,對于每個例外源,在被懸起的情況下,都會有一個對應的“懸起狀態暫存器”保存其例外請求,直到該例外能夠執行為止,這與傳統的 ARM 是完全不同的,在以前,是由產生中斷的設備保持住請求信號,現在NVIC 的懸起狀態暫存器的出現解決了這個問題,即使后來設備已經釋放了請求信號,曾經的中斷請求也不會錯失,
系統任務切換的工程分析
在系統中正常執行的任務(假設沒有外部中斷IRQ),用Systick直接做背景關系切換是完全沒有問題的,如圖:

但是問題是幾乎很少嵌入式的設備會不用其豐富的中斷回應,所以,直接用systick做系統的背景關系切換那是不實際的,這存在很大的風險,因為假設systick打斷了一個中斷(IRQ),立即做出背景關系切換的話,則觸犯用法 fault 例外,除了重啟你沒有其他辦法了,這樣子做出來的產品就是垃圾!!用我老板的話說就是寫的什么狗屎!!!如圖所示:

那這么說這樣不行那也不行,怎么辦啊?請看看前面接介紹的PendSV,是不是有點豁然開朗了?PendSV 來完美解決這個問題,PendSV 例外會自動延遲背景關系切換的請求,直到其它的 ISR 都完成了處理后才放行,為實作這個機制,需要把 PendSV 編程為最低優先級的例外,如果OS檢測到某 IRQ 正在活動并且被 SysTick 搶占,它將懸起一個 PendSV 例外,以便緩期執行背景關系切換,
懂了嗎?就是說,只要將PendSV的優先級設為最低的,systick即使是打斷了IRQ,它也不會馬上進行背景關系切換,而是等到IRQ執行完,PendSV 服務例程才開始執行,并且在里面執行背景關系切換,程序如圖所示:

任務切換的原始碼實作
程序差不多了解了,那看看FreeRTOS中怎么實作吧!!
FreeRTOS有兩種方法觸發任務切換:
-
一種就是
systick觸發PendSV例外,這是最經常使用的, -
另一種是主動進行切換任務,執行系統呼叫,比如普通任務可以使用taskYIELD()強制任務切換,中斷服務程式中使用
portYIELD_FROM_ISR()強制任務切換,
第一種
先說說第一種吧,就在systick中斷中呼叫xPortSysTickHandler();
下面是原始碼:
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
它的執行程序是這樣子的,屏蔽所有中斷,因為SysTick以最低的中斷優先級運行,所以當這個中斷執行時所有中斷必須被屏蔽,vPortRaiseBASEPRI();就是屏蔽所有中斷的,而且并不需要保存本次中斷的值,因為systick的中斷優先級是已知的,執行完直接恢復所有中斷即可,
在xTaskIncrementTick()中會對tick的計數值進行自加,然后檢查有沒有處于就緒態的最優先級任務,如果有,則回傳非零值,然后表示需要進行任務切換,而并非馬上進行任務切換,此處要注意,它只是向中斷狀態暫存器bit28位寫入1,只是將PendSV掛起,假如沒有比PendSV更高優先級的中斷,它才會進入PendSV中斷服務函式進行任務切換,
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
然后解除屏蔽所有中斷,
vPortClearBASEPRIFromISR();
第二種
另一種方法是主動進行任務切換,不管是使用taskYIELD()還是portYIELD_FROM_ISR(),最終都會執行下面的代碼:
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
這portYIELD()其實是一個宏定義來的,同樣是向中斷狀態暫存器bit28位寫入1,將PendSV掛起,然后等待任務的切換,
具體的任務切換原始碼
一直在說怎么進行任務切換的,好像還沒看到任務切換的原始碼啊,哎,下面來看看任務切換的真面目!!
__asm void xPortPendSVHandler(void)
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [r3]
stmdb r0!, {r4-r11} /* Save the remaining registers. */
str r0, [r2] /* Save the new top of stack into the first member of the TCB. */
stmdb sp!, {r3, r14}
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
bl vTaskSwitchContext
mov r0, #0
msr basepri, r0
ldmia sp!, {r3, r14}
ldr r1, [r3]
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */
msr psp, r0
isb
bx r14
nop
}
不是我不想看,是我看到匯編就頭大啊,這幾天我也在看原始碼,實在是頭大,
找到核心的函式看看就好啦,不管那么多,有興趣的可以研究一下中斷代碼,有不懂的也很歡迎你們來問我,一起研究研究,也是不錯的選擇,
下面是看重點的地方了:
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
這兩句代碼是關閉中斷的,關中斷就得干活了,嘿嘿嘿~
bl vTaskSwitchContext
BL是跳轉指令嘛,這個我還是有點懂的,
呼叫函式vTaskSwitchContext(),尋找新的任務運行,通過使變數pxCurrentTCB指向新的任務來實作任務切換,然后就是打開中斷,退出去了,
尋找下一個要運行任務
是不是感覺沒什么大不了的樣子,如果你是這樣子覺得的,可能還沒學到家,趕緊去看看FreeRTOS的原始碼,在config.h組態檔中是不是有一個叫做硬體查找下一個運行的任務呢?configUSE_PORT_OPTIMISED_TASK_SELECTION,這個在FreeRTOS中叫做特殊方法,其實也是硬體查找啦,但是并不是每種單片機都支持的,如果是不支持的話,只能選擇軟體查找的方法了,就是所謂的通用方法,通用方法我就不多說了,因為我用的是STM32,他是支持硬體方法的,這樣子效率更高,所以我也沒必要去研究他的軟體方法,假如有興趣的小伙伴可以研讀一下原始碼,有不懂的可以向我提問,原始碼如下:
#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 */
而硬體的方法原始碼則在下面:
#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 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
其方法是利用硬體提供的計算前導零指令CLZ,具體宏定義為:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
靜態變數uxTopReadyPriority包含了處于就緒態任務的最高優先級的資訊,因為FreeRTOS運行的永遠是處于最高優先級的運行態,而下個處于最高優先級的就緒態則必定會在下次任務切換的時候運行,uxTopReadyPriority使用每一位來表示任務是否處于就緒態,比如變數uxTopReadyPriority的bit0為1,則表示存在優先級為0的任務處于就緒態,bit6為1則表示存在優先級為6的任務處于就緒態,并且,由于bit0的優先級高于bit6,那么下個任務就是bit0的任務運行了(陣列越低優先級越高),由于32位整形數最多只有32位,因此使用這種特殊方法限定最大可用優先級數目為32,即優先級0~31,得到了下個處于最高優先級就緒態任務了,就呼叫listGET_OWNER_OF_NEXT_ENTRY來獲取下一個任務的串列項,然后將該串列項的任務控制塊TCB賦值給pxCurrentTCB,那么我們就得到下一個要運行的任務了,
至此,任務切換已經完成,
END
關注我

更多資料歡迎關注“物聯網IoT開發”公眾號!
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/33402.html
標籤:嵌入式
