skaiuijing

前言

先讲讲RTOS。

RTOS有两种大类型,分别是事件触发系统和时间触发系统。

事件触发系统由中断驱动,通常采用中断线程化等技术,适用于响应速度要求高的场景,例如传感器数据采集,但是当大量事情一次性发生时,计算机会面临大问题。

(一般的中断是CPU放下一切全力去做一件事情,中断线程化就是把事情放线程里,等中断触发这个线程,然后让这个线程跟其他线程并行执行)

时间触发系统的任务调度基于定时器中断,适用于周期性任务和确定性要求高的场景,如控制系统。它的内部有一个时钟,每隔一定时间间隔就会触发一次时间中断,每次时钟中断时,调度器决定是否需要切换任务。

不过现在的RTOS往往会结合这两种类型,比如FreeRTOS就属于时间触发系统和时间触发系统的结合。

在第十节的最后,我们得到了这些架构图,这就是Sparrow的蓝图:

img

sparrow的运行

现在让我们来看看Sparrow的设计,因为它并不是一个完整的RTOS内核,现在还是时间触发系统的部分占主要成分:

当Sparrow运行起来后,调度器负责选择并且切换任务,结合笔者前面的arm架构的文章可知:

1.调度器的组成有SVC、Systick、PendSV三个中断,它们都属于arm cortex m3架构的硬件资源。

2.SVC:负责当MCU复位后启动第一个任务,此时属于特权级线程模式,确保os的启动成功。

  1. Systick: 定时触发,触发Systick中断,Systick中断会根据当前时钟检查是否需要把某些任务加入就绪列表,在那之后,会触发PendSV中断。
  2. PendSV中断:保存当前任务状态,然后选择最高优先级的任务进行切换。它是整个调度器的精髓所在。

任务对象

我们得到的任务对象的蓝图如下,对于arm架构和rtos架构大家可能有点陌生,所以笔者啰嗦一点,任务对象让我们直接开始写代码,在代码中结合框图理解:

img

Sparrow是动态创建任务,对于任务栈和任务控制块(TCB_t),我们都要分配内存。xTaskCreat是用户与RTOS的接口,我们可以得到TCB_t的各项参数并赋值,现在还没有就绪列表,所以笔者注释了几行代码。

(usStackDepth - (uint32_t)1)是为了获取栈顶的地址,我们分配内存会得到内存的第一个地址,所以加上栈的大小后要减1才是栈顶。举例:申请500,起始地址为1,那就是1–500这部分空间,1+500 -1得到的值才是500这个栈顶。

& (~…是字节对齐。

xTaskCreat完成了:

1.获得用户设置的参数,例如优先级,栈的大小等等,然后初始化任务控制块。

2.为任务栈和任务控制块开辟内存

3.初始化任务栈,获得任务栈的栈顶参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void xTaskCreate( TaskFunction_t pxTaskCode,
const uint16_t usStackDepth,
void * const pvParameters,//it can be used to debug
uint32_t uxPriority,
TaskHandle_t * const self )
{
uint32_t *topStack =NULL;
TCB_t *NewTcb = (TCB_t *)heap_malloc(sizeof(TCB_t *));
*self = ( TCB_t *) NewTcb;
//TcbTaskTable[uxPriority] = NewTcb;
NewTcb->uxPriority = uxPriority;
NewTcb->pxStack = ( uint32_t *) heap_malloc( ( ( ( size_t ) usStackDepth ) * sizeof( uint32_t * ) ) );
topStack = NewTcb->pxStack + (usStackDepth - (uint32_t)1) ;
topStack = ( uint32_t *) (((uint32_t)topStack) & (~((uint32_t) aligment_byte)));
NewTcb->pxTopOfStack = pxPortInitialiseStack(topStack,pxTaskCode,pvParameters,self);

pxCurrentTCB = NewTcb;
//ReadyBitTable |= (1 << uxPriority);
}

pxPortInitialiseStack函数负责初始化任务栈,任务栈是保存寄存器的地方,让我们先看看arm cm3手册:

img

笔者在stack_register结构体中已经添加了这些寄存器,让我们边写代码,边听笔者慢慢讲解::

在32位架构下,一个指针大小为一个字,即32位,一个寄存器大小也为一个字。

pxTopOfStack-=16,因为一开始它是指向栈顶的,这是为了容纳stack_register结构体,把这个地址赋值给stack_register结构体,现在结构体的起始地址就是结构体中的第一个寄存器。

然后设置xPSR寄存器:xPSR的24位被置1,表示这是Thumb指令状态,实际上cm3架构只支持Thumb指令状态。

设置PC寄存器:pxCode是任务函数的地址,在arm架构中,分为arm指令集状态和Thumb指令集状态,你可以使用这两种指令集,区别是,Thumb指令集寄存器最低位是0,cm3下都是Thumb指令集,所以,为了预防未定义问题,必须把任务的地址最低位进行清0。

设置LR寄存器:LR寄存器是用于存储任务调用返回地址的专用寄存器,当一个函数被调用时,返回地址会被保存到 LR 寄存器中,以便在函数执行完毕后能够正确返回到调用该函数的下一条指令。但是,rtos中通常任务是不会返回的,所以这里的pvParameters其实是一个笔者用来调试的参数,观察栈有没有被破坏。读者可以把它改成自己的调试函数的地址。

设置r0:self也是一个笔者用来调试的参数,当任务执行后,r0的值会被马上更改,主要是用来判断任务栈是否创建成功。

(*self)->self_stack = Stack:设置任务对象的栈对象,这一操作是为了方便调试时找到一个线程的栈地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint32_t * pxPortInitialiseStack( uint32_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters ,
TaskHandle_t * const self)
{
pxTopOfStack -= 16;
Stack_register *Stack = (Stack_register *)pxTopOfStack;

Stack->xPSR = 0x01000000UL;
Stack->PC = ( ( uint32_t ) pxCode ) & ( ( uint32_t ) 0xfffffffeUL );
Stack->LR = ( uint32_t ) pvParameters;
Stack->r0 = ( uint32_t ) self;
(*self)->self_stack = Stack;

return pxTopOfStack;
}

总结

笔者先给出Sparrow的框图,根据框图一步步完成了任务的创建以及初始化部分。任务对象的创建已经完成了,下一节笔者将会带领大家完成调度器对象的创建,同时使用SVC中断启动第一个任务!