大家晚上好,我是傑傑,最近挺忙的,好久沒有更新了,今天周末就吐血更新一下吧!
前言
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開發”公眾號!