Build Your Own RTOS Part4:PendSV
Build Your Own RTOS 系列文章:
1 PendSV
在前几期中,我们已经理解了寄存器,学会了如何模仿硬件压栈来完成上下文切换。本期我们就要在PendSV_Handler()中实现它。
1.1 什么是PendSV?
PendSV(Pendable Service Call,可挂起的系统调用)是CM3内核专门设计的一种异常。就如字面所述,它是可以“挂起”(等待)的。
如果没有PendSV,我们可能要在别的中断(例如SysTick中)完成上下文切换。试想一下,我们正在处理紧急的硬件中断(或其他高优先级的任务),这时SysTick过来,不分青红皂白的切换上下文,直接让CPU去执行别的普通任务,原任务就被搁置了,这对我们的系统是致命的。
但是有了PendSV就不一样了。我们通常把PendSV的优先级设置成全系统最低(优先级号越大,优先级越低)。当SysTick触发任务切换的指令时,系统会先检查有没有比PendSV优先级更高的中断,如果有就先去执行优先级高的,直到最后只剩下PendSV,此时才执行任务切换。
1.2 为什么PendSV_Handler()只能用汇编写?
因为编译器太智能了,它会在你看不到的地方,在你手写的C语言函数代码前后添加一堆乱七八糟的东西(Prologue和Epilogue),虽然对于普通的代码而言无关紧要,但是对PendSV_Handler()这种高要求的操作,会出现大麻烦。所以,涉及到这种底层寄存器的操作,我们只能使用纯汇编来写。幸运的是,之后基本就不用了。
1.3 实现逻辑
我们现在就正式开始准备实现PendSV_Handler()。由于我们还没有写调度,所以今天我们先写一个简单版,等到后面我们再把内容添加进来。
为了切换任务,我们需要两个指针来分别指向当前的任务和下一个要运行的任务。我们用CurrentTCB和NextTCB来表示。
首先我们要明确:PendSV_Handler()要做些什么。假设我们已经有了两个任务,任务1的运行时间到了,接下来准备执行任务2。那么我们怎么处理现在寄存器里的数据呢?
- 先把现在正在用的数据存起来。第一步:获取当前PSP的值,存在R0里。由于R0-R3被硬件自动压栈了,所以存在R0里是安全的。然后,再把硬件没存的寄存器手动存到R0此刻指向的地址,也就是和硬件自动存储的数据放在一起。最后把
CurrentTCB的值(也就是当前执行任务的TCB的地址)读出来放进R1,再把此刻R0的值存到R1指向的地址。 - 接下来就是恢复现场,把任务2的数据拿回来。首先把
NextTCB的值写入CurrentTCB,现在的CurrentTCB已经是新的任务了。然后获取CurrentTCB的sp,从这个地址中弹出数据到R4-R11,最后更新PSP。至此,任务切换结束。
好了,逻辑基本讲清楚了,我们就开始写代码。把PendSV_Handler()函数的代码写在os_cpu_a.s中。我们需要在函数的开头使用CPSID I关中断、结尾BX LR前使用CPSIE I开中断来制造临界区。这在实际应用中是不需要甚至应该避免的,但是为了今天的调试和学习,我们先这么做,防止别的中断打断我们的逻辑。
1 | PendSV_Handler PROC ; PROC代表函数的开头 |
注意:这里的代码只是逻辑实现,完整的实现请参考下文。
2 “启动”
接下来我们就开始做实验,亲眼看看我们的代码是不是成功了。首先因为我们没写调度器,所以我们需要一个用于触发PendSV的函数OS_Yield()。它通过向中断控制及状态寄存器ICSR(地址:0xE000_ED04)的Bit[28]写入1来挂起PendSV中断。
1 | void OS_Yield(void) |
然后我们要写一个开始函数OS_Start()。当我们还没有开始任务调度的时候,CurrentTCB绝对是NULL,即0;而NextTCB指向下一个TCB。所以我们OS_Start()的任务实际上就是PendSV_Handler()的下半部分,只切换不存储。所以我们直接复用PendSV_Handler(),不再写一遍了。注意:这里很容易错,如果一个不小心寄存器存反了就进HardFault了。所以请仔细核查自己的代码,实在不行就直接复制吧。
1 |
|
在main.c里这么写(不要忘记在main函数之前定义任务的TCB和任务栈数组,还有在os_core.c里创建CurrentTCB和NextTCB两个OS_TCB*指针):
1 |
|
这里的Task1和Task2函数就这么写:
1 | uint32_t count1 = 0; |
最后,我们在stm32f1xx_it.c中找到PendSV_Handler(),把它注释掉,让程序使用我们自己写的PendSV_Handler()。
接下来就是插板子调试了。我们可以使用第三期类似的方法,打开Watch Windows,输入count1和count2,如果程序运行正常,它们的值应该飞速增大且最多相差1。

3 总结
我们已经啃下了路线上最难啃的骨头,也许没有之一! 后面的代码大多是逻辑层面的C语言编程,主要考察的是设计架构和算法编写能力,会轻松很多。
.jpg)