freertos之因此可以準確的按照配置的時間片進行任務調度徹底依靠硬件支持。硬件上的某個計數器會提供週期性中斷,在中斷處理中解決任務調度 如:task1切換到task2,task2再切換到task1,如此循環往復,在外部就表現的如同多個任務在一塊兒執行。編程
在CM3/CM4上有提供內部異常定時器:systick。使用systick做爲實時系統的運行心臟再好不過,由於人家的名字就叫作系統滴答定時器,可謂門當戶對。其它定時器也能夠完成相似功能,由於freertos只關注可否提供週期中斷,並不會在乎中斷產生着是白貓仍是黑貓。使用systick的一個好處是在開發低功耗功能時它將不會受到影響。api
在調度器啓動以前已經使能了systick,systick中斷髮生後進入xPortSysTickHandler異常。在xPortSysTickHandler中沒有發生任務切換,它只是利用xTaskIncrementTick函數處理了readylist與delaylist上的任務結點,須要進行任務切換時會交給pendSV來完成。函數
(調度器流程圖)(拖動放大)源碼分析
在進入xTaskIncrementTick以前首先屏蔽了中斷,防止發生中斷嵌套破壞內核鏈表。spa
xTaskIncrementTick中率先自增了xTickCount。xTickCount可以準確表示systick發生了多少次中斷,它也理所固然成爲freertos中的時間基石。不少api調用與時間有關,例如 某個任務須要等待一個信號量,能夠給它設置等待超時,超過必定時間放棄等待;或者某個任務須要被delay一段時間再被從新調度。線程
調度器使用xTickCount記錄內核啓動時間,一個TCB使用節點上的xItemValue值表示下一次事件觸發的時間。假設當前xTickCount值爲5,systick 1ms觸發一次中斷,此時運行的某個任務使用了vTaskDelay來掛起10ms,那麼TCB上的xItemValue會被賦值爲15,等到xTickCount累加到15時將任務拉回就緒態。指針
時間值一直累加總會有溢出的狀況發生。爲了更快的故意達到此效果,假設TCB上的xItemValue值與xTickCount都使用1字節存儲。當xTickCount值爲230時,某個任務調用了vTaskDelay掛起30ms,xItemValue值累加後發生溢出變成了4,調度器再次比較xTickCount與xItemValue值時就會認爲此任務到達喚醒時間(小於xTickCount值都會被喚醒),vTaskDelay成爲無效的調用。事件
xDelayedTaskList1和xDelayedTaskList2解決了tick值溢出的問題,當累加後xTickCount與xItemValue值比累加以前還要小時能夠判斷爲溢出發生,插入的鏈表也要發生變化,如圖:內存
(拖動放大)資源
pxDelayedTaskList與pxOverflowDelayedTaskList兩個指針分別指向xDelayedTaskList1和xDelayedTaskList2兩條鏈表,如有xItemValue值發生溢出則插入pxOverflowDelayedTaskList中。
pxDelayedTaskList鏈表上的任務將等待xTickCount自增到某一值後從新被喚醒,然而
pxOverflowDelayedTaskList鏈表上的任務永遠不會被喚醒,直到xTickCount值發生溢出將兩個指針互換:
(拖動放大)
發生交換後,原來pxOverflowDelayedTaskList上的結點就能以準確的tick值被喚醒,如此重複,整個鏈表就能得以正確運行調度。
每當有節點插入pxDelayedTaskList時會更新一次xNextTaskUnblockTime變量值,在pxDelayedTaskList上的任務都是須要「睡眠」一段時間,等到喚醒後從新插入pxReadyTasksLists參與調度。pxDelayedTaskList上的結點是按照升序插入,xNextTaskUnblockTime值會一直等於鏈表上第一個節點的值,也就是須要遍歷pxDelayedTaskList鏈表的最小時間,這樣能夠減小在systick中斷裏遍歷pxDelayedTaskList的次數。
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
檢查當前優先級的任務數是否大於1,大於的話則使能pendSV。有了這行代碼,在freertos中多個優先級相同的高優先級任務會按照時間片分配cpu資源。若是當前優先級任務數爲1的話則放棄調度,cpu資源將會一直被當前任務佔據,直到它被某個操做掛起才能輪到下一個較低優先級的任務執行。
PendSV是CM3/CM4中可以被「緩期執行」一個異常,懸起 PendSV方法是手工向 NVIC 的 PendSV 懸起寄存器中寫 1。
CM3權威指南:
PendSV 的典型使用場合是在上下文切換時(在不一樣任務之間切換)。
須要把 PendSV 編程爲最低優先級的異常(PendSV中斷返回時須要置cpu狀態爲線程模式(LR位段控制),若是 OS 在某中斷活躍時嘗試切入線程模式,將觸犯用法 fault 異常)。
xPortPendSVHandler是pendSV異常處理函數,由彙編實現,主要更新了重要寄存器和調用vTaskSwitchContext進行任務切換。
前面準備了一大堆中斷屏蔽以及鏈表遍歷等工做,都爲的是這一刻的輝煌!在vTaskSwitchContext中pxCurrentTCB值終於被替換掉,新的任務被分配到cpu資源,調度由此產生。
在更換pxCurrentTCB以前還要檢查當前任務棧是否溢出,本着認真負責的態度它還檢查了兩次。開發者在開發過程當中可能只計算了本身的變量在任務中消耗的棧空間,而且「恰到好處的」分配了任務棧空間,若異常發生時由硬件自動壓入的幾個寄存器值頗有可能會超出棧的設定範圍(異常響應時若當前使用PSP則壓入PSP,若使用MSP則壓入MSP,進入異常後一直使用MSP),一個TCB中會記錄當前任務棧的棧頂和棧底,若是棧頂地址超過棧底則發生溢出。因此只要內存夠仍是把棧搞得儘可能大比較好。
獲取當前最高任務優先級後,從pxReadyTasksLists中取出新TCB賦給pxCurrentTCB指針就完成了他的使命。
xPortPendSVHandler:
mrs r0, psp //保存當前任務PSP地址到R0中
ldr r3, pxCurrentTCBConst //獲取pxCurrentTCBConst指針地址
ldr r2, [r3] //R2被賦予當前TCB地址
stmdb r0!, {r4-r11} //R4-R11壓入PSP
str r0, [r2] //新的PSP地址存入到TCB中
stmdb sp!, {r3, r14} //壓R3,R14值至MSP
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY //或缺中斷屏蔽閾值
msr basepri, r0 //中斷屏蔽
dsb
isb
bl vTaskSwitchContext //調用vTaskSwitchContext
mov r0, #0
msr basepri, r0 //打開中斷
dsb
isb
ldmia sp!, {r3, r14} //出棧R3,R14
ldr r1, [r3] //R3依然是pxCurrentTCBConst指針地址,R1變爲新TCB地址
ldr r0, [r1] //R0值成爲新TCB的棧地址(該TCB發生上一次調度的PSP值)
ldmia r0!, {r4-r11} //出PSP
msr psp, r0 //更新PSP
bx r14 //LR位段控制返回線程模式並使用PSP做爲SP
.align 2
pxCurrentTCBConst: .word pxCurrentTCB
調度時不僅是更換pxCurrentTCB值,R0-R15等寄存器都要進行大換血才能恢復到新任務的上一次運行環境:
(拖動放大)