FreeRTOS学习(9)-任务调度汇编函数分析(难点)

前文提要
FreeRTOS学习(1)—为什么使用RTOS
FreeRTOS学习(2)-链表和节点之结构体分析
FreeRTOS学习(3)-链表和节点程序分析
FreeRTOS学习(4)-什么是任务?
FreeRTOS学习(5)-任务之静态创建函数解析
FreeRTOS学习(6)-任务之静态创建函数解析2(重点)
FreeRTOS学习(7)-任务切换理论分析(重点)
FreeRTOS学习(8)–pendsv和systick优先级分析以及任务切换场景

调度器(Scheduler)是操作系统的核心组件之一,其主要功能是管理和协调多任务的执行。它通过决定在特定时刻哪一个任务应该获得CPU的使用权,从而实现多任务的并发执行。

1、调度器的启动由 vTaskStartScheduler()函数来完成

(1)源代码

void vTaskStartScheduler( void )
{
    /* 手动指定第一个运行的任务 */
    pxCurrentTCB = &Task1TCB;
    
    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */
    }
}

(2)代码解释

    第一个就是指定当前的任务pxCurrentTCB,我们通常通过这个全局变量 pxCurrentTCB(指向当前任务控制块的指针)来跟踪当前正在运行的任务。
    第二个就是启动调度器函数xPortStartScheduler,这个函数是里面包括汇编语言。接下来将详细解释。




2、启动调度器xPortStartScheduler函数解析

(1)函数源码

BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中断优先级为最低 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* 启动第一个任务,不再返回 */
	prvStartFirstTask();

	/* 不应该运行到这里 */
	return 0;
}

(2)代码解析

①为什么Pendsv和SysTick的优先级设置最低,上一篇博客已经写明原因。>FreeRTOS学习(8)–pendsv和systick优先级分析以及任务切换场景
②启动第一个任务prvStartFirstTask函数,单独解析。





3、在分析第一个启动任务之前,我们先介绍一下主堆栈(Main Stack)和进程堆栈(Process Stack)

(1)主堆栈(Main Stack)

是在 ARM Cortex-M 微控制器中用来保存处理器执行期间的上下文信息(如寄存器值)的内存区域。具体来说,主堆栈是由主堆栈指针(MSP)管理的堆栈。

(2)主堆栈作用

①异常和中断处理:

主堆栈主要用于处理异常(如硬件中断)和中断服务程序(ISR)。在发生中断时,处理器会自动使用主堆栈保存当前任务的上下文信息,然后跳转到中断服务程序。
当中断服务程序执行完毕后,处理器会从主堆栈中恢复上下文信息,继续执行被中断的任务。

②系统启动和初始化:

在系统启动和初始化阶段,处理器通常会使用主堆栈。这是因为在系统启动时,还没有创建其他任务,主堆栈是唯一可用的堆栈。

③特权级别的任务:

一些操作系统内核和特权级别的任务(例如,操作系统的关键任务)可能会使用主堆栈,以确保它们在受保护的堆栈中执行。

(3)进程堆栈

进程堆栈(Process Stack)通常由普通用户任务或线程使用,每个任务或线程都有自己的堆栈空间,以实现任务间的隔离和独立。

(4)ARM Cortex-M 微控制器支持两个堆栈指针:

主堆栈指针(MSP):用于管理主堆栈。
进程堆栈指针(PSP):用于管理进程堆栈。

(5)切换堆栈指针

ARM Cortex-M 处理器可以通过修改 CONTROL 寄存器来在 MSP 和 PSP 之间切换堆栈指针。具体来说,CONTROL 寄存器的第一个位(CONTROL[1])决定当前使用的是 MSP 还是 PSP:
当 CONTROL[1] 位为 0 时,处理器使用主堆栈指针(MSP)。
当 CONTROL[1] 位为 1 时,处理器使用进程堆栈指针(PSP)。




4、prvStartFirstTask函数解析

(1)源代码

__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* 在Cortex-M中,0xE000ED08是SCB_VTOR这个寄存器的地址,
       里面存放的是向量表的起始地址,即MSP的地址 */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* 设置主堆栈指针msp的值 */
	msr msp, r0
    
	/* 使能全局中断 */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* 调用SVC去启动第一个任务 */
	svc 0  
	nop
	nop
}

(2)代码解析

这段代码的主要目的是为启动第一个任务做好准备,具体步骤如下:

①从向量表读取初始主堆栈指针(MSP)并设置 MSP。

②使能全局中断,以确保中断处理可以进行。

③触发 SVC 中断,该中断处理程序将会执行第一个任务的上下文切换,从而启动第一个任务。

(3)汇编代码的逐句解析

在这里插入图片描述

在这里插入图片描述




5、 SVC 中断vPortSVCHandler函数解析

(1)源代码

