skaiuijing

前言

调度器是整个RTOS的核心,在前面我们得到了调度器对象的框架图,并且简单介绍了调度器的原理。

在本节中,我们将会初始化调度器并且启动第一个任务。

本节内容需要一定的arm架构功底才能完全看懂,但是ARM架构只是RTOS这片大海中微不足道的一小滴海水,也不存在所谓的必须懂ARM架构才能懂RTOS的说法!陷入微小的细节中是没有必要的!读者如果没接触过arm架构,不需要细究,把握RTOS的宏观思想才是最重要的!

1
2
3
4
5
6
7
8
graph LR
Aa(调度器)-->Ab(初始化)-->Ae(启动)
Ae-->Ac(切换任务)
Ac-->Ba(保存任务状态)
Ac-->Bo(选择优先级最高的任务)
Ac-->Bb(切换到下一个任务)


调度器初始化

调度器的初始化比较简单,我们创建了第一个任务leisureTask,并且把它的各项参数传入任务创建函数中,创建对应的任务控制块与任务栈。由于现在还没有引入多优先级,因此注释掉TicksTableInit()函数。

第一个任务的内容是让单片机上的灯不断闪烁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void EnterSleepMode(void)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_Delay(500);
}


//Task handle can be hide, but in order to debug, it must be created manually by the user
TaskHandle_t leisureTcb = NULL;

void leisureTask( )
{//leisureTask content can be manually modified as needed
while (1) {
EnterSleepMode();
}
}

void SchedulerInit( void )
{
//TicksTableInit();现在还没有引入多优先级
xTaskCreate( leisureTask,
128,
NULL,
0,
&leisureTcb
);
}

调度器启动

调度器启动时,会找到向量表,然后触发SVC中断开启第一个任务。

ldr这三行代码的作用是–找到主栈的栈顶指针,svc下是特权模式运行,所以使用msp作为堆栈指针。然后将栈顶指针的值存储到msp,现在msp就指向栈顶指针了,之后的四行代码就是开启全局中断和异常,然后开启svc中断响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __attribute__( ( always_inline ) ) SchedulerStart( void )
{

/* Start the first task. */
__asm volatile (
" ldr r0, =0xE000ED08 \n"/* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"/* Set the msp back to the start of the stack. */
" cpsie i \n"/* Globally enable interrupts. */
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"/* System call to start first task. */
" nop \n"
" .ltorg \n"
);
}

关于它是如何找到栈顶指针的,可以参考下图,arm手册上讲得非常好:

bea8a5cc452a4a13b6c2f2d1c5562cec.png

简单来说就是,先找到向量表偏移量寄存器,再找到向量表,向量表记录了msp的初始值:

4580158378ca490c85885ea6611b2840.png

开启第一个任务

采用封装的思想看待SVC调用,其实就是它把任务栈中的内容加载到了CPU的寄存器里面。

首先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define vPortSVCHandler SVC_Handler

void __attribute__( ( naked ) ) vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11} \n"
" msr psp, r0 \n"
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" orr r14, #0xd \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}

这里就是把任务栈中的内容加载到CPU,还记得任务栈中的内容吗?先看看任务栈的结构体笔者再解释代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Class(Stack_register)
{
//automatic stacking
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
//manual stacking
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t LR;
uint32_t PC;
uint32_t xPSR;
};

前面的代码的含义就是:先令r3寄存器的值为pxCurrentTCB的地址,再把这个地址指向的内容给r1,现在r1存储的就是指针,我们知道指针就是地址,再把这个指针的值给r0,那么r0就会找到任务控制块。

pxCurrentTCB的作用是指向当前运行的任务或即将运行的任务的控制块(TCB)。前面三行代码的作用是获取pxCurrentTCB指向的任务栈,因为TCB的第一个成员就是栈顶指针。

从r0这个内存地址开始,把栈中往上九个地址的内容依次加载到CPU的寄存器r4-r11和r14,然后r0自增(加载后r0寄存器刚好表示这个栈里面的r0位置),执行任务时将会使用psp,于是我们更新psp这个栈指针。

然后清零r0寄存器,然后设置basepri寄存器为0,就是打开所有中断,basepri寄存器是一个用来控制屏蔽中断的寄存器,我们将会在临界区学习它。

最后两行代码的作用就是返回。

关于basepri寄存器,读者可以看看这些:

img

img

实验

在写下以上代码后,我们可以尝试编译代码,下载到开发板中,我们会发现stm32f103c8t6最小系统板上的指示灯一闪一闪亮晶晶

成功运行实验的工程,读者有需要可以自行参考:skaiui2/SKRTOS_sparrow at experiment

总结

创建了调度器对象并且使用SVC中断从任务栈中加载内容到CPU寄存器中,从而开启第一个任务。