FreeRtos学习笔记(10)任务切换原理刨析
Posted 不咸不要钱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FreeRtos学习笔记(10)任务切换原理刨析相关的知识,希望对你有一定的参考价值。
FreeRtos学习笔记(10)任务切换原理刨析
STM32 单片机启动流程中介绍了SP和PC寄存器,
STM32单片机bootloader扫盲中说过如何通过控制SP和PC寄存器从而控制程序从bootLoader跳转到APP,RTOS任务切换和BootLoader与APP之间的跳转类似,也是通过控制SP和PC指针实现任务之间跳转。
MSP和PSP
在中断服务函数使用MSP作为堆栈指针,如果工程中没有特殊设置(即非RTOS工程)整个工程都会默认使用MSP。如果工程使用了RTOS,则除了中断服务函数外,其他任务使用PSP作为堆栈指针。
Cortex‐M3 拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个。
主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
进程堆栈指针(PSP):由用户的应用程序代码使用。
堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。 在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。除了外部中断外,当有指令执行了“非法操作”,或者访问被禁的内存区间,因各种错误产生的 fault,以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。另外,程序代码也可以主动请求进入异常状态的(常用于系统调用)。
为什么堆栈指针有两个?
- 可以将用户应用程序的堆栈与特权级/操作系统内核(kernel)的堆栈分开,阻止用户程序访问内核的堆栈,消除了内核数据被破坏的可能。(举个例子,windos系统下,一个软件卡死并不会使整个windos操作系统卡死)
- 可以使RTOS实现任务间“可抢占的系统调用”,大幅提高实时性能(中断前使用PSP,进入中断服务函数后会自动使用MSP,在中断中修改PSP值,退出中断服务函数后SP会自动切换到PSP,而PSP的值在中断中修改过,退出中断时会根据新的PSP, POP出PC寄存器及其他寄存器值,从而完成任务切换)
MSP和PSP之间如何切换?
M3权威指南中指出MSP和PSP之间的切换有两种方法:
- 在特权级线程模式下写CONTROL[1]
- 在中断服务函数结束时修改LR寄存器(R14),下图为LR寄存器低四位所代表的含义
FreeRtos中就是通过修改LR寄存器值实现从MSP切换到PSP的。
上电后默认使用MSP,然后进行外设初始化,创建任务,最后调用vTaskStartScheduler()启动RTOS,在vTaskStartScheduler()中会调用xPortStartScheduler()函数,xPortStartScheduler()函数中会调用port.c中的prvPortStartFirstTask();启动第一个任务。
prvPortStartFirstTask()为一个汇编函数,主要功能就是触发SVC中断
static void prvPortStartFirstTask( void )
{
__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"/* 触发SVC异常,在SVC中断服务函数中启动第一个任务. */
" nop \\n"
" .ltorg \\n"
);
}
SVC中断服务函数-- vPortSVCHandler()也是一个汇编函数,主要干了两件事,恢复任务现场(也就是将任务栈中保存的寄存器值POP到对应寄存器);将MSP切换为PSP;汇编语句具体含义可以对照M3权威指南中第四章指令集自行翻译。
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \\n"/* Restore the context. */
" ldr r1, [r3] \\n"/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
" ldr r0, [r1] \\n"/* The first item in pxCurrentTCB is the task top of stack. */
" ldmia r0!, {r4-r11} \\n"/* 将R4-R11的值从栈中弹出到对应寄存器,其他寄存器编译器会自动弹出 */
" msr psp, r0 \\n"/* Restore the task stack pointer. */
" isb \\n"
" mov r0, #0 \\n"
" msr basepri, r0 \\n"
" orr r14, #0xd \\n"/* 返回线程模式,并使用线程堆栈(SP从MSP切换到PSP) */
" bx r14 \\n"
" \\n"
" .align 4 \\n"
"pxCurrentTCBConst2: .word pxCurrentTCB \\n"
);
}
任务之间如何切换?
任务之间切换时需要保存现场,以便下次跳转回来时可以恢复现场,继续执行。所谓的现场就是内核的寄存器,而保存现场就是将寄存器组的当前值PUSH到栈中保存,恢复现场就是将栈中保存的寄存器值POP到对应寄存器,下图为cortex-M3寄存器组。
FreeRtos的任务切换在PendSV中断服务函数中完成的,该中断服务函数在port.c中。
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
/**************************第一部分保存现场****************************/
" mrs r0, psp \\n"
" isb \\n"
" \\n"
" ldr r3, pxCurrentTCBConst \\n"/* R3指向pxCurrentTCBConst,pxCurrentTCBConst指向当前任务控制块 */
" ldr r2, [r3] \\n"
" \\n"
" stmdb r0!, {r4-r11} \\n"/* 保存现场,将R4-R11的值压入栈中. */
" str r0, [r2] \\n"/* 由于上面将R4-R11压入栈中,栈顶PSP也会随之改变,这里将新的栈顶PSP存入任务控制块 */
" \\n"
" stmdb sp!, {r3, r14} \\n"/* 将R3入栈,后面还要用的R3,但是后面会调用 vTaskSwitchContext函数,防止vTaskSwitchContext中修改了R3的值 */
/*******************第二部分找到当前就绪任务中优先级最高的****************/
" mov r0, %0 \\n"
" msr basepri, r0 \\n"/* 关中断 进入临界区 要修改pxCurrentTCBConst */
" bl vTaskSwitchContext \\n"/* 查找就绪任务中优先级最高的任务 把pxCurrentTCBConst指向该任务控制块 */
" mov r0, #0 \\n"
" msr basepri, r0 \\n"/* 开中断 */
/**************************第三部分恢复现场****************************/
" ldmia sp!, {r3, r14} \\n"
" \\n"/* 将R3从栈中取出,R3指向pxCurrentTCBConst,但此时pxCurrentTCBConst可能已经在vTaskSwitchContext中修改过了 */
" ldr r1, [r3] \\n"
" ldr r0, [r1] \\n"
" ldmia r0!, {r4-r11} \\n"/* 从栈中POP出R4-R11 */
" msr psp, r0 \\n"/* 更新psp */
" isb \\n"
" bx r14 \\n"
" \\n"
" .align 4 \\n"
"pxCurrentTCBConst: .word pxCurrentTCB \\n"
::"i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
);
}
xPortPendSVHandler函数大致可以分为三部分:
-
保存现场
进入中断服务函数前,xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈中,而R4-R11就需要我们自行编写代码进行入栈。
- 找到当前就绪任务中优先级最高的
怎么找到就绪比较复杂,后面会具体介绍,这里只需要知道pxCurrentTCBConst已经指向了就绪任务中任务优先级最高的任务控制块。
- 恢复现场
和保存现场一样,从中断服务函数中退出时,堆栈会自动弹出恢复xPSR, PC, LR, R12以及R3‐R0寄存器的值。R4-R11需要在退出中断服务函数前自行编写代码恢复。
因此在FreeRtos中想要进行上下文切换(任务切换)只需要触发PendSV中断即可。
任务控制块和任务堆栈
RTOS的任务都是死循环,每个任务都拥有自己独立的栈,任务的栈从哪里来?
freeRtos在heap_4.c中声明了一个大数组,创建任务时会根据指定的栈大小从该数组分配一段空间作为该任务的栈。
任务控制块是一个包含任务所有信息的结构体,通过宏定义对内核进行剪裁时,任务控制块内的成员也会有所增减,重要的结构体成员已经添加了注释。任务间的切换主要用到了pxTopOfStack来保存栈顶。至于xStateListItem、xEventListItem、uxPriority和查找就绪任务中优先级最高任务有关,后面会具体介绍。
typedef struct tskTaskControlBlock /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
volatile StackType_t * pxTopOfStack; /*< 指向任务的栈顶.任务之间切换需要用到 */
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*< 使用MPU时需要用到. */
#endif
ListItem_t xStateListItem; /*< 状态链表节点,可以将该节点挂在不同状态(就绪、堵塞、挂起)链表中 */
ListItem_t xEventListItem; /*< 链表节点,可以将该节点挂在不同队列链表中,实现队列堵塞等功能 */
UBaseType_t uxPriority; /*< 任务优先级 */
StackType_t * pxStack; /*< 指向任务栈起始位置 */
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名字. */
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxEndOfStack; /*< Points to the highest valid address for the stack. */
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */
UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
/* Allocate a Newlib reent structure that is specific to this task.
* Note Newlib support has been included by popular demand, but is not
* used by the FreeRTOS maintainers themselves. FreeRTOS is not
* responsible for resulting newlib operation. User must be familiar with
* newlib and must provide system-wide implementations of the necessary
* stubs. Be warned that (at the time of writing) the current newlib design
* implements a system-wide malloc() that must be provided with locks.
*
* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
* for additional information. */
struct _reent xNewLib_reent;
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
/* See the comments in FreeRTOS.h with the definition of
* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
以上是关于FreeRtos学习笔记(10)任务切换原理刨析的主要内容,如果未能解决你的问题,请参考以下文章
FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析
FreeRTOSFreeRTOS学习笔记— 任务创建删除挂起和恢复
FreeRTOSFreeRTOS学习笔记(10)— FreeRTOS的osThreadDef创建任务(CMSIS_API)