前文提要
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.异常返回:
使用异常返回机制,处理器需要将下一个任务的执行环境恢复到寄存器中,然后从该任务的入口地址继续执行。
供查阅
个人学习文档,有问题欢迎大家评论交流,如果感到有用的话点个赞吧。ヽ(。◕‿◕。)ノ゚