Build Your Own RTOS 系列文章

  1. 理解M3架构
  2. 当前阅读:硬件自动压栈
  3. 初始化栈
  4. PendSV
  5. 时间片轮转
  6. 延时
  7. 信号量

1 “保存现场”

在上一篇中,我们已经讲解了中断发生时,硬件自动进行的操作以及我们需要执行的操作。但我们还没有亲眼见证过硬件的动作,这一次我们将设计一个中断并触发它,看看硬件究竟会做些什么。

1.1 准备工作

由于我们使用CubeMX自动生成文件,代码里已经设定好了时钟树和SysTick。我们进入stm32f1xx_it.c文件寻找一下SysTick_Handler(),然后将其修改成这样:

1
2
3
4
5
6
7
8
9
10
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
__NOP();
/* USER CODE END SysTick_IRQn 0 */
//HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */

/* USER CODE END SysTick_IRQn 1 */
}

__NOP()是一个宏,代表空指令,在这里设置一个断点(Keil中不允许对空行打断点)。然后我们注释掉HAL_IncTick(),先不管HAL库的SysTick逻辑。在主函数里的HAL_Init()函数中,会自动初始化SysTick,所以我们可以直接观察这个中断。

这一次我们同样创建一个函数void set_registers(),它的目的和上次“伪造现场”的目的类似,但是这一次我们不填充R4-R11,而是填充R0-R3。

然后我们回到main.c中,在main函数的while(1)循环里调用set_registers(),调试运行等待程序碰到断点。

1.2 观察现象

我们看到图中,SP对应的内存地址的确整齐的排列好了我们伪造的数据。这证明了:

  1. 中断的确发生了。
  2. 中断发生时,硬件会自动将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:返回线程模式,并使用MSP
  • 0xFFFF FFFD:返回线程模式,并使用PSP

主程序在线程模式下,使用哪个栈进入了中断,就会以哪个栈退出中断(以异常返回码标志)。

2 双栈

我们已经知道,RTOS是使用两个栈的,那我们就来模拟一下使用两个栈。

我们需要在main函数里,初始化SysTick前,设置PSP寄存器的值为0x20002000(模拟开辟了一个任务栈),然后将CONTROL寄存器的Bit[1]置1。同样的,我们在main.c中编写一个函数void create_fake_task()来完成。

我们可以非常清楚的看到,LR的值变成了0xFFFFFFFD,符合我们的预期。

3 总结

本期我们承接着上期的内容,完整的看了一遍中断的全流程,对硬件自动做的软件(我们)需要做的已经有了明确的了解。下期我们将完成一次真正的 “任务切换”