无OS的堆栈结构:此时每个任务的堆栈都在一起
在FreeRTOS中对线程、进程没有明显区分,统一称为任务。
有OS的堆栈结构:每个任务都有独立栈空间,用于保存上下文信息和全局变量
ARM 架构的寄存器可以分为以下几类:
通用寄存器:ARM 架构有 17 个 32 位的通用寄存器,它们命名为 R0-R15,其中 R13-R15 被用作栈指针、链接寄存器(LR)和程序计数器(PC)。
状态寄存器:也称为程序状态寄存器(PSR),用于存储当前处理器的状态信息,例如条件码、中断使能位等。
特殊寄存器:包括控制寄存器、处理器 ID 寄存器、系统控制寄存器等,它们用于控制处理器的一些重要行为和特性。
浮点寄存器:ARMv7 架构引入了 VFP 协处理器,增加了 32 个浮点寄存器(S0-S31),可用于高精度浮点运算。
特殊寄存器是指在 ARM 架构中具有特殊用途的寄存器,包括以下几个:
控制寄存器(Control Register):用于控制处理器的一些重要行为和特性,如异常处理、缓存设置等。
处理器 ID 寄存器(Processor ID Register):用于标识处理器的类型和版本信息。
系统控制寄存器(System Control Register):用于控制系统级的功能和特性,如内存管理单元(MMU)的设置、缓存控制等。
中断控制寄存器(Interrupt Control Register):用于控制中断的使能和屏蔽,包括中断优先级、中断屏蔽位等。
定时器控制寄存器(Timer Control Register):用于控制定时器的计数和触发方式,包括定时器的频率、计数模式等。
异常控制寄存器(Exception Control Register):用于控制异常处理和异常向量表的地址。
这些特殊寄存器在 ARM 架构中扮演着关键的角色,可以控制和配置处理器的各种功能和特性,用于实现更高级的操作和控制。
一般来说Cortex-M系列有两种工作模式:
Thread Mode (线程模式):程序按照编译好的代码顺序执行
Handler Mode(中断模式):收到中断信号并执行中断处理函数
所以,Cortex-M系列内核使用了双堆栈,即MSP和PSP
R13在任何时刻只能是其中一个,默认情况为MSP,可以通过控制寄存器:CONTROL寄存器的bit1来改变。
关于MSP和PSP的选用,其是通过CONTORL寄存器来配置,仅在Thread Mode下才可设置CONTORL寄存器。一般情况下,没有必要使用PSP,除非是有os存在时
,MSP用于os内核的sp,而PSP用于thread级app的sp,这两个sp需严格分开。
在编译器中
,可以通过r13(R13)或sp(SP)来访问堆栈(具体是MSP和PSP由当时环境决定);也可以通过指定的MRS、MSR指令来访问MSP和PSP。
控制寄存器有两个用途,其一用于定义特权级别,其二用于选择当前使用哪个堆栈指针。由两个比特来行使这两个职能。
在裸机开发中,CONTROL的bit1始终是0,也就是说裸机开发中全程使用程MSP
,并没有使用PSP。在执行后台程序(大循环程序)SP使用的是MSP,在执行前台程序(中断服务程序)SP使用的是MSP
。
在OS开发中,当运行中断服务程序的时候CONTROL的bit1是0,SP使用的是MSP;当运行线程程序的时候CONTROL的bit1是1,SP使用的是PSP
。MSP是系统复位后(即其处于Handler Mode)的指定sp(vector table的前4Byte自动载入),用于处理异常中断。当结束Reset_Handler后,cpu进入正常运行状态(即其处于Thread Mode),仅在此状态下PSP才能被使用,当然MSP也可以使用。其后如有硬中断来临,则进入Handler Mode,如果硬件中断结束,则返回Thread Mode。
Cortex-M0中堆栈方向是向低地址方向增长,为满堆栈机制。堆栈操作是通过PUSH和POP来完成操作的。实际上,除了POP指令可以从栈顶中取数据外;MOV指令也可从任意位置取数据,但不会影响栈结构(即不影响其sp)
压栈与弹栈的操作:
在普通的函数嵌套调用中,当一个函数调用另一个函数时,程序会将当前函数的执行现场(包括 PC(程序计数器)、当前函数的状态、参数、返回地址等)保存在当前函数的栈帧中
。接着,将栈指针 SP(stack pointer)移动到新的栈帧的栈顶位置(一般是上一个栈帧的底部位置),然后继续执行被调用的函数。
在这个过程中,SP 指针的变化如下:
进入被调用函数前:将当前函数的执行现场保存在当前函数的栈帧中,SP 指针不变。
进入被调用函数时:将新的栈帧的起始位置指定为上一个栈帧的底部,这个位置需要根据前一个栈帧的大小确定,即 SP 指针移动到新的栈帧的底部位置。
执行被调用函数时:如果有局部变量,则将这些变量分配在新的栈帧中。此时,SP 指针可能会进一步移动到这些变量的内存起始位置。
返回到调用函数时:将被调用函数的返回值或状态存储在新的栈帧中,并且将 SP 指针恢复到上一个栈帧的底部位置
,以便取出当前函数的执行现场,同时恢复上一个栈帧中的现场并继续执行。
需要注意的是,在函数嵌套调用过程中,递归调用、多层嵌套和异常处理等情况会使 SP 指针的变化更加复杂。因此,在编写和调试程序时,需要仔细注意栈帧的大小、内存分配和释放以及异常处理等问题,以确保程序的正确性和稳定性。
首先,设立双指针是为了保证OS的安全性和稳健性。通常来说,操作系统和异常事件(中断或其他fault)使用MSP,用户程序(线程)使用PSP
。MSP与PSP指针之间的切换会在处理异常事件时自动完成。
本质上,区别于用户程序使用PSP,操作系统和异常事件单独使用一个MSP指针的目的,是为了保证栈数据不会被用户程序意外访问或栈空间被用户程序占用
。比如,当应用程序发生栈溢出问题时,必须要确保应用程序的故障不会影响到操作系统的运行和异常事件的处理——也就需要保证始终要有栈空间来执行异常事件。
裸机操作时,使用的就是MSP指针。其实OS在上电复位到切换线程之前用的都是MSP指针,也就是线程切换之前都是一个裸机程序的状态。
参考博文:
关于FreeRTOS的底层实现和基础认识