?
摘要:操作系統實際上我們接觸的很多,比如說windows,安卓、IOS、linux都是一種操作系統。單片機也有它自己的操作系統,叫做實時操作系統。那么這種實時操作系統和我們用的這些系統有什么區別呢?
我們經常使用的這些實際上是非實時的操作系統。為什么說它是非實時的,因為它的內核實際上是對任務進行時間片輪轉的調度方式。比如說有3個任務,分別是任務A,任務B和任務C。那么在時間片輪轉的調度機制里,它會讓任務A運行一斷時間,然后切換到任務B,然后切換到任務C,這樣子不斷的輪轉。
![9e83c81c-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbSAW-jdAAEz5ArxhaM080.png)
那么這樣有一個什么缺點呢?如果有一臺自動駕駛的汽車里面任務C,是用來檢測障礙物和躲避障礙物的,如果任務C不能得到及時的執行的話,有可能這一臺自動駕駛的汽車就會撞到障礙物上,實際上這樣是非常危險。所以我們就出現了實時的操作系統,它支持搶占式調度機制,也就是說我們可以把任務C的優先級提高。這樣當任務C就緒的時候,就先運行任務C,就保證了任務C的實時性。在操作系統中,最基礎的功能就是實現任務調度。
接下來了解一下FreeRTOS,實時操作系統的任務調度。在了解實時操作系統之前,要先了解一下內核,這里用ARM Cortex‐M3內核作為模板。首先我們先來了解一下CPU寄存器,這個是CM3的CPU寄存器的表。CM3 擁有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,但是絕大多數的 16 位指令只能使用 R0‐R7(低組寄存器),而 32 位的Thumb‐2指令則可以訪問所有通用寄存器。特殊功能寄存器有預定義的功能,而且必須通過專用的指令來訪問。
![9e9a2832-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbSANEzCAAGhNU7MKEA018.png)
可以看得到,前面這里都是通用寄存器。它們分為有低位寄存器(所有指令都能訪問它們)和高位寄存器(只有很少的 16 位 Thumb 指令能訪問它們)。那么它們為什么要這樣分呢?實際上在ARM內核的早期版本,ARM指令和Thumb指令可以訪問的寄存器不一樣,所以就分有低位寄存器和高位寄存器。還有后面的R13、R14和R15分別是棧指針、連接寄存器和PC程序指針寄存器。
除此之外CM3還有一些特寄存器。
![9ea6d1e0-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbSAIOMtAACy62jc2p4910.png)
大家有沒有想過當CPU進入中斷的時候,實際上是相當于打斷了之前的任務。那么在執行完中斷之后,CPU又是如何返回到原來的任務?而保證原來的任務不丟失的呢?
![9eba3f78-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbSAfU2qAAGTCnY1T1o978.png)
![9ec960e8-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWAfm5vAABD1KFPxaE061.png)
在進入中斷之前,也就是在左半部分,我們先把CPU寄存器里面的值送入內存中,也稱為壓棧。然后再運行中斷服務函數,在運行中斷服務函數的時候,CPU寄存器會被改寫。但是這并沒有什么關系,因為當中斷結束之后,返回到原來的任務的時候,之前CPU寄存器的值就會被從內存中取出,也叫做彈棧。那么通過這樣一個機制,就保證了原來的進程的數據不丟失。
那么接下來我們來了解一下CM3的壓棧順序?
![9ed77f20-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWAEX-OAAEnWa3MPUA144.png)
上圖是Cortex-M3進入中斷時,硬件的壓棧順序。也就是說在它進入中斷的時候,硬件會自動把這幾個寄存器壓棧。分別是PC指針、xPSR特殊寄存器、R0到R3通用寄存器、R12通用寄存器,還有LR連接寄存器(保存函數的返回地址)會被壓入棧中。按照下面第三列的標號順序保存到內存中。
![9ed77f20-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWAEX-OAAEnWa3MPUA144.png)
那么在壓入棧成功之后,當中斷執行完成,返回到原來的進程中時,棧里面的內容就會被彈出到CPU寄存器中它的彈出順序和壓入順序剛好是相反的。也就是說先彈出LR,然后這樣依次往下這樣子彈出,因為棧是先進后出,所以它是這樣一個出棧順序。
前面我們知道CPU一共有R0-R15以及幾個特殊的寄存器。在中斷函數到來時上面幾個寄存器是硬件自動壓入棧中的,那么還有幾個是軟件壓入棧中的,這又如何理解?
舉個例子:
![9f036842-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWAUjf_AAMUXAxx_Zw179.png)
程序在執行
if(a<=b)
?a=b;
時候,突然來了中斷。任何程序,最終都會轉換為機器碼,上述C代碼可以轉換為右邊的匯編指令。
對于這4條指令,它們可能隨時被異常打斷,怎么保證異常處理完后,被打斷的程序還能正確運行?
這4條指令涉及R0、R1寄存器,程序被打斷時、恢復運行時,R0、R1要保持不變,執行完第3條指令時,比較結果保存在程序狀態寄存器PSR里,程序被打斷時、恢復運行時,程序狀態寄存器保持不變。這4條指令,讀取a、b內存,程序被打斷時、恢復運行時,a、b內存保持不變。內存保持不變,這很容易實現,程序不越界就可以。所以,關鍵在于R0、R1、程序狀態寄存器要保持不變(當然不止這些寄存器):
- 在處理異常前,把這些寄存器保存在棧中,這稱為保存現場,也就是壓棧。
- 在處理完異常后,從棧中恢復這些寄存器,這稱為恢復現場,也就是彈棧。
再舉一個例子:
void?A()
{
????B();
}
比如函數A調用函數B,函數A應該知道:R0-R3是用來傳參數給函數B的;函數B可以肆意修改R0-R3;函數A不要指望函數B幫你保存R0-R3;保存R0-R3,是函數A的事情;對于LR、PSR也是同樣的道理,保存它們是函數A的責任。由硬件幫我們完成。
對于函數B:我用到R4-R11中的某一個,我都會在函數入口保存、在函數返回前恢復,從內存中彈棧到CPU的寄存器中;保證在B函數調用前后,函數A看到的R4-R11保存不變。
假設函數B就是異常/中斷處理函數,函數B本身能保證R4-R11不變,那么保存現場時,硬件只需要保存R0-R3,R12,LR,PSR和PC這8個寄存器。
那么接下來我們來了解一下CM3的兩種特殊中斷機制。當CM3開始響應一個中斷時,會在它看不見的體內奔涌起三股暗流:
- 入棧:把8個寄存器的值壓入棧。
- 取向量:從向量表中找出對應的服務程序入口地址。
- 選擇堆棧指針MSP/PSP,更新堆棧指針SP,更新連接寄存器LR,更新程序計數器PC。
第一種叫做咬尾中斷
我們知道,在進入中斷的時候需要執行入棧,而退出中斷的時候需要執行出棧。那么當兩個中斷來臨的時候,像這樣在第一個中斷執行完成之后,要執行第二個中斷。在CM3 處理器內核中是不會再執行出棧和入棧的。也就是說這里節省了出棧和入棧的時間,實際上相當于第2個中斷把第一個中斷的尾巴咬掉。也就是沒有讓它再出棧,所以這就被稱為咬尾中斷。
![9f1043aa-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWAY8R-AAEsBza8pbo853.png)
第二種中斷機制叫做晚到中斷
晚到中斷就是說,當有一個高優限級的任務來臨時,之前低優先級的任務取向量還沒有完成的時候(之前低優先級的任務還沒有從向量表中找出對應的服務程序入口地址),那么這一次壓棧就是為高優先級任務做的。也就是說就算高優先級的中斷晚到了,它仍然可以用低優先級中斷壓入的棧。
![9f241ce0-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWALV7fAAF5dhl52r8435.png)
CM3 處理器內核中斷表
在實時操作系統中,經常用到的是這三個中斷 PendSV、Systick、SVC。
![9f30e240-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWASserAAAydsc45Go878.png)
那么在FreeRTOS中Systick這個中斷是用來提供實時操作系統的時鐘周期的。而PendSV這個是可懸掛中斷,是用來切換進程的。SVC在FreeRTOS中只用了一次,也就是啟動第一個進程的時候用到了它。
__asm?void?vPortSVCHandler(?void?)
{
/*?*INDENT-OFF*?*/
????PRESERVE8
????ldr?r3,?=?pxCurrentTCB???//取出當前的任務控制塊
????ldr?r1,?[?r3?]?//使用?pxCurrentTCBConst?獲取?pxCurrentTCB?地址
????ldr?r0,?[?r1?]?//pxCurrentTCB?中的第一項是棧頂任務
????ldmia?r0?!,?{?r4?-?r11?}?//手動將R4-R11,R14寄存器壓棧
????msr?psp,?r0????//恢復任務棧指針
????isb
????mov?r0,?#?0
????msr?basepri,?r0?//打開所有的中斷
????orr?r14,?#?0xd
????bx?r14
/*?*INDENT-ON*?*/
}
![9f464220-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbWADs0JAAO75VDNu3Q616.png)
那么有些人可能就會問了,為什么我不直接在Systick中切換任務呢?而是要在PendSV中切換任務呢?那我們就可以來看一下:
![9f566c68-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbaALam9AAGXrN2fZxQ851.png)
如果在Systick中斷到來時,前面有一個中斷正在執行,也就是這里的IRQ正在執行。那它就會被打斷,然后Systick執行上下文來切換,這時候切換到任務b,它要等待一斷時間直到下一次上下文切換,切換回原來IRQ這個中斷執行的內容。這樣中斷才能被執行完成,但是這樣我們可以看得到,中斷被嚴重的耽誤了,所以這樣做實際上是不方便。而且容易出錯的。
![9f783a00-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbaAQVEUAAHxElJBrB4032.png)
這時候它們就想出一種辦法,說我在Systick中我判斷這個時候有沒有中斷在執行,如果有那么我們就不切換,如果沒有我們就切換。這樣呢實際上也會造成一個問題,就是如果這個中斷函數的中斷時間和Systick差不多,比如說如果這是一個定時器中斷,這是Systick系統時鐘中斷。它們的中斷周期都是1毫秒,那么它們經常就會面臨著兩個同時到來的情況。這樣就有可能導致進程遲遲無法切換,導致了延誤的產生,所以這樣做也不是很好。
所以就出現了PendSV可懸掛中斷
![9f86e028-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbaAZhFwAAGsThTEDOw160.png)
在這種中斷中有什么好處呢?我們可以看得到,在Systick中它只將PendSV的中斷位掛起,也就是說,它不執行經常切換的這個操作。而是等到后面,當所有的中斷執行完成之后在PendSV中執行上下文切換,這樣既保證了任務的及時切換,也保證了中斷的及時執行。PendSV異常會自動延遲上下文切換的請求,直到其它的ISR都完成了處理后才放行。為實現這個機制,需要把PendSV編程為最低優先級的異常。如果 OS 檢測到某 IRQ正在活動并且被Systick搶占,它將懸起一個PendSV異常,以便緩期執行上下文切換。
那么在PendSV中到底是怎么樣進行進程切換?在這里用的是匯編語言寫的。
__asm?void?xPortPendSVHandler(?void?)
{
????extern?uxCriticalNesting;
????extern?pxCurrentTCB;
????extern?vTaskSwitchContext;
????PRESERVE8
????mrs?r0,?psp//將當前進程棧指針保存在R0寄存器中
????isb
????ldr?r3,?=pxCurrentTCB?//取出當前的任務控制塊
????ldr?r2,?[?r3?]?//將任務控制塊地址保存在R2寄存器中
????stmdb?r0?!,?{?r4?-?r11?}?//手動將R4-R11,R14寄存器壓棧
????str?r0,?[?r2?]?//將當前的棧頂地址寫入控制塊
????stmdb?sp?!,?{?r3,?r14?}
????mov?r0,?#configMAX_SYSCALL_INTERRUPT_PRIORITY//將這個宏所代表的立即數寫入R0寄存器,而這個宏是用戶想要屏蔽的最高優先級中斷
????msr?basepri,?r0?//將剛剛R0寄存器的值寫入特殊寄存器basepriority中,這個寄存器可以對中斷進行細膩的控制它可以將高于這個優先級的中斷不屏蔽,而低于這個優先級的中斷屏蔽
????dsb
????isb
????bl?vTaskSwitchContext
????mov?r0,?#0
????msr?basepri,?r0?//取消中斷屏蔽
????ldmia?sp?!,?{?r3,?r14?}?//將當前的棧指針從R3寄存器中恢復,這個時候R3寄存器存的值是剛剛從下一任務控制塊取
????ldr?r1,?[?r3?]
????ldr?r0,?[?r1?]?//將新任務的棧頂保存到R0寄存器中
????ldmia?r0?!,?{?r4?-?r11?}?//手動將R4-R11以及R14寄存器彈棧
????msr?psp,?r0
????isb
????bx?r14??//異常返回,返回后硬件將自動恢復其余寄存器,并且使用進程棧指針。
????nop
/*?*INDENT-ON*?*/
}
那么我們剛剛已經了解到了,FreeRTOS實時操作系統的最基本的功能任務切換。但是如果想做一個完善的實時操作系統,還需要非常多的其它的東西,比如說列表和列表項、任務通知、低功耗模式任務控制塊及對堆棧處理內存管理、空閑任務、對信號量、軟件定時器、事件標志組等等這些內容。
參考內容:
《FreeRTOS源碼詳解與應用開發》
《ARM?Cortex-M3權威指南》
看看程序中具體是怎么實現中斷的
下面這張表來自《ARM Cortex-M3權威指南》
![9fa826b6-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbaARUtXAARRUZDA1qA984.png)
在Cortex-M3中有15個異常中斷,對應在stm32中如下圖
![9fbc4506-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbaAIT4aAAQL7ikPvYE055.png)
![9fd39026-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbeARzreAAPXgS5_s5E944.png)
在啟動文件中不僅有異常,還有中斷,其實中斷也是屬于一種異常。我們說中斷的時候,更多的說的是某一種設備發出的信號比如GPIO模塊:發信號給CPU比如12C控制器發送完數據,發出信號給CPU比如UART接收到一個數據之后也會產生中斷注意了:中斷屬于異常。除了中斷外其他異常一般有哪些呢:復位:也是一種異常,發生了各種錯誤:屬于異常。
當我們板子復位的時候CPU會執行中斷向量表中的Reset_Handler執行這個函數。
![9fe6f0f8-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbeAdwZDAALZvM1BlJk833.png)
當我們板子看門狗中斷時的時候CPU會執行中斷向量表中的WWDG_IRQHandler執行這個函數。
![9ffe0536-0820-11ed-ba43-dac502259ad0.png](https://file1.elecfans.com//web2/M00/95/C0/wKgZomTnDbeABTg_AAONKJYA1J4792.png)
你肯定有這樣一個疑問?CPU怎么知道跳轉到中斷向量表中的執行哪一個函數呢?
這肯定是硬件確定,因為這時候軟件還沒有開始執行,硬件確定當前發生的是哪一個異常,哪一個中斷,當恢復的時候由軟件觸發、硬件恢復。
/**
??*?@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)
{
}
好了,現在你知道MCU的中斷流程和RTOS的的基本原理了吧?
評論