Build Your Own RTOS 系列文章

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

1 创建任务

我们都知道,使用RTOS时要先初始化任务。当任务还没有被执行时,即它没有任何历史记录时,CPU怎么知道应该从哪里开始执行呢?

这就是我们要做的工作:初始化任务就是初始化栈,即通过手动填充栈,制造一个“假象”,让 CPU 认为这个任务之前正在运行并被中断了。当下一次调度发生时,CPU 就能“恢复”到任务的入口函数。

涉及到硬件的底层知识基础在前两期中已经讲的差不多了,本期我们聚焦于两个软件设计上的重点:任务控制块(Task Control Block, TCB)和栈初始化函数OS_StackInit()

2 任务控制块

我们已经知道,CPU没有记忆,记忆都放在寄存器里。为了给每一个任务都分配特定的记忆,我们就需要任务控制块。在代码中,任务控制块就是一个简单的结构体。

既然我们已经知道任务控制块的作用,那结构体里面有什么内容也就很清晰了:

  1. 最重要的东西:SP指针。这是显而易见的,任务控制块中必须有的东西,否则上下文就无法恢复了。值得一提的是,我们通常把这个SP指针放在结构体的第一位。这是因为当任务切换时,最紧急的事情就是先去栈里面找寄存器的值,让他们出栈恢复现场。如果SP指针放在第一位,我们就可以使用类似LR R0, [R1]的语句直接读出SP的值,不需要计算偏移量。这样一来,CPU也不需要计算。而且,不管你在SP的后面添加什么东西,都不需要改动这段读取代码,也方便了你自己。
  2. 任务链表指针:只有定义了上下家是谁,OS才能把任务都串起来。
  3. 任务状态:只需看一眼,OS就能知道这个任务是“准备好了”,还是“睡眠中”,还是“挂起了”。
  4. 优先级:同样只要一眼,OS就知道哪个任务要排在前面。
  5. 任务名字。

所以,我们的代码应该会是下面这样:

1
2
3
4
5
6
7
8
9
10
typedef struct TaskControlBlock {
volatile uint32_t *stackPtr; // 【第一位】SP指针

struct TaskControlBlock *next; // 下一个任务是谁
uint32_t state; // 现在的状态
uint32_t priority; // 优先级
char *name; // 任务名字

// ... 可能还有别的,但上面这些最重要
} OS_TCB;

当然,现在我们不用管那么多,TCB里面只放一个sp就行。

1
2
3
4
5
/* in os_core.h */
typedef struct Task_Control_Block {
volatile uint32_t *stackPtr;
} OS_TCB;

3 栈初始化

现在我们就准备开始创建我们的任务栈初始化函数。首先我们要明确,这个函数是要接收参数的,但应该接收哪几个参数呢?

既然是任务的初始化,肯定要知道任务是什么,所以一定要接受任务函数的入口地址,也就是我们写好的任务函数的指针。同时,用户会给任务分配栈大小,这也是必须要传入的数据。既然已经有了栈大小,我们就要规划栈放在哪里,所以要传入栈数组的起始地址。这就是这个函数接受的三个参数。

接下来我们就开始实现这个函数。还记得中断发生和返回的逻辑吗?我们就要顺着这个逻辑来。

  1. 首先我们要找到栈顶,也就是栈数组的起始地址 + 栈大小。在这里还有一个需要注意的地方,就是要字节对齐,让栈顶指针指向8的倍数。
  2. 然后我们就开始模仿硬件压栈的操作。(还记得压栈的顺序吗?)首先是xPSR,必须要填0x01000000。这是把xPSR的Bit[24]置1,表示使用Thumb模式,如果不这么做就会直接进入Hard Fault。
  3. 然后是PC,填入函数入口地址即可,表示“中断”返回后CPU从哪里继续。
  4. 再然后是LR,它应该填入一个“错误返回函数”的地址。因为RTOS的任务一般是死循环,是不会返回的,如果返回肯定是哪里出错了,就要跳转到这个地方去处理错误。
  5. 最后是通用寄存器们,通通填0即可。当然我们今天可以填一些特殊的数字(比如给R1填0x11111111),用于调试。

好了,寄存器就处理完了,现在栈指针就下降到了某个地方。我们要把这个栈指针作为返回值,填到TCB的第一个位置,至此任务初始化结束。

逻辑讲完了,我们就开始写代码。明确以下几点:任务函数的入口地址是void*类型,栈数组的起始地址是uint32_t*类型,栈大小是uint32_t类型;在实现逻辑中,可以使用*(--sp) = ...给内存地址赋值,模拟PUSH操作。我们在os_cpu.c文件里完成这个函数的实现。

这里先写一个简单的OS_TaskReturn()函数,供给LR。

1
2
3
4
void OS_TaskReturn(void)
{
for(;;);
}

4 整合与验证

这个阶段中我们将创建两个简单的任务、定义两个TCB变量、定义两个静态数组作为栈。然后我们实现一个简单的错误返回函数供给LR,最后调试。

首先,在main.c中声明一个结构体OS_TCB Task1TCB;

然后定义一个数组,uint32_t Task1stack[128],分配给这个任务。

最后我们定义一个简单的任务函数:

1
2
3
4
void Task1(void)
{
for(;;);
}

然后在main函数里,我们调用写好的函数:Task1TCB.stackPtr = OS_StackInit(Task1, Task1stack, 128);。注意要在这一行打断点,因为我们没写调度器,运行下去可能有问题。

pZMNDSS.png

在菜单栏打开Watch Windows,然后在<Enter expression>中输入刚刚定义的栈数组,就可以找到这个数组对应的地址。然后+200(16进制的512),在这个地址附近找就可以了。(因为我们内存对齐的原因,可能不会精确的在头地址+200处开始存储)

pZMNwJf.png

可以看到我们储存的数据

5 总结

好了,我们已经解决了路线上的第一个大难点,理解了任务控制块存在的意义、实现了OS_StackInit()函数。下期我们将构造RTOS的心脏PendSV_Handler,是我们路线上的第二个大难点。加油!