这是简单STM32 OS的第一章stm32超轻量操作系统之抢占式内核
在这个最简程序中只有两个任务交替执行,任务一和任务二,两个任务分别控制两个LED灯的亮灭。只是完成了最简单的任务切换功能。
这一版的程序中没有加入像一般的OS中调用延时函数时会发生任务调度的功能,也没有优先级,没有时间片,只是两个任务不断交替执行。在第二章中会加入抢占式内核和延时功能。
STM32的任务调度可以有两种方式
1.通过systick_handler定时器调度
2.执行一个系统调用
cortexM3的寄存器只有16个,cortexM4除了这16个还有很多浮点运算和MPU单元,如果不用这些单元它和M4没有区别,我是用的cortexM4内核的STM32F407,因为没有用浮点运算和MPU保护单元因此OS也和M3内核兼容。
因此在任务调度的过程中,也是不断的保存现在任务的这16个寄存器,弹出下一个任务的16个寄存器。
任务调度的步骤总结为如下:
1.保存程序的上下文即当前任务的寄存器,保存存储寄存器的任务堆栈的地址。
2.根据下一个任务的任务堆栈地址依次弹出下一个任务的16个寄存器。
堆栈中的寄存器保存顺序如下,
XPSR
SP(代表MSP或PSP,在任务调度完成后,根据PSP的值定位了是哪个任务)
LR(存储函数的返回)
R12
R3
R2
R1
R0
R11
R10
R9
R8
R7
R6
R5
R4
具体寄存器的功能可以查看这位博主的文章 https://blog.csdn.net/sagitta_zl/article/details/51318507
接下来根据程序执行的顺序解释程序
首先介绍几个定义的变量
1)TCB程序控制块,程序控制块是一个结构体,其中存储了每一个任务的堆栈的地址指针。
2)taskTCB[2],最多两个任务TCB。程序中定义了最大的同时执行的任务数量为2,也即只有两个任务互相交替,为了简化也没有加入IdleTask。
3)uint32_t stack[100],任务堆栈的大小为。也即是100*4个字节大小。当任务的嵌套层数很多,或很长有很多局部变量时要增大任务的堆栈。局部变量保存在了堆栈中。
4)TCB *currTCB,*nextTCB分别存储了当前任务和下一个任务的TCB,在任务切换的时候使用
1. OSInit()
OSInit中执行了对TCB的初始化,后面根据TCB初始化的值可以判定哪个TCB还处于空闲状态可以放入新的任务。很简单,栈顶指针初始化为了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. 新建任务堆栈
新建任务堆栈是通过申请了一个静态的数组,也即是uint32_t stack[100]当作堆栈,如果采用动态分配的话要涉及到内存管理,现阶段简单的任务没有必要加上,一切从简。
3. 新建任务
根据任务的地址,任务的堆栈,就可以新建任务了
void OSCreateNewTask(void (*fun)(void),uint32_t *stackAddress)
{
int i = 0;
//进入临界区,关中断,也就是在临界区之内不发生中断
EnterCriticalRegion();
//寻找空闲的任务块
while(taskTCB[i].topStackPtr!=NULL)
{
i++;
}
//初始化任务的栈,栈存储的寄存器顺序不能错,顺序见图一
*stackAddress = (uint32_t)0x01000000uL; //xPSR的值,有个1代表是thumb模式
*(--stackAddress) = (uint32_t)fun; //存储了要执行的任务的地址
*(--stackAddress) = (uint32_t)0xffffffffuL; //R14(LR)因为程序是个无限的大循环,因此不返回
*(--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;
//离开临界区,代表可以进行任务的切换
ExitCriticalRegion();
}
4. OSStart()
在这个部分中完成的任务比较重要,首先我们要知道PendSV中断的作用。前面提到了执行任务切换的两种方式,其中systick_handler就是通过调用PendSV来完成的任务切换。
个中事件的流水账记录如下:
1) 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
2) OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
3) 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
4) 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
5) 发生了一个中断,并且中断服务程序开始执行
6) 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
7) OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
8) 当 SysTick 退出后,回到先前被抢占的 ISR 中,ISR 继续执行
9) ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换
10) 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。
可以看到PendSV的优先级是最低的,这样才能够不影响其他中断的执行,影响了实时性。在Systick中可以把PendSV挂起,在ISR执行完成后再执行这个优先级最低的中断。
__ASM void OSStart()
{
PRESERVE8
//关中断
CPSID I
//设置PendSV的优先级为最低
LDR R0,=NVIC_SYSPRI14 //R0 = NVIC_SYSPRI14
LDR R1,=NVIC_PENDSV_PRI //R1 = NVIC_PENDSV_PRI
STRB R1,[R0] //R0 = *R1
//赋PSP=0,代表是第一次执行,作用见下文
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,因此一旦开启了中断,程序将会进入PendSV执行。在这一步中,需要理解到进入函数跳转即进入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,如果是则代表是第一次执行程序,那么就没有了保存当前寄存器这个过程,直接跳转到弹出寄存器
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,保存当前任务上下文的最后一步,也即当前任务的TCB保存了其任务堆栈栈顶的指针,完成了保存。
nop
//因为没有BX跳转指令,执行完这个函数后接着会执行下面的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完成了指向新的任务TCB的工作
LDR R0,[R1] //R0=*R1 R0保存了TCB堆栈栈顶的指针
LDMIA R0!,{R4-R11} //依次弹出R4-R11,完成后R0=R0+0x20
MSR PSP,R0 //PSP=R0
ORR LR,LR,#0x04 //LR=LR|0x04,表示函数返回后使用PSP指针
CPSIE I
BX LR
nop
}
6. OSSwitch()
OSSwitch函数只是简单的更新了nextTCB,之后完成了触发PendSV,即设置PendSV相应寄存器的位为1
void OSSwitch()
{
EnterCriticalRegion();
if(currTCB==&taskTCB[0])
nextTCB = &taskTCB[1];
else
nextTCB = &taskTCB[0];
OSTaskSchedule();
ExitCriticalRegion();
}
7. EnterCtiticalRegion和ExitCriticalRegion很简单,内容就是开关中断
__ASM void EnterCriticalRegion()
{
PRESERVE8
CPSID I //关中断
BX LR //LR跳回
}
__ASM void ExitCriticalRegion()
{
PRESERVE8
CPSIE I
BX LR
}
至此就完了最简单的任务切换功能,下一章将加入抢占式内核、时间片和延时进行调度的功能。
程序链接如下,实验用的是STM32F407的开发板
链接:https://pan.baidu.com/s/1my2HPG6shXB7QiwbR47Dnw
提取码:j5hw