這是簡單STM32 OS的第一章stm32超輕量操作系統(tǒng)之搶占式內(nèi)核
在這個最簡程序中只有兩個任務(wù)交替執(zhí)行盐碱,任務(wù)一和任務(wù)二瓮顽,兩個任務(wù)分別控制兩個LED燈的亮滅。只是完成了最簡單的任務(wù)切換功能聘惦。
這一版的程序中沒有加入像一般的OS中調(diào)用延時函數(shù)時會發(fā)生任務(wù)調(diào)度的功能儒恋,也沒有優(yōu)先級,沒有時間片禀酱,只是兩個任務(wù)不斷交替執(zhí)行牧嫉。在第二章中會加入搶占式內(nèi)核和延時功能减途。
STM32的任務(wù)調(diào)度可以有兩種方式
1.通過systick_handler定時器調(diào)度
2.執(zhí)行一個系統(tǒng)調(diào)用
cortexM3的寄存器只有16個鳍置,cortexM4除了這16個還有很多浮點運算和MPU單元送淆,如果不用這些單元它和M4沒有區(qū)別,我是用的cortexM4內(nèi)核的STM32F407辟拷,因為沒有用浮點運算和MPU保護單元因此OS也和M3內(nèi)核兼容衫冻。
因此在任務(wù)調(diào)度的過程中谒出,也是不斷的保存現(xiàn)在任務(wù)的這16個寄存器,彈出下一個任務(wù)的16個寄存器考赛。
任務(wù)調(diào)度的步驟總結(jié)為如下:
1.保存程序的上下文即當前任務(wù)的寄存器莉测,保存存儲寄存器的任務(wù)堆棧的地址捣卤。
2.根據(jù)下一個任務(wù)的任務(wù)堆棧地址依次彈出下一個任務(wù)的16個寄存器八孝。
堆棧中的寄存器保存順序如下,
XPSR
SP(代表MSP或PSP,在任務(wù)調(diào)度完成后子姜,根據(jù)PSP的值定位了是哪個任務(wù))
LR(存儲函數(shù)的返回)
R12
R3
R2
R1
R0
R11
R10
R9
R8
R7
R6
R5
R4
具體寄存器的功能可以查看這位博主的文章 https://blog.csdn.net/sagitta_zl/article/details/51318507
接下來根據(jù)程序執(zhí)行的順序解釋程序
首先介紹幾個定義的變量
1)TCB程序控制塊哥捕,程序控制塊是一個結(jié)構(gòu)體嘉熊,其中存儲了每一個任務(wù)的堆棧的地址指針。
2)taskTCB[2]阐肤,最多兩個任務(wù)TCB讲坎。程序中定義了最大的同時執(zhí)行的任務(wù)數(shù)量為2晨炕,也即只有兩個任務(wù)互相交替厚满,為了簡化也沒有加入IdleTask。
3)uint32_t stack[100]遵馆,任務(wù)堆棧的大小為丰榴。也即是100*4個字節(jié)大小四濒。當任務(wù)的嵌套層數(shù)很多,或很長有很多局部變量時要增大任務(wù)的堆棧戈二。局部變量保存在了堆棧中喳资。
4)TCB *currTCB,*nextTCB分別存儲了當前任務(wù)和下一個任務(wù)的TCB,在任務(wù)切換的時候使用
1. OSInit()
OSInit中執(zhí)行了對TCB的初始化鲜滩,后面根據(jù)TCB初始化的值可以判定哪個TCB還處于空閑狀態(tài)可以放入新的任務(wù)节值。很簡單,棧頂指針初始化為了NULL嗓蘑,后面就通過判斷是不是為NULL來判定這個TCB還能不能用匿乃。
void OSInit()
{
int i = 0;
for(i = 0;i<MAX_TASK_NUMBER;i++)
{
taskTCB[i].topStackPtr = NULL;
}
currTCB = &taskTCB[0];
nextTCB = &taskTCB[0];
}
2. 新建任務(wù)堆棧
新建任務(wù)堆棧是通過申請了一個靜態(tài)的數(shù)組扳埂,也即是uint32_t stack[100]當作堆棧,如果采用動態(tài)分配的話要涉及到內(nèi)存管理梅尤,現(xiàn)階段簡單的任務(wù)沒有必要加上,一切從簡赡盘。
3. 新建任務(wù)
根據(jù)任務(wù)的地址缰揪,任務(wù)的堆棧钝腺,就可以新建任務(wù)了
void OSCreateNewTask(void (*fun)(void),uint32_t *stackAddress)
{
int i = 0;
//進入臨界區(qū),關(guān)中斷定硝,也就是在臨界區(qū)之內(nèi)不發(fā)生中斷
EnterCriticalRegion();
//尋找空閑的任務(wù)塊
while(taskTCB[i].topStackPtr!=NULL)
{
i++;
}
//初始化任務(wù)的棧毫目,棧存儲的寄存器順序不能錯,順序見圖一
*stackAddress? ? = (uint32_t)0x01000000uL;? //xPSR的值箱蟆,有個1代表是thumb模式
*(--stackAddress) = (uint32_t)fun;? ? ? ? ? //存儲了要執(zhí)行的任務(wù)的地址
*(--stackAddress) = (uint32_t)0xffffffffuL;? //R14(LR)因為程序是個無限的大循環(huán)空猜,因此不返回
*(--stackAddress) = (uint32_t)0x12121212uL;? //R12
*(--stackAddress) = (uint32_t)0x03030303uL;? //R3
*(--stackAddress) = (uint32_t)0x02020202uL;? //R2
*(--stackAddress) = (uint32_t)0x01010101uL;? //R1
*(--stackAddress) = (uint32_t)0x00000000uL;? //R0
*(--stackAddress) = (uint32_t)0x11111111uL;? //R11
*(--stackAddress) = (uint32_t)0x10101010uL;? //R10
*(--stackAddress) = (uint32_t)0x09090909uL;? //R9
*(--stackAddress) = (uint32_t)0x08080808uL;? //R8
*(--stackAddress) = (uint32_t)0x07070707uL;? //R7
*(--stackAddress) = (uint32_t)0x06060606uL;? //R6
*(--stackAddress) = (uint32_t)0x05050505uL;? //R5
*(--stackAddress) = (uint32_t)0x04040404uL;? //R4
//TCB棧頂指針的初始化
taskTCB[i].topStackPtr = stackAddress;
//離開臨界區(qū)诺核,代表可以進行任務(wù)的切換
ExitCriticalRegion();
}
4. OSStart()
在這個部分中完成的任務(wù)比較重要窖杀,首先我們要知道PendSV中斷的作用裙士。前面提到了執(zhí)行任務(wù)切換的兩種方式,其中systick_handler就是通過調(diào)用PendSV來完成的任務(wù)切換桌硫。
個中事件的流水賬記錄如下:
1)? 任務(wù) A 呼叫 SVC 來請求任務(wù)切換(例如铆隘,等待某些工作完成)
2)? OS 接收到請求南用,做好上下文切換的準備掏湾,并且 pend 一個 PendSV 異常融击。
3)? 當 CPU 退出 SVC 后雳窟,它立即進入 PendSV,從而執(zhí)行上下文切換拇涤。
4)? 當 PendSV 執(zhí)行完畢后誉结,將返回到任務(wù) B搓彻,同時進入線程模式。
5)? 發(fā)生了一個中斷怔接,并且中斷服務(wù)程序開始執(zhí)行
6)? 在 ISR 執(zhí)行過程中稀轨,發(fā)生 SysTick 異常,并且搶占了該 ISR奋刽。
7)? OS 執(zhí)行必要的操作,然后 pend 起 PendSV 異常以作好上下文切換的準備肚吏。
8)? 當 SysTick 退出后狭魂,回到先前被搶占的 ISR 中雌澄,ISR 繼續(xù)執(zhí)行
9)? ISR 執(zhí)行完畢并退出后,PendSV 服務(wù)例程開始執(zhí)行镐牺,并且在里面執(zhí)行上下文切換
10) 當 PendSV 執(zhí)行完畢后睬涧,回到任務(wù) A沛厨,同時系統(tǒng)再次進入線程模式逆皮。
可以看到PendSV的優(yōu)先級是最低的参袱,這樣才能夠不影響其他中斷的執(zhí)行,影響了實時性剿牺。在Systick中可以把PendSV掛起环壤,在ISR執(zhí)行完成后再執(zhí)行這個優(yōu)先級最低的中斷郑现。
__ASM void OSStart()
{
PRESERVE8
//關(guān)中斷
CPSID I
//設(shè)置PendSV的優(yōu)先級為最低
LDR R0,=NVIC_SYSPRI14 //R0 = NVIC_SYSPRI14
LDR R1,=NVIC_PENDSV_PRI //R1 = NVIC_PENDSV_PRI
STRB R1,[R0] //R0 = *R1
//賦PSP=0,代表是第一次執(zhí)行攒读,作用見下文
LDR R4,=0x0 //R4 = 0
MSR PSP,R4 //PSP = R4
//LDR R4,=0x3
//MSR CONTROL,R4
//掛起PendSV中斷辛友,通過直接寫寄存器的方式可以掛起
? LDR? R4, =NVIC_INT_CTRL? ? ? ? ? ? ?
? LDR? R5, =NVIC_PENDSVSET? ? ? ? ? ?
? STR? R5, [R4]? ? ? ? ? ? ? ? ? ? ? ?
//開中斷
CPSIE I
BX LR
nop //對齊
}
5. PendSV
在步驟4中程序的最后掛起了PendSV废累,因此一旦開啟了中斷,程序?qū)M入PendSV執(zhí)行日缨。在這一步中殿遂,需要理解到進入函數(shù)跳轉(zhuǎn)即進入PendSV時乙各,硬件自動會完成在PSP指向的地址中存儲xPSR,LR,SP,R0-R3,R12這8個寄存器的工作幢竹。因此完成這個步驟之后PSP=PSP-0x20,之后再在PSP指向的地址中存儲R4-R11 8個寄存器蹲坷。之后的彈出過程與之相反,先彈出R4-R11寄存器级乐,跳出PendSV后县匠,硬件自動完成剩余的8個寄存器的彈出乞旦,PSP=PSP+0x20
__ASM void PendSV_Handler()
{
extern PendSVFirst;
extern currTCB;
extern nextTCB;
PRESERVE8
CPSID I
//判斷PSP是否為0,如果是則代表是第一次執(zhí)行程序故痊,那么就沒有了保存當前寄存器這個過程玖姑,直接跳轉(zhuǎn)到彈出寄存器
MRS R0,PSP
CBZ R0,PendSVPopData
STMDB R0!,{R4-R11} //在R0中依次保存R4-R11寄存器 完成后R0=R0-0x20
LDR R1,=currTCB
LDR R1,[R1] //R1=currTCB->StackTopPtr
STR R0,[R1] //currTCB->StackTopPtr=R0焰络,保存當前任務(wù)上下文的最后一步豫领,也即當前任務(wù)的TCB保存了其任務(wù)堆棧棧頂?shù)闹羔槪瓿闪吮4妗?/p>
nop
//因為沒有BX跳轉(zhuǎn)指令舔琅,執(zhí)行完這個函數(shù)后接著會執(zhí)行下面的PendSVPopData()
}
__ASM void PendSVPopData()
{
extern PendSVFirst;
extern currTCB;
extern nextTCB;
PRESERVE8
LDR R0,=currTCB //R0=currTCB?
LDR R1,=nextTCB //R1=nextTCB
LDR R1,[R1] //R1=*R1
STR R1,[R0] //*R0=R1 currTCB=nextTCB完成了指向新的任務(wù)TCB的工作
LDR R0,[R1] //R0=*R1 R0保存了TCB堆棧棧頂?shù)闹羔?/p>
LDMIA R0!,{R4-R11} //依次彈出R4-R11等恐,完成后R0=R0+0x20
MSR PSP,R0 //PSP=R0
ORR LR,LR,#0x04 //LR=LR|0x04,表示函數(shù)返回后使用PSP指針
CPSIE I
BX LR
nop
}
6. OSSwitch()
OSSwitch函數(shù)只是簡單的更新了nextTCB备蚓,之后完成了觸發(fā)PendSV课蔬,即設(shè)置PendSV相應(yīng)寄存器的位為1
void OSSwitch()
{
EnterCriticalRegion();
if(currTCB==&taskTCB[0])
nextTCB = &taskTCB[1];
else
nextTCB = &taskTCB[0];
OSTaskSchedule();
ExitCriticalRegion();
}
7. EnterCtiticalRegion和ExitCriticalRegion很簡單,內(nèi)容就是開關(guān)中斷
__ASM void EnterCriticalRegion()
{
PRESERVE8
CPSID I? //關(guān)中斷
BX? ? LR //LR跳回
}
__ASM void ExitCriticalRegion()
{
PRESERVE8
CPSIE I
BX LR
}
至此就完了最簡單的任務(wù)切換功能郊尝,下一章將加入搶占式內(nèi)核二跋、時間片和延時進行調(diào)度的功能。
程序鏈接如下扎即,實驗用的是STM32F407的開發(fā)板
鏈接:https://pan.baidu.com/s/1my2HPG6shXB7QiwbR47Dnw
提取碼:j5hw