Build Your Own RTOS Part2:硬件自动压栈
Build Your Own RTOS 系列文章:
1 “保存现场”
在上一篇中,我们已经讲解了中断发生时,硬件自动进行的操作以及我们需要执行的操作。但我们还没有亲眼见证过硬件的动作,这一次我们将设计一个中断并触发它,看看硬件究竟会做些什么。
1.1 准备工作
由于我们使用CubeMX自动生成文件,代码里已经设定好了时钟树和SysTick。我们进入stm32f1xx_it.c文件寻找一下SysTick_Handler(),然后将其修改成这样:
1 | void SysTick_Handler(void) |
__NOP()是一个宏,代表空指令,在这里设置一个断点(Keil中不允许对空行打断点)。然后我们注释掉HAL_IncTick(),先不管HAL库的SysTick逻辑。在主函数里的HAL_Init()函数中,会自动初始化SysTick,所以我们可以直接观察这个中断。
这一次我们同样创建一个函数void set_registers(),它的目的和上次“伪造现场”的目的类似,但是这一次我们不填充R4-R11,而是填充R0-R3。
1 | __asm void set_registers(void) |
然后我们回到main.c中,在main函数的while(1)循环里调用set_registers(),调试运行等待程序碰到断点。
1.2 观察现象

我们看到图中,SP对应的内存地址的确整齐的排列好了我们伪造的数据。这证明了:
- 中断的确发生了。
- 中断发生时,硬件会自动将R0-R3等寄存器压入栈中。
然后我们再看看0x44444444后面都有些什么。


它们分别是(小端序):
00 0F 00 00:这就是R12的值,只不过在内存里翻过来了。B9 0A 00 08:这是LR的值(0x08000AB9),他代表了中断结束后,程序要回到的地址。
图中的
0x08000AB8就是while(1)。这说明程序马上要回到循环中。(还记得为什么LR的值和实际地址差1吗?)58 01 00 08:这是PC的值,也就是被打断点的__NOP()的地址。00 00 00 61:这是xPSR的值。是状态寄存器在进入中断前的状态。
那么,寄存器窗口的值为什么会与栈中的数据不一样呢?
我们一个个来看:
- 寄存器中LR(
0xFFFFFFF9):这其实是EXC_RETURN(异常返回码)一旦进入中断,硬件会自动把LR寄存器改成这个特殊值。 - 寄存器中xPSR(
0x6100000F):还记得xPSR的低位代表什么吗?0x0F是十进制的15,代表SysTick Exception,说明目前正在处理SysTick中断。
至此,我们已经通过实验验证了:在触发中断后,硬件会自动将一些寄存器的值压入栈中,且它们的顺序是xPSR -> PC -> LR -> R12 -> R3-R0。这非常重要,推荐记在小本本上。
但于此同时,也引出了第二个问题:虽然我们已经处理好了这些数据,但是中断结束后怎么办?我该如何知道是使用MSP还是PSP?
这就用到了异常返回码。实际上,当异常返回码进入PC后,就会启动中断返回序列。合法的异常返回码只有三个:
0xFFFF FFF1:返回Handler模式0xFFFF FFF9:返回线程模式,并使用MSP0xFFFF FFFD:返回线程模式,并使用PSP
主程序在线程模式下,使用哪个栈进入了中断,就会以哪个栈退出中断(以异常返回码标志)。
2 双栈
我们已经知道,RTOS是使用两个栈的,那我们就来模拟一下使用两个栈。
我们需要在main函数里,初始化SysTick前,设置PSP寄存器的值为0x20002000(模拟开辟了一个任务栈),然后将CONTROL寄存器的Bit[1]置1。同样的,我们在main.c中编写一个函数void create_fake_task()来完成。
1 | __asm void create_fake_task() |

我们可以非常清楚的看到,LR的值变成了0xFFFFFFFD,符合我们的预期。
3 总结
本期我们承接着上期的内容,完整的看了一遍中断的全流程,对硬件自动做的和软件(我们)需要做的已经有了明确的了解。下期我们将完成一次真正的 “任务切换”
.jpg)