摘要:操作系統(tǒng)實際上我們接觸的很多,比如說windows,安卓、IOS、linux都是一種操作系統(tǒng)。單片機也有它自己的操作系統(tǒng),叫做實時操作系統(tǒng)。那么這種實時操作系統(tǒng)和我們用的這些系統(tǒng)有什么區(qū)別呢?
我們經(jīng)常使用的這些實際上是非實時的操作系統(tǒng)。為什么說它是非實時的,因為它的內(nèi)核實際上是對任務(wù)進(jìn)行時間片輪轉(zhuǎn)的調(diào)度方式。比如說有3個任務(wù),分別是任務(wù)A,任務(wù)B和任務(wù)C。那么在時間片輪轉(zhuǎn)的調(diào)度機制里,它會讓任務(wù)A運行一斷時間,然后切換到任務(wù)B,然后切換到任務(wù)C,這樣子不斷的輪轉(zhuǎn)。
兩個任務(wù)間通過 Systick 輪轉(zhuǎn)調(diào)度的簡單模式
那么這樣有一個什么缺點呢?如果有一臺自動駕駛的汽車?yán)锩嫒蝿?wù)C,是用來檢測障礙物和躲避障礙物的,如果任務(wù)C不能得到及時的執(zhí)行的話,有可能這一臺自動駕駛的汽車就會撞到障礙物上,實際上這樣是非常危險。所以我們就出現(xiàn)了實時的操作系統(tǒng),它支持搶占式調(diào)度機制,也就是說我們可以把任務(wù)C的優(yōu)先級提高。這樣當(dāng)任務(wù)C就緒的時候,就先運行任務(wù)C,就保證了任務(wù)C的實時性。在操作系統(tǒng)中,最基礎(chǔ)的功能就是實現(xiàn)任務(wù)調(diào)度。
接下來了解一下FreeRTOS,實時操作系統(tǒng)的任務(wù)調(diào)度。在了解實時操作系統(tǒng)之前,要先了解一下內(nèi)核,這里用ARM Cortex‐M3內(nèi)核作為模板。首先我們先來了解一下CPU寄存器,這個是CM3的CPU寄存器的表。CM3 擁有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,但是絕大多數(shù)的 16 位指令只能使用 R0‐R7(低組寄存器),而 32 位的Thumb‐2指令則可以訪問所有通用寄存器。特殊功能寄存器有預(yù)定義的功能,而且必須通過專用的指令來訪問。
Cortex‐M3 的寄存器組
可以看得到,前面這里都是通用寄存器。它們分為有低位寄存器(所有指令都能訪問它們)和高位寄存器(只有很少的 16 位 Thumb 指令能訪問它們)。那么它們?yōu)槭裁匆@樣分呢?實際上在ARM內(nèi)核的早期版本,ARM指令和Thumb指令可以訪問的寄存器不一樣,所以就分有低位寄存器和高位寄存器。還有后面的R13、R14和R15分別是棧指針、連接寄存器和PC程序指針寄存器。
除此之外CM3還有一些特寄存器。
?
大家有沒有想過當(dāng)CPU進(jìn)入中斷的時候,實際上是相當(dāng)于打斷了之前的任務(wù)。那么在執(zhí)行完中斷之后,CPU又是如何返回到原來的任務(wù)?而保證原來的任務(wù)不丟失的呢?
?
?
在進(jìn)入中斷之前,也就是在左半部分,我們先把CPU寄存器里面的值送入內(nèi)存中,也稱為壓棧。然后再運行中斷服務(wù)函數(shù),在運行中斷服務(wù)函數(shù)的時候,CPU寄存器會被改寫。但是這并沒有什么關(guān)系,因為當(dāng)中斷結(jié)束之后,返回到原來的任務(wù)的時候,之前CPU寄存器的值就會被從內(nèi)存中取出,也叫做彈棧。那么通過這樣一個機制,就保證了原來的進(jìn)程的數(shù)據(jù)不丟失。
那么接下來我們來了解一下CM3的壓棧順序?
入棧順序以及入棧后堆棧中的內(nèi)容 第3列所示
上圖是Cortex-M3進(jìn)入中斷時,硬件的壓棧順序。也就是說在它進(jìn)入中斷的時候,硬件會自動把這幾個寄存器壓棧。分別是PC指針、xPSR特殊寄存器、R0到R3通用寄存器、R12通用寄存器,還有LR連接寄存器(保存函數(shù)的返回地址)會被壓入棧中。按照下面第三列的標(biāo)號順序保存到內(nèi)存中。
那么在壓入棧成功之后,當(dāng)中斷執(zhí)行完成,返回到原來的進(jìn)程中時,棧里面的內(nèi)容就會被彈出到CPU寄存器中它的彈出順序和壓入順序剛好是相反的。也就是說先彈出LR,然后這樣依次往下這樣子彈出,因為棧是先進(jìn)后出,所以它是這樣一個出棧順序。
前面我們知道CPU一共有R0-R15以及幾個特殊的寄存器。在中斷函數(shù)到來時上面幾個寄存器是硬件自動壓入棧中的,那么還有幾個是軟件壓入棧中的,這又如何理解?
舉個例子:
程序在執(zhí)行
?
if(a<=b) ?a=b;
?
時候,突然來了中斷。任何程序,最終都會轉(zhuǎn)換為機器碼,上述C代碼可以轉(zhuǎn)換為右邊的匯編指令。
對于這4條指令,它們可能隨時被異常打斷,怎么保證異常處理完后,被打斷的程序還能正確運行?
這4條指令涉及R0、R1寄存器,程序被打斷時、恢復(fù)運行時,R0、R1要保持不變,執(zhí)行完第3條指令時,比較結(jié)果保存在程序狀態(tài)寄存器PSR里,程序被打斷時、恢復(fù)運行時,程序狀態(tài)寄存器保持不變。這4條指令,讀取a、b內(nèi)存,程序被打斷時、恢復(fù)運行時,a、b內(nèi)存保持不變。內(nèi)存保持不變,這很容易實現(xiàn),程序不越界就可以。所以,關(guān)鍵在于R0、R1、程序狀態(tài)寄存器要保持不變(當(dāng)然不止這些寄存器):
在處理異常前,把這些寄存器保存在棧中,這稱為保存現(xiàn)場,也就是壓棧。
在處理完異常后,從棧中恢復(fù)這些寄存器,這稱為恢復(fù)現(xiàn)場,也就是彈棧。
再舉一個例子:
?
void?A() { ????B(); }
?
比如函數(shù)A調(diào)用函數(shù)B,函數(shù)A應(yīng)該知道:R0-R3是用來傳參數(shù)給函數(shù)B的;函數(shù)B可以肆意修改R0-R3;函數(shù)A不要指望函數(shù)B幫你保存R0-R3;保存R0-R3,是函數(shù)A的事情;對于LR、PSR也是同樣的道理,保存它們是函數(shù)A的責(zé)任。由硬件幫我們完成。
對于函數(shù)B:我用到R4-R11中的某一個,我都會在函數(shù)入口保存、在函數(shù)返回前恢復(fù),從內(nèi)存中彈棧到CPU的寄存器中;保證在B函數(shù)調(diào)用前后,函數(shù)A看到的R4-R11保存不變。
假設(shè)函數(shù)B就是異常/中斷處理函數(shù),函數(shù)B本身能保證R4-R11不變,那么保存現(xiàn)場時,硬件只需要保存R0-R3,R12,LR,PSR和PC這8個寄存器。
那么接下來我們來了解一下CM3的兩種特殊中斷機制。當(dāng)CM3開始響應(yīng)一個中斷時,會在它看不見的體內(nèi)奔涌起三股暗流:
入棧:把8個寄存器的值壓入棧。
取向量:從向量表中找出對應(yīng)的服務(wù)程序入口地址。
選擇堆棧指針MSP/PSP,更新堆棧指針SP,更新連接寄存器LR,更新程序計數(shù)器PC。
第一種叫做咬尾中斷
我們知道,在進(jìn)入中斷的時候需要執(zhí)行入棧,而退出中斷的時候需要執(zhí)行出棧。那么當(dāng)兩個中斷來臨的時候,像這樣在第一個中斷執(zhí)行完成之后,要執(zhí)行第二個中斷。在CM3 處理器內(nèi)核中是不會再執(zhí)行出棧和入棧的。也就是說這里節(jié)省了出棧和入棧的時間,實際上相當(dāng)于第2個中斷把第一個中斷的尾巴咬掉。也就是沒有讓它再出棧,所以這就被稱為咬尾中斷。
第二種中斷機制叫做晚到中斷
晚到中斷就是說,當(dāng)有一個高優(yōu)限級的任務(wù)來臨時,之前低優(yōu)先級的任務(wù)取向量還沒有完成的時候(之前低優(yōu)先級的任務(wù)還沒有從向量表中找出對應(yīng)的服務(wù)程序入口地址),那么這一次壓棧就是為高優(yōu)先級任務(wù)做的。也就是說就算高優(yōu)先級的中斷晚到了,它仍然可以用低優(yōu)先級中斷壓入的棧。
CM3 處理器內(nèi)核中斷表
在實時操作系統(tǒng)中,經(jīng)常用到的是這三個中斷 PendSV、Systick、SVC。
那么在FreeRTOS中Systick這個中斷是用來提供實時操作系統(tǒng)的時鐘周期的。而PendSV這個是可懸掛中斷,是用來切換進(jìn)程的。SVC在FreeRTOS中只用了一次,也就是啟動第一個進(jìn)程的時候用到了它。
?
__asm?void?vPortSVCHandler(?void?) { /*?*INDENT-OFF*?*/ ????PRESERVE8 ????ldr?r3,?=?pxCurrentTCB???//取出當(dāng)前的任務(wù)控制塊 ????ldr?r1,?[?r3?]?//使用?pxCurrentTCBConst?獲取?pxCurrentTCB?地址 ????ldr?r0,?[?r1?]?//pxCurrentTCB?中的第一項是棧頂任務(wù) ????ldmia?r0?!,?{?r4?-?r11?}?//手動將R4-R11,R14寄存器壓棧 ????msr?psp,?r0????//恢復(fù)任務(wù)棧指針 ????isb ????mov?r0,?#?0 ????msr?basepri,?r0?//打開所有的中斷 ????orr?r14,?#?0xd ????bx?r14 /*?*INDENT-ON*?*/ }

系統(tǒng)異常清單
那么有些人可能就會問了,為什么我不直接在Systick中切換任務(wù)呢?而是要在PendSV中切換任務(wù)呢?那我們就可以來看一下:
發(fā)生 IRQ 時上下文切換的問題
如果在Systick中斷到來時,前面有一個中斷正在執(zhí)行,也就是這里的IRQ正在執(zhí)行。那它就會被打斷,然后Systick執(zhí)行上下文來切換,這時候切換到任務(wù)b,它要等待一斷時間直到下一次上下文切換,切換回原來IRQ這個中斷執(zhí)行的內(nèi)容。這樣中斷才能被執(zhí)行完成,但是這樣我們可以看得到,中斷被嚴(yán)重的耽誤了,所以這樣做實際上是不方便。而且容易出錯的。
這時候它們就想出一種辦法,說我在Systick中我判斷這個時候有沒有中斷在執(zhí)行,如果有那么我們就不切換,如果沒有我們就切換。這樣呢實際上也會造成一個問題,就是如果這個中斷函數(shù)的中斷時間和Systick差不多,比如說如果這是一個定時器中斷,這是Systick系統(tǒng)時鐘中斷。它們的中斷周期都是1毫秒,那么它們經(jīng)常就會面臨著兩個同時到來的情況。這樣就有可能導(dǎo)致進(jìn)程遲遲無法切換,導(dǎo)致了延誤的產(chǎn)生,所以這樣做也不是很好。
所以就出現(xiàn)了PendSV可懸掛中斷
使用PendSV控制上下文切換
在這種中斷中有什么好處呢?我們可以看得到,在Systick中它只將PendSV的中斷位掛起,也就是說,它不執(zhí)行經(jīng)常切換的這個操作。而是等到后面,當(dāng)所有的中斷執(zhí)行完成之后在PendSV中執(zhí)行上下文切換,這樣既保證了任務(wù)的及時切換,也保證了中斷的及時執(zhí)行。PendSV異常會自動延遲上下文切換的請求,直到其它的ISR都完成了處理后才放行。為實現(xiàn)這個機制,需要把PendSV編程為最低優(yōu)先級的異常。如果 OS 檢測到某 IRQ正在活動并且被Systick搶占,它將懸起一個PendSV異常,以便緩期執(zhí)行上下文切換。
那么在PendSV中到底是怎么樣進(jìn)行進(jìn)程切換?在這里用的是匯編語言寫的。
?
__asm?void?xPortPendSVHandler(?void?) { ????extern?uxCriticalNesting; ????extern?pxCurrentTCB; ????extern?vTaskSwitchContext; ????PRESERVE8 ????mrs?r0,?psp//將當(dāng)前進(jìn)程棧指針保存在R0寄存器中 ????isb ????ldr?r3,?=pxCurrentTCB?//取出當(dāng)前的任務(wù)控制塊 ????ldr?r2,?[?r3?]?//將任務(wù)控制塊地址保存在R2寄存器中 ????stmdb?r0?!,?{?r4?-?r11?}?//手動將R4-R11,R14寄存器壓棧 ????str?r0,?[?r2?]?//將當(dāng)前的棧頂?shù)刂穼懭肟刂茐K ????stmdb?sp?!,?{?r3,?r14?} ????mov?r0,?#configMAX_SYSCALL_INTERRUPT_PRIORITY//將這個宏所代表的立即數(shù)寫入R0寄存器,而這個宏是用戶想要屏蔽的最高優(yōu)先級中斷 ????msr?basepri,?r0?//將剛剛R0寄存器的值寫入特殊寄存器basepriority中,這個寄存器可以對中斷進(jìn)行細(xì)膩的控制它可以將高于這個優(yōu)先級的中斷不屏蔽,而低于這個優(yōu)先級的中斷屏蔽 ????dsb ????isb ????bl?vTaskSwitchContext ????mov?r0,?#0 ????msr?basepri,?r0?//取消中斷屏蔽 ????ldmia?sp?!,?{?r3,?r14?}?//將當(dāng)前的棧指針從R3寄存器中恢復(fù),這個時候R3寄存器存的值是剛剛從下一任務(wù)控制塊取 ????ldr?r1,?[?r3?] ????ldr?r0,?[?r1?]?//將新任務(wù)的棧頂保存到R0寄存器中 ????ldmia?r0?!,?{?r4?-?r11?}?//手動將R4-R11以及R14寄存器彈棧 ????msr?psp,?r0 ????isb ????bx?r14??//異常返回,返回后硬件將自動恢復(fù)其余寄存器,并且使用進(jìn)程棧指針。 ????nop /*?*INDENT-ON*?*/ }
?
那么我們剛剛已經(jīng)了解到了,F(xiàn)reeRTOS實時操作系統(tǒng)的最基本的功能任務(wù)切換。但是如果想做一個完善的實時操作系統(tǒng),還需要非常多的其它的東西,比如說列表和列表項、任務(wù)通知、低功耗模式任務(wù)控制塊及對堆棧處理內(nèi)存管理、空閑任務(wù)、對信號量、軟件定時器、事件標(biāo)志組等等這些內(nèi)容。
看看程序中具體是怎么實現(xiàn)中斷的
下面這張表來自《ARM Cortex-M3權(quán)威指南》
在Cortex-M3中有15個異常中斷,對應(yīng)在stm32中如下圖
在啟動文件中不僅有異常,還有中斷,其實中斷也是屬于一種異常。我們說中斷的時候,更多的說的是某一種設(shè)備發(fā)出的信號比如GPIO模塊:發(fā)信號給CPU比如12C控制器發(fā)送完數(shù)據(jù),發(fā)出信號給CPU比如UART接收到一個數(shù)據(jù)之后也會產(chǎn)生中斷注意了:中斷屬于異常。除了中斷外其他異常一般有哪些呢:復(fù)位:也是一種異常,發(fā)生了各種錯誤:屬于異常。
當(dāng)我們板子復(fù)位的時候CPU會執(zhí)行中斷向量表中的Reset_Handler執(zhí)行這個函數(shù)。
當(dāng)我們板子看門狗中斷時的時候CPU會執(zhí)行中斷向量表中的WWDG_IRQHandler執(zhí)行這個函數(shù)。
你肯定有這樣一個疑問?CPU怎么知道跳轉(zhuǎn)到中斷向量表中的執(zhí)行哪一個函數(shù)呢?
這肯定是硬件確定,因為這時候軟件還沒有開始執(zhí)行,硬件確定當(dāng)前發(fā)生的是哪一個異常,哪一個中斷,當(dāng)恢復(fù)的時候由軟件觸發(fā)、硬件恢復(fù)。
?
/** ??*?@brief??This?function?handles?NMI?exception. ??*?@param??None ??*?@retval?None ??*/ void?NMI_Handler(void) { } /** ??*?@brief??This?function?handles?Hard?Fault?exception. ??*?@param??None ??*?@retval?None ??*/ void?HardFault_Handler(void) { ??/*?Go?to?infinite?loop?when?Hard?Fault?exception?occurs?*/ ??while?(1) ??{ ??} } /** ??*?@brief??This?function?handles?Memory?Manage?exception. ??*?@param??None ??*?@retval?None ??*/ void?MemManage_Handler(void) { ??/*?Go?to?infinite?loop?when?Memory?Manage?exception?occurs?*/ ??while?(1) ??{ ??} } /** ??*?@brief??This?function?handles?Bus?Fault?exception. ??*?@param??None ??*?@retval?None ??*/ void?BusFault_Handler(void) { ??/*?Go?to?infinite?loop?when?Bus?Fault?exception?occurs?*/ ??while?(1) ??{ ??} } /** ??*?@brief??This?function?handles?Usage?Fault?exception. ??*?@param??None ??*?@retval?None ??*/ void?UsageFault_Handler(void) { ??/*?Go?to?infinite?loop?when?Usage?Fault?exception?occurs?*/ ??while?(1) ??{ ??} } /** ??*?@brief??This?function?handles?SVCall?exception. ??*?@param??None ??*?@retval?None ??*/ void?SVC_Handler(void) { } /** ??*?@brief??This?function?handles?Debug?Monitor?exception. ??*?@param??None ??*?@retval?None ??*/ void?DebugMon_Handler(void) { } /** ??*?@brief??This?function?handles?PendSVC?exception. ??*?@param??None ??*?@retval?None ??*/ void?PendSV_Handler(void) { } /** ??*?@brief??This?function?handles?SysTick?Handler. ??*?@param??None ??*?@retval?None ??*/ void?SysTick_Handler(void) { }
?
好了,現(xiàn)在你知道MCU的中斷流程和RTOS的的基本原理了吧?
審核編輯:劉清
評論