Build Your Own RTOS Part3:初始化栈
Build Your Own RTOS 系列文章:
1 创建任务
我们都知道,使用RTOS时要先初始化任务。当任务还没有被执行时,即它没有任何历史记录时,CPU怎么知道应该从哪里开始执行呢?
这就是我们要做的工作:初始化任务就是初始化栈,即通过手动填充栈,制造一个“假象”,让 CPU 认为这个任务之前正在运行并被中断了。当下一次调度发生时,CPU 就能“恢复”到任务的入口函数。
涉及到硬件的底层知识基础在前两期中已经讲的差不多了,本期我们聚焦于两个软件设计上的重点:任务控制块(Task Control Block, TCB)和栈初始化函数OS_StackInit()。
2 任务控制块
我们已经知道,CPU没有记忆,记忆都放在寄存器里。为了给每一个任务都分配特定的记忆,我们就需要任务控制块。在代码中,任务控制块就是一个简单的结构体。
既然我们已经知道任务控制块的作用,那结构体里面有什么内容也就很清晰了:
- 最重要的东西:SP指针。这是显而易见的,任务控制块中必须有的东西,否则上下文就无法恢复了。值得一提的是,我们通常把这个SP指针放在结构体的第一位。这是因为当任务切换时,最紧急的事情就是先去栈里面找寄存器的值,让他们出栈恢复现场。如果SP指针放在第一位,我们就可以使用类似
LR R0, [R1]的语句直接读出SP的值,不需要计算偏移量。这样一来,CPU也不需要计算。而且,不管你在SP的后面添加什么东西,都不需要改动这段读取代码,也方便了你自己。 - 任务链表指针:只有定义了上下家是谁,OS才能把任务都串起来。
- 任务状态:只需看一眼,OS就能知道这个任务是“准备好了”,还是“睡眠中”,还是“挂起了”。
- 优先级:同样只要一眼,OS就知道哪个任务要排在前面。
- 任务名字。
所以,我们的代码应该会是下面这样:
1 | typedef struct TaskControlBlock { |
当然,现在我们不用管那么多,TCB里面只放一个sp就行。
1 | /* in os_core.h */ |
3 栈初始化
现在我们就准备开始创建我们的任务栈初始化函数。首先我们要明确,这个函数是要接收参数的,但应该接收哪几个参数呢?
既然是任务的初始化,肯定要知道任务是什么,所以一定要接受任务函数的入口地址,也就是我们写好的任务函数的指针。同时,用户会给任务分配栈大小,这也是必须要传入的数据。既然已经有了栈大小,我们就要规划栈放在哪里,所以要传入栈数组的起始地址。这就是这个函数接受的三个参数。
接下来我们就开始实现这个函数。还记得中断发生和返回的逻辑吗?我们就要顺着这个逻辑来。
- 首先我们要找到栈顶,也就是
栈数组的起始地址 + 栈大小。在这里还有一个需要注意的地方,就是要字节对齐,让栈顶指针指向8的倍数。 - 然后我们就开始模仿硬件压栈的操作。(还记得压栈的顺序吗?)首先是xPSR,必须要填
0x01000000。这是把xPSR的Bit[24]置1,表示使用Thumb模式,如果不这么做就会直接进入Hard Fault。 - 然后是PC,填入函数入口地址即可,表示“中断”返回后CPU从哪里继续。
- 再然后是LR,它应该填入一个“错误返回函数”的地址。因为RTOS的任务一般是死循环,是不会返回的,如果返回肯定是哪里出错了,就要跳转到这个地方去处理错误。
- 最后是通用寄存器们,通通填0即可。当然我们今天可以填一些特殊的数字(比如给R1填
0x11111111),用于调试。
好了,寄存器就处理完了,现在栈指针就下降到了某个地方。我们要把这个栈指针作为返回值,填到TCB的第一个位置,至此任务初始化结束。
逻辑讲完了,我们就开始写代码。明确以下几点:任务函数的入口地址是void*类型,栈数组的起始地址是uint32_t*类型,栈大小是uint32_t类型;在实现逻辑中,可以使用*(--sp) = ...给内存地址赋值,模拟PUSH操作。我们在os_cpu.c文件里完成这个函数的实现。
这里先写一个简单的OS_TaskReturn()函数,供给LR。
1 | void OS_TaskReturn(void) |
1 | uint32_t* OS_StackInit(void* task_function, uint32_t* stack_init_address, uint32_t stack_depth) |
4 整合与验证
这个阶段中我们将创建两个简单的任务、定义两个TCB变量、定义两个静态数组作为栈。然后我们实现一个简单的错误返回函数供给LR,最后调试。
首先,在main.c中声明一个结构体OS_TCB Task1TCB;:
然后定义一个数组,uint32_t Task1stack[128],分配给这个任务。
最后我们定义一个简单的任务函数:
1 | void Task1(void) |
然后在main函数里,我们调用写好的函数:Task1TCB.stackPtr = OS_StackInit(Task1, Task1stack, 128);。注意要在这一行打断点,因为我们没写调度器,运行下去可能有问题。
在菜单栏打开Watch Windows,然后在<Enter expression>中输入刚刚定义的栈数组,就可以找到这个数组头对应的地址。然后+200(16进制的512),在这个地址附近找就可以了。(因为我们内存对齐的原因,可能不会精确的在头地址+200处开始存储)
5 总结
好了,我们已经解决了路线上的第一个大难点,理解了任务控制块存在的意义、实现了OS_StackInit()函数。下期我们将构造RTOS的心脏PendSV_Handler,是我们路线上的第二个大难点。加油!
.jpg)