__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
    
    PRESERVE8

	ldr	r3, =pxCurrentTCB	/* 加载pxCurrentTCB的地址到r3 */
	ldr r1, [r3]			/* 加载pxCurrentTCB到r1 */
	ldr r0, [r1]			/* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶 */
	ldmia r0!, {r4-r11}		/* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
	msr psp, r0				/* 将r0的值,即任务的栈指针更新到psp */
	isb
	mov r0, #0              /* 设置r0的值为0 */
	msr	basepri, r0         /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
	orr r14, #0xd           /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
                               使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
    
	bx r14                  /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
                               xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
                               同时PSP的值也将更新,即指向任务栈的栈顶 */
}

(2)汇编代码的逐句解析

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

问题1:PSP指针最终是怎么回到栈顶的?

    bx r14 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0 (任务的形参)同时 PSP 的值也将更新,即指向任务栈的栈顶。这一过程是自动完成的

问题2:为什么要进入线程模式再返回呢?

这涉及到 Cortex-M 处理器的架构特性和中断处理的实现方式:

中断处理的流程控制: 中断服务例程通常会将处理器从线程模式切换到处理器模式(如 SVC 模式),以便于完成特权级别的操作或者保存现场。完成必要的处理后,通过设置正确的返回地址和状态标志,处理器再切换回线程模式。在之前的FreeRTOS学习(7)-任务切换理论分析(重点)中已经说明。> FreeRTOS学习(7)-任务切换理论分析(重点)

(3)函数主要步骤

这段代码实现了SVC中断服务例程,用于从SVC中断返回到任务上下文。由于这是启动第一个任务的代码,因此返回的就是第一个任务的数据。

1.加载当前任务的栈指针。

2.恢复(加载)任务上下文(寄存器r4到r11)。

3.更新进程堆栈指针(PSP)。

4.清除BASEPRI寄存器,使能中断。

5.配置返回模式,以确保返回时使用PSP并进入线程模式。

6.从SVC中断返回,恢复任务的其他寄存器并开始任务执行。




6、任务切换函数portYIELD

(1)源代码

#define portYIELD()																\
{																				\
	/* 触发PendSV,产生上下文切换 */								                \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

(2)代码解释

在这里插入图片描述

所以真正切换任务的是pendsv中断函数。接下来进行详细解释。有朋友可能疑惑前面的svc怎么不叫任务切换,个人认为第一个任务启动属于任务加载。并没有用到pendsv进行切换。




7、xPortPendSVHandler( void ),pendsv中断函数详解

(1)源代码

__asm void xPortPendSVHandler( void )
{
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

    /* 当进入PendSVC Handler时,上一个任务运行的环境即:
       xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
       这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
    /* 获取任务栈指针到r0 */
	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* 加载pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加载pxCurrentTCB到r2 */

	stmdb r0!, {r4-r11}			/* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
	str r0, [r2]                /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */				
                               

	stmdb sp!, {r3, r14}        /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
                                  调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
                                  R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 进入临界段 */
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ 
	mov r0, #0                  /* 退出临界段 */
	msr basepri, r0
	ldmia sp!, {r3, r14}        /* 恢复r3和r14 */

	ldr r1, [r3]
	ldr r0, [r1] 				/* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
	ldmia r0!, {r4-r11}			/* 出栈 */
	msr psp, r0
	isb
	bx r14                      /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
                                   使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
                                   然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
                                   当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
	nop
}

(2)汇编代码的逐句解析之上文保存

在这里插入图片描述

到此完成了上一个任务的数据保存。保存结果如图。
在这里插入图片描述

(3)汇编代码的逐句解析之下一个任务数据加载

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

8、总结

在 FreeRTOS 中,调度器通过一系列复杂的操作来管理任务的切换。这些操作包括设置和切换堆栈指针、保存和恢复任务的上下文、以及使能和禁用中断。理解这些底层细节对于深入掌握 FreeRTOS 的任务调度机制至关重要。通过学习这些汇编代码和相关的 ARM Cortex-M 架构知识,我们可以更好地理解和优化我们的实时操作系统应用。

总结出FreeRTOS 任务切换的关键步骤如下:

1.触发任务切换

通过设置 PendSV 位,触发 PendSV 中断。

2.保存当前任务的上下文:

将当前任务的 CPU 寄存器值保存到当前任务的堆栈中。

3.进入临界区并调用调度器函数:

确保任务切换过程中不被中断,调用调度器函数选择下一个要运行的任务。

4.恢复(加载)下一个任务的上下文:

从下一个任务的堆栈中恢复(加载) CPU 寄存器值。

5.异常返回:

使用异常返回机制,处理器需要将下一个任务的执行环境恢复到寄存器中,然后从该任务的入口地址继续执行。

供查阅

个人学习文档,有问题欢迎大家评论交流,如果感到有用的话点个赞吧。ヽ(。◕‿◕。)ノ゚

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值