Freertos Cortex-M3上下文切换

上下文切换是操作系统实现虚拟化的核心功能,操作系统对任务的管理通过上下文切换完成。
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"
    );
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值