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"
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"
" cpsie i \n"
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"
" nop \n"
" .ltorg \n"
);
}
SVC中断服务函数-- vPortSVCHandler()也是一个汇编函数,主要干了两件事,恢复任务现场(也就是将任务栈中保存的寄存器值POP到对应寄存器);将MSP切换为PSP;汇编语句具体含义可以对照M3权威指南中第四章指令集自行翻译。
void 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"
);
}
任务之间如何切换?
任务之间切换时需要保存现场,以便下次跳转回来时可以恢复现场,继续执行。所谓的现场就是内核的寄存器,而保存现场就是将寄存器组的当前值PUSH到栈中保存,恢复现场就是将栈中保存的寄存器值POP到对应寄存器,下图为cortex-M3寄存器组。
FreeRtos的任务切换在PendSV中断服务函数中完成的,该中断服务函数在port.c中。
void xPortPendSVHandler( void )
{
__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
" ldr r3, pxCurrentTCBConst \n"
" ldr r2, [r3] \n"
" \n"
" stmdb r0!, {r4-r11} \n"
" str r0, [r2] \n"
" \n"
" stmdb sp!, {r3, r14} \n"
" mov r0, %0 \n"
" msr basepri, r0 \n"
" bl vTaskSwitchContext \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" ldmia sp!, {r3, r14} \n"
" \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11} \n"
" msr psp, r0 \n"
" 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
{
volatile StackType_t * pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;
#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;
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
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;
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
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
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)