文章目录
- 前言
- 1. 预备知识
- 1.1 是谁决定了CPU运行哪段代码?
- 1.2 函数调用与返回机制
- 1.3 任务调度和上下文切换
- 1.4 堆栈指针(SP/MSP/PSP)
- 2. 代码流程分析
- 2.1 启动任务调度器
- 2.2 执行调度
- 3. 参考链接
前言
我们知道在使用FreeRTOS
时,当我们在main函数中调用了vTaskStartScheduler();
之后,FreeRTOS
的任务调度器就接管了main
()了,且不会运行main中vTaskStartScheduler();
之后的代码了。之前我一直认为FreeRTOS
中有类似于while(1)地方,所以调用了vTaskStartScheduler
();之后就不会再往下继续运行了。
然后带着这种想法我去查看了FreeRTOS
的代码,可是走进vTaskStartScheduler
()的实现,并没有找到有任何wihle
(1)的地方。
于是为了找到答案,我重新梳理了下FreeRTOS
的任务调度器是如何接管main
()函数的,这里向大家分享一下,如果有不对的地方希望大家能够评论说出来一起讨论下。
1. 预备知识
前面一直以为FreeRTOS的任务调度器是通过while(1)的方式接管的main()函数归咎其原因还是自己对代码到底是怎么运行的这块理解的不深,所以这里我们需要先了解一些基本知识,了解了这些之后后面再理解如何被接管的就更加轻松了。
1.1 是谁决定了CPU运行哪段代码?
我们在探讨FreeRTOS的任务调度器是如何接管了main()函数的运行前,需要首先了解决定CPU接下来到底运行哪段程序的因素到底是什么?是函数调用吗?还是将某个函数指针赋值到某个特定的寄存器?还是其他?
程序计数器(PC)决定了CPU的执行流程
程序计数器(PC, Program Counter
)是决定 CPU 执行哪段代码的核心机制。每条指令执行时,CPU
会读取 PC 中保存的地址,并根据该地址从内存中取出指令进行执行。执行一条指令后,PC
会根据指令类型的不同进行相应的更新,通常是递增的,指向下一条指令的地址。
-
PC 递增:程序按顺序执行时,PC 会递增到下一条指令的地址。
-
跳转指令:例如
goto、if、while、for
等控制结构会修改PC
的值,指向条件判断或跳转目标。 -
函数调用:在函数调用时,当前指令的地址(即返回地址)会被压入栈中,PC 会跳转到被调用函数的地址。函数执行完毕后,PC 会通过栈恢复到函数调用处的下一条指令。
1.2 函数调用与返回机制
函数调用和返回是程序流控制的基本方式。当一个函数被调用时,CPU
会执行以下操作:
- 保存当前地址:将当前的
PC
值(即调用指令的地址)压入栈(LR
)中,以便函数执行完后能够返回。 - 修改 PC:将 PC 修改为被调用函数的起始地址,执行该函数。
- 函数返回:函数执行完成后,
PC
会从栈中弹出返回地址,跳转回函数调用处继续执行。
1.3 任务调度和上下文切换
在操作系统中,任务调度器决定了哪个任务运行、停止,这个决策依赖于操作系统的调度策略(优先级、时间片)
当从一个任务切换到另外一个任务时,最关键的是上下文切换,指的是保存当前任务的状态(PC、寄存器、栈指针),然后将控制权转交给另一个任务。这个过程通常由时钟中断触发,操作系统会在每次中断时检查是否需要切换任务。
- 任务控制块(TCB):每个任务都有一个 TCB,其中包含了该任务的状态信息,包括 PC 和其他寄存器的值。
- 上下文保存与恢复:当任务调度发生时,当前任务的 PC 和其他寄存器的值会被保存到
TCB
中,并且下一任务的 PC 和寄存器的值会被恢复,从而实现任务的切换。
1.4 堆栈指针(SP/MSP/PSP)
堆栈指针(Stack Pointer,SP
)作为处理器中的一个关键寄存器,用于管理堆栈操作。堆栈是一个特殊的内存区域,用于存储函数调用时的局部变量、返回地址、保存的寄存器等信息。STM32
中的堆栈指针有两个主要类型:主堆栈指针(MSP
) 和 进程堆栈指针(PSP
)。
SP、MSP和PSP的关系
SP、MSP和PSP都是与堆栈指针相关的寄存器,但它们在 STM32
(基于 ARM Cortex-M 核心的微控制器)中的作用和使用方式有所不同
- SP:它实际上是 MSP 或 PSP 的别名,表示当前使用的堆栈指针。
- MSP(Main Stack Pointer,主堆栈指针):它在异常模式(如中断或者系统调用)下用于堆栈操作,是默认的堆栈指针(main函数使用的就是MSP)。
- PSP(Process Stack Pointer,进程堆栈指针):它通常用于线程模式下,即应用程序或者RTOS中的用户任务。当应用程序运行时,通常使用 PSP 进行堆栈操作,特别是在多任务操作系统中,每个任务会有自己的 PSP。
执行时的堆栈指针
- 默认情况下: 如果没有执行任务切换或终端,SP指向MSP,执行的是主堆栈。
- 中断发生时:处理器会自动使用MSP。
- 任务切换时:RTOS会切换到PSP,执行对应任务的堆栈
代码示例:切换堆栈指针
// 切换到 PSP 模式
__set_PSP(0x20002000); // 设置 PSP 指向新的堆栈地址
__set_CONTROL(__get_CONTROL() | 0x02); // 切换到线程模式(使用 PSP)// 切换回 MSP 模式
__set_CONTROL(__get_CONTROL() & ~0x02); // 切换到异常模式(使用 MSP)
__set_MSP(0x20001000); // 设置 MSP 指向主堆栈地址
堆栈指针内容
在理解堆栈指针时,大家可以这么想,每个函数都有一个属于自己的栈帧,该栈帧就是该函数的堆栈指针,其中包含了函数的返回地址以及R0~R11这些寄存器中要填充的值。
当进行函数调用或者任务切换时,实际上就是将正在使用的函数的栈信息先存下来,然后挑战转新函数或者新任务的地址去执行,同时将新函数或者任务的栈帧取出来填充到实际的R0~R11、PC、LR这些寄存器中。这就是任务的切换与还原。
2. 代码流程分析
2.1 启动任务调度器
vTaskStartScheduler
我们一般是在main()中调用vTaskStartScheduler
()启动线程。我们看下该函数,进入该函数后首先它会创建一个idle线程,这个idle线程一般是优先级最低的。idle线程是必须要有的,因为这样当没有任务需要执行时就能够执行idle
线程。否则我们想一下如果任何任务都没有那不就没有任务能够接管main
()了吗
xPortStartScheduler
vTaskStartScheduler
又会调用到prvStartFirstTask
()。
该函数主要是做一些任务调度器启动前的准备工作,例如将要用到的中断优先级拉低。然后开始启动第一个FreeRTOS
的task
prvStartFirstTask
该函数的作用:
- 恢复主堆栈指针 (
MSP
),确保系统的堆栈指针指向正确的位置。
当操作系统(如FreeRTOS
)开始调度任务时,通常会将任务切换的堆栈指针切换为 进程堆栈指针 (PSP
)。这是因为每个任务需要独立的堆栈空间,以避免不同任务之间的数据冲突或覆盖。但是在启动启动阶段没有任何任务,所以在启动第一个任务之前,必须确保MSP被正确恢复,如果不恢复系统将无法正常执行切换。 - 清除控制寄存器中的 FPU 使用位,以便于在启动任务前不占用不必要的空间。
- 启用中断,确保系统可以响应中断。
- 启动第一个任务,通过一个系统调用(
svc 0
)
另外需要注意msr control, r0
ARM Cortex-M
处理器有一个 CONTROL
寄存器,它控制系统当前使用的是 MSP
还是 PSP
。CONTROL
寄存器的第 1 位表示当前是否使用 PSP
,如果为 1,则使用 PSP
,如果为 0,则使用 MSP
。
执行svc中断(svc 0)vPortSVCHandler
我们需要先了解下SVC
中断,在arm架构中SVC
是具备特权模式的中断,一般用来从应用模式切换到特权模式(内核模式)。在特权模式下处理器有更高的权限,可以执行一些用户模式下不能执行的操作。
像msr psp, r0
这种只有在特权模式下才能被执行。
该中断的含义就是将第一个任务的的堆栈指针取出来,获取要执行任务的地址以及恢复任务的现场。
通过 msr psp, r0
指令可以恢复堆栈指针为 PSP(任务堆栈)并执行相应的任务。
这之后FreeRTOS的第一个任务就接管了main()函数,当前运行的SP也从MSP转变为了PSP。
2.2 执行调度
这块的内容可以参考我之前的一篇博客《聊一聊 - FreeRTOS的任务调度实现》
调度主要是利用tick中断以及pendSV中断来实现的。
tick
中断中执行xPortSysTickHandler
,该函数会调用xTaskIncrementTick
()增加RTOS
的tick
计数,同时检查当前是否有高优先级的任务处于就绪状态了,以及是否有任务的时间片到了可以出发任务切换了。
当xTaskIncrementTick
返回为pdTRUE
(即需要进行任务切换时),会通过
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
打开pendSV
中断。然后在pendSV
中断中执行任务的切换。
3. 参考链接
FreeRTOS学习笔记:FreeRTOS启动方式及流程
FreeRTOS的启动流程