FreeRTOS任务创建、启动调度器、任务切换的过程分析——基于ARM-CotexM3
ARM-CM3创建任务、开启调度器、任务调度的整个流程:
【创建任务】
- 创建任务控制块。为任务申请空间并创建一个任务控制块NewTCB;
- 申请任务栈空间。为任务申请一块栈空间,并将起始地址存储到NewTCB.pxStack中;
- 初始化任务相关参数。将任务名、优先级和相关列表项等存放到任务控制块;
- 初始化任务栈中的上下文。计算栈顶指针pxTopOfStack,并通过该指针初始化上下文堆栈,主要包括xPSR、PC、LR,并给其他上下文留空,最后将栈顶指针pxTopOfStack存储到任务控制块TCB中。其中PC初始化为任务函数指针pxCode;
- 将新创建的任务加入任务就绪列表;
【启动任务调度器】
- 开启PendSV和Systick中断;
- 启动第一个任务。找到主栈的起始地址赋给MSP寄存器,使能中断并触发SVC服务来完成第一个任务的启动。SVC服务完成以下工作:(1)首先从TCB中获取第一个任务的栈顶指针,然后从栈顶开始恢复r11-r4寄存器;(2)将此时的栈顶指针赋给PSP寄存器供系统自动恢复其余上下文使用;(3)开启中断;(4)将r14(保存返回地址)或上0x0D,即设置返回时进入线程模式,从而在自动恢复上下文时使用PSP(ARM-CotexM3的堆栈指针分为主栈指针MSP和进程栈指针PSP);(5)最后执行<bx r14>从PSP处恢复其余上下文并返回,然后执行PC指向的任务函数;
【进行任务切换】
- 任务切换有两种场合:执行系统调用和触发SysTick中断,但最终都是依靠PendSV中断来实现。当发生任务切换时,进入PendSV中断,进入中断前已自动保存一部分上下文,从PSP指向地址开始入栈。进入中断后首先读取PSP到r0寄存器用于手动入栈操作,此时PSP应该指向另一部分上下文的起始地址,然后将另一部分上下文入栈保存,最后将此时的r0存储到任务控制块的栈顶指针成员即pxNewTCB->pxTopOfStack,至此完成了当前任务的上下文保存。接下来进行任务的切换,首先将r3和r14入主栈保存,防止调用vTaskSwitchContext函数时被覆盖,因为r14中保存了PendSV中断的返回地址;且当任务切换函数vTaskSwitchContext执行完成后,pxCurrentTCB被更新,而r3中保存了变量pxCurrentTCB的地址,因此可以继续使用r3访问它。找到新的任务后,要想使其运行,需要将其上下文出栈恢复,因此与启动第一个任务一样,从pxCurrentTCB中保存的任务栈顶指针处先恢复下文,然后将此时的栈顶指针赋给PSP,然后执行返回指令,从而自动恢复其他上下文并跳转至PC所指向的指令地址——任务函数地址。
关键函数代码如下:
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ) { /* Simulate the stack frame as it would be created by a context switch interrupt. */ pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */ *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */ pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ return pxTopOfStack; } BaseType_t xPortStartScheduler( void ) { /* Make PendSV and SysTick the lowest priority interrupts. */ portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; /* Start the timer that generates the tick ISR. Interrupts are disabled here already. */ vPortSetupTimerInterrupt(); /* Initialise the critical nesting count ready for the first task. */ uxCriticalNesting = 0; /* Start the first task. */ prvStartFirstTask(); /* Should not get here! */ return 0; } __asm void prvStartFirstTask( void ) { PRESERVE8 /* Use the NVIC offset register to locate the stack. */ ldr r0, =0xE000ED08 ldr r0, [r0] ldr r0, [r0] /* Set the msp back to the start of the stack. */ msr msp, r0 /* Globally enable interrupts. */ cpsie i cpsie f dsb isb /* Call SVC to start the first task. */ svc 0 nop nop } __asm void vPortSVCHandler( void ) { PRESERVE8 ldr r3, =pxCurrentTCB /* Restore the context. */ ldr r1, [r3] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */ ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ ldmia r0!, {r4-r11} /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */ msr psp, r0 /* Restore the task stack pointer. */ isb mov r0, #0 msr basepri, r0 orr r14, #0xd bx r14 } __asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 mrs r0, psp isb ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ ldr r2, [r3] stmdb r0!, {r4-r11} /* Save the remaining registers. */ str r0, [r2] /* Save the new top of stack into the first member of the TCB. */ stmdb sp!, {r3, r14} mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp!, {r3, r14} ldr r1, [r3] ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ ldmia r0!, {r4-r11} /* Pop the registers and the critical nesting count. */ msr psp, r0 isb bx r14 nop }