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

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