大家晚上好,我是傑傑,最近挺忙的,好久沒有更新了,今天周末就吐血更新一下吧! 前言 是一個是實時內核,任務是程式執行的最小單位,也是調度器處理的基本單位,移植了 ,則避免不了對任務的管理,在多個任務運行的時候,任務切換顯得尤為重要。而任務切換的效率會決定了系統的穩定性與效率。 的任務切換是幹嘛的呢, ...
大家晚上好,我是傑傑,最近挺忙的,好久沒有更新了,今天周末就吐血更新一下吧!
前言
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開發”公眾號!