上下文切换是操作系统实现虚拟化的核心功能,操作系统对任务的管理通过上下文切换完成。
Freertos 在STM32F103上的上下文切换是本文介绍的内容。STM32F103 采用 Cortex-M3 内核 。
上下文切换的本质是对现场的保护和恢复现场,切换 CPU的运行环境。
上下文切换的触发来源于调度器,调度器根据复杂智能的调度算法来判断,是否要进行上下文切换。这换意味着一个任务被‘暂停’,任务中所做的事情都会被暂停。
1.调度器的优先级
Freertos 调度器本身的周期性运行是靠一个时间定时器systick的中断来触发的 ,假设每过1ms调度器就会随着systick的中断而运行。CUP不得不每过1ms就要去处理一下调度器的任务,增加了系统开销。这个时间间隔过短就会频繁中断CPU,过长就会让任务无法及时的切换。通常这个时间间隔选择1ms。
实时系统管关注的是实时性,Freertos做了一个非常聪明的机制。设定systick的中断优先级,那么比这个优先级高的中断就不会被打断,这样在系统中需要及时处理的问题就不会受到调度器的干扰。
这个优先级的定义
configMAX_SYSCALL_INTERRUPT_PRIORITY
默认的设定是 调度器不希望打断任何中断,所以把systick的中断优先级 设置为所有中断中最低的优先级。这样调度器不会打乱任何中断的运行。
那么这个中断优先级是如何设定的,打开Cortex-M3 权威指南
下图是异常列表
0~15是16 个 Cortex-M3 的中断线 对于外部中断,从第十六个开始。
SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号: 15)。来产生操作系统需要的滴答中断,作为整个系统的时基。
11 为SVCall,14为PendSv 上下文切换就放在11和14中进行
所有我们要把他配置为最低的中断优先级就可以了。直接写0不就完事了吗?
挺有趣的, Cortex-M3 中0是可以配置的最大优先级,在 Cortex-M3 中优先级寄存器数字越小代表优先级最高。所以我们就是要确定优先级寄存器最大能填多少,这样就能找到最低优先级了。
这里已经很有趣又有点绕了,STM32F103在优先级设定上又做了一次设计,神操作让我们设定一下优先级 会变的的这么复杂。Cortex-M3 设计优先级的时候,一个中断节点优先级用8bit表示。STM32F103只用了其中4个,而且是最高位4个。所以中断优先级一共222*2=16级。但是IC设计人员没有用8位,用的高4位 所以,16 优先级来说来说就是
1111 0000 =240
Freertos的开发人员为了应对不同IC厂的操作,他们想了一个办法。
就是先拿一个中断寄存器出来,写个最大值进去,ARM Cortex-M3 给了8bit干脆就写个256(0xff)进去。然后读出来看看是多少,比如写 1111 1111(0xFF )进去读出来是 1111 0000,那显然这IC是用了高4位。
然后我们看Freertos的源码xPortStartSchedule一下截取一部分
xPortStartScheduler()
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
/* Determine the maximum priority from which ISR safe FreeRTOS API
* functions can be called. ISR safe functions are those that end in
* "FromISR". FreeRTOS maintains separate thread and ISR API functions to
* ensure interrupt entry is as fast and simple as possible.
*
* Save the interrupt priority value that is about to be clobbered. */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* Determine the number of priority bits available. First write to all
* possible bits. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
这里一堆代码就是干这个事情,最后怕搞出事情来,再把原来的值给写回去
谁是这个幸运儿呢?就是第16个中断节点,IRQ #0,显然他是一个外人。
ulOriginalPriority = *pucFirstUserPriorityRegister;
*pucFirstUserPriorityRegister = ulOriginalPriority;
优先级的事情说完了。
2.异常处理
异常差不多就可以认为是我们原来学过的中断,异常有时候说是来源于内核。我们 原来所学的中断基本上是说来源于外部。有的时候异常也叫做陷阱。种之就是停下当前做的事情去执行其他的事情。Cortex-M3权威指南把这些都统称为异常。这个其他事情的入口就是异常向量表了。我们发起一个异常请求,那么CPU就会做异常跳转。跳转也是 一种上下文切换,硬件会保存一些寄存器的值。
3.栈
能关注上下文切换的同学都已经知道什么是栈了。
在 CPU启动的时候,为了建立C语言的运行环境 ,已经设置了一个栈 MSP指向栈顶。异常的调转发生时,硬件会做上下文保存入MSP系统主栈,可以认为这个是裸机栈。保存的内容为
4.上下文切换
Cortex-M3 的双堆栈机制
任务创建的时候我们为每个任务设定了栈空间,这个空间用来保存任务在CPU中运行的上下文,也就是CPU中和任务执行相关的数据,如果这些数据丢了,那么任务也就没办法恢复运行了。
一般来说CPU再进行异常调转的时候,因为异常调转是硬件进行的,所以他会自动保存一些数据。这个是下上文的一部分。那么自动保存到哪里去了呢?默认情况下是主栈MSP。开机到运行Main,都在使用MSP。裸机的情况下所有的代码都是一个任务,这个MSP就是大任务的栈。有个时候,要把这个 栈切换 到任务自己的栈。
使用线程模式,CPU硬件入栈 对应的栈为PSP栈
使用内核模式,CPU硬件入栈,对应的是MSP栈
通过读取 PSP 的值, OS 就能够获取用户应用程序使用的堆栈,进一步地就知道了在发
生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用STMDB 和LDMIA
的书写形式)。 OS 还可以修改 PSP,用于实现多任务中的任务上下文切换。
上下文的切换的重点
1:搞清楚所使用的栈 MSP,PSP,各个任务的栈
2:上下文寄存器 哪些手动保存,哪些由硬件自动保存。
3:不同模式下,内核选择使用什么栈
笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由 SP 给出。寄存器的数据通
过 PUSH 操作存入堆栈,以后用 POP 操作从堆栈中取回。在 PUSH 与 POP 的操作中, SP 的
值会按堆栈的使用法则自动调整,以保证后续的 PUSH 不会破坏先前 PUSH 进去的内容。
任务初始化的模拟入栈
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;
}
__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 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
}
/*-----------------------------------------------------------*/
static void prvPortStartFirstTask( void )
{
__asm volatile(
" ldr r0, =0xE000ED08 \n" /向量表偏移寄存器地址 CotexM3/
" ldr r0, [r0] \n" /取向量表地址/
" ldr r0, [r0] \n" /取 MSP 初始值/
/重置msp指针 宣示 系统接管/
" msr msp, r0 \n"
" cpsie i \n" /开中断/
" cpsie f \n" /开异常/
/流水线相关/
" dsb \n" /数据同步隔离/
" isb \n" /指令同步隔离/
/触发异常 启动第一个任务/
" svc 0 \n"
" nop \n"
);
}
第一个程序运行的时候主栈复位。然后跳转到SVC处理第一次上下文切换
void vPortSVCHandler( void )
{
__asm volatile (
/取 pxCurrentTCB 的地址/
“ldr r3, pxCurrentTCBConst2 \n”
/取出 pxCurrentTCB 的值 : TCB 地址/
“ldr r1, [r3] \n”
/*取出 TCB 第一项 : 任务的栈顶 */
“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”
);
首先发生SVC后,ARM处理器会转为 主栈,自动保存R0 ~ PSR
进入SVC后恢复R4~R11
然后清洗流水线返回线程模式。这个时候线程栈中的R0~PSR(任务初始化是填入的值)就会恢复到CPU,从而完成切换。
正常情况下的上下文切换
PUSH 指令和 POP 指令默认使用 SP。
stmdb用于将寄存器压栈,ldmia用于将寄存器弹出栈
1.进入PSV后,CPU自动切换为主栈 也就是当前sp为异常向量表开头设置的主栈
2.mrs r0, psp 获取当前 任务栈的位置
3.保存当前任务控制块
4.把R4~R11 保存到当前 任务栈中,应为r4-r11不会自动入栈。
5.然后保存当前r0也就是当前任务的栈位置
6.执行一次 C函数,保存当前 r3 r14
7.执行选取目标任务
8.恢复r4~r11 这部分是需要手动保存
9.这个时候需要更新到新任务堆栈应为返回的时候,会把psp恢复到CPU
10.线程模式返回
void xPortPendSVHandler( void )
{
/* This is a naked function. */
__asm volatile
(
/*取出当前任务的栈顶指针 也就是 psp -> R0*/
" mrs r0, psp \n"
" isb \n"
" \n"
/*取出当前任务控制块指针 -> R2*/
" ldr r3, pxCurrentTCBConst \n"
" ldr r2, [r3] \n"
" \n"
/*R4-R11 这些系统不会自动入栈,需要手动推到当前任务的堆栈*/
" stmdb r0!, {r4-r11} \n"
/*最后,保存当前的栈顶指针
R0 保存当前任务栈顶地址
[R2] 是 TCB 首地址,也就是 pxTopOfStack
下次,任务激活可以重新取出恢复栈顶,并取出其他数据
*/
" str r0, [r2] \n"
" \n"
/*保护现场,调用函数更新下一个准备运行的新任务*/
" stmdb sp!, {r3, r14} \n"
/*设置优先级 第一个参数,
即:configMAX_SYSCALL_INTERRUPT_PRIORITY
进入临界区*/
" 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"
/*函数返回 退出临界区
pxCurrentTCB 指向新任务
取出新的 pxCurrentTCB 保存到 R1
*/
" ldr r1, [r3] \n"
/*取出新任务的栈顶*/
" ldr r0, [r1] \n"
/*恢复手动保存的寄存器*/
" ldmia r0!, {r4-r11} \n"
/*设置线程指针 psp 指向新任务栈顶*/
" msr psp, r0 \n"
" isb \n"
/*返回, 硬件执行现场恢复
开始执行任务
*/
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB \n"
);
}