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手册上讲得非常好:
简单来说就是,先找到向量表偏移量寄存器,再找到向量表,向量表记录了msp的初始值:
开启第一个任务 采用封装的思想看待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寄存器,读者可以看看这些:
实验 在写下以上代码后,我们可以尝试编译代码,下载到开发板中,我们会发现stm32f103c8t6最小系统板上的指示灯一闪一闪亮晶晶 。
成功运行实验的工程,读者有需要可以自行参考:skaiui2/SKRTOS_sparrow at experiment
总结 创建了调度器对象并且使用SVC中断从任务栈中加载内容到CPU寄存器中,从而开启第一个任务。