前言
调度器是整个RTOS的核心,在前面我们得到了调度器对象的框架图,并且简单介绍了调度器的原理。
在本节中,我们将会初始化调度器并且启动第一个任务。
本节内容需要一定的arm架构功底才能完全看懂,但是ARM架构只是RTOS这片大海中微不足道的一小滴海水,也不存在所谓的必须懂ARM架构才能懂RTOS的说法!陷入微小的细节中是没有必要的!读者如果没接触过arm架构,不需要细究,把握RTOS的宏观思想才是最重要的!
调度器初始化
调度器的初始化比较简单,我们创建了第一个任务leisureTask,并且把它的各项参数传入任务创建函数中,创建对应的任务控制块与任务栈。由于现在还没有引入多优先级,因此注释掉TicksTableInit()函数。
第一个任务的内容是让单片机上的灯不断闪烁。
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中断响应。
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手册上讲得非常好:
简单来说就是,先找到向量表偏移量寄存器,再找到向量表,向量表记录了msp的初始值:
开启第一个任务
采用封装的思想看待SVC调用,其实就是它把任务栈中的内容加载到了CPU的寄存器里面。
首先看代码:
#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,还记得任务栈中的内容吗?先看看任务栈的结构体笔者再解释代码:
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寄存器,读者可以看看这些:
实验
在写下以上代码后,我们可以尝试编译代码,下载到开发板中,我们会发现stm32f103c8t6最小系统板上的指示灯一闪一闪亮晶晶。
成功运行实验的工程,读者有需要可以自行参考:skaiui2/SKRTOS_sparrow at experiment
总结
创建了调度器对象并且使用SVC中断从任务栈中加载内容到CPU寄存器中,从而开启第一个任务。