FreeRTOS-ARM架构深入理解

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转FreeRTOS
💬保持学习、保持热爱、认真分享、一起进步!

前言

由于FreeRTOS操作系统所涉及的ARM架构的知识较多,而且这是知识对理解FreeRTOS的本质和底层实现至关重要,仿佛ARM架构是为操作系统量身定制一般,所以ARM架构的知识的重要性我就不说了,本篇文章主要是对操作系统底层实现所用到ARM架构的知识进行汇总,所以本篇文章参考《Cortex-M3权威指南》,也是对我上篇文章《FreeRTOS-ARM架构与程序的本质》的补充。

一.寄存器组

CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通
用目的”的,特殊功能寄存器有预定义的功能,而且必须通过专用的指
令来访问。

1.通用寄存器组

在这里插入图片描述
R0 - R3 : 子程序传递参数(函数传参保存参数)

R4 - R11:子程序保存局部变量

R12:子程序间的scratch寄存器,记为IP 暂存寄存器

R13:堆栈指针(别名SP):栈指针寄存器。在进入子程序时,和退出子程序时,值必须相等。(意思是调用函数申请栈帧(供函数使用的一段内存)退出函数释放栈帧)

在 CM3 处理器内核中共有两个堆栈指针,于是也就支持两个堆栈。 当引用 R13(或写作 SP)时,你引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问(MRS,MSR 指令)。这两个堆栈指针分别是:

  • 主堆栈指针(MSP),或写作 SP_main。这是缺省的堆栈指针,它由 OS 内核、异常服务例程(中断服务函数)以及所有需要特权访问的应用程序代码来使用。
  • 进程堆栈指针(PSP),或写作 SP_process。用于常规的应用程序代码(不处于异常服 用例程中时)。

需要注意:
1.这里所说的堆栈指针,与堆无关,是指向栈的指针,堆栈指针只是习惯性的叫法。
2.有两个堆栈指针,不是代表有两个栈而是代表两个栈指针,虽然两个指针指向不同的栈,而且同一时间只能使用一个。
3.不是所有的情况都需要两个堆栈指针,只是在简单的裸机中一般使用主堆栈指针就好,如果是在由操作系统的情况下,系统/进中断时候的栈使用主堆栈指针(MSP),而任务的栈使用进程堆栈指针(PSP)。

至于为什么需要两个堆栈指针,到底有什么作用,后面有详解的解释。

R14:连接寄存器(别名LR):用于保存子程序的返回值,不像大多数其它处理器,ARM 为了减少访问内存的次数,把返回地址直接存储在寄存器中。这样足以使很多只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。

通俗点讲:LR寄存器是用来保存函数的返回地址,在调用一个函数(上面所说的子程序)前保存该下一条指令的地址,执行完函数,要回过来继续执行下一条指令,如果只调用一次函数不需要将LR保存的返回地址入栈,如果调用的函数又去调用函数所以LR保存的最初的返回地址将会被覆盖,所以得将LR的值入栈,去保存新的返回地址。(大概有个概念就好后面会结合实际代码讲解保证你理解返回地址的作用,以及为什么当有多层调用关系的时候为什么要保存LR的值到内存(栈)中)

R15:程序计数寄存器(别名PC):指向当前的程序地址。如果修改它的值,就能改变程序的执行流,指向当前的正在运行的指令地址。(通俗点讲:程序运行到哪就指向哪个位置,改变它就能改变程序执行的位置)

CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1,最后一位为1表示Thumb状态),用以表明这是在Thumb 状态下执行。倘若写了 0,则视为企图转入 ARM 模式,CM3 将产生一个 fault 异常。

2.特殊功能寄存器组

除了寄存器组中的寄存器外,处理器中还存在多个特殊寄存器,这些寄存器表示处理器状态、定义了操作状态和中断/异常屏蔽。
在使用C等高级编程语言开发简单的应用时,需要访问这些寄存器的情形不多。在操作系统或需要高级中断屏蔽特性时这些特殊寄功能寄存器就派上用场了。在这里插入图片描述

Cortex‐M3 中的特殊功能寄存器包括:

  • 程序状态寄存器组( xPSR)
  • 中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)
  • 控制寄存器(CONTROL)

它们只能被专用的 MSR 和 MRS(特殊)指令访问,而且它们也没有存储器地址。

1.程序状态寄存器组( xPSR)

程序状态寄存器在其内部又被分为三个子状态寄存器:

  • 应用程序 PSR(APSR):应用PSR(APSR) : 包含前一条指令执行后的条件标志,比较结果:大于等于,小于,进位等等。
  • 中断号 PSR(IPSR):包含当前ISR的异常编号
  • 执行 PSR(EPSR):包含Thumb状态位

通过 MRS/MSR 指令,这 3 个 PSRs 即可以单独访问,也可以组合访问(2 个组合,3 个组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
1.EPSR寄存器中24位为1表示thumb状态,清零会触发系统异常。
2.IPSR寄存器的0~8位用来表示正在执行的中断服务程序的中断号。

2.中断屏蔽寄存器组

PRIMASK,FAUITMASK和 BASEPRI寄存器都用于异常或中断屏蔽这些特殊寄存器可基于优先等级屏蔽异常,只有在特权访问等级(什么是特权模式后面会讲解)对它们进行操作(非特权状态下的写操作会被忽略,而读出则会返回0)。它们默认全部为0,也就是屏蔽(禁止异常/中断)不起作用。在这里插入图片描述
下面这个图一定要认真看完:
在这里插入图片描述
上面提到MNI异常还有硬件中断是中断向量表中优先级比较高的中断,而NMI是不可屏蔽中断。
在这里插入图片描述
NMI中断:不可屏蔽中断,产生这个中断的时候,表示系统发生了致命的错误,通知CPU发生了灾难性事件,如电源掉电、总线奇偶位出错等所以一定不屏蔽。
关于更多中断内容请参考:《中断-NVIC与EXTI外设详解(超全面)》

1)PRIMASK

在许多应用中,可能都需要暂时禁止所有中断以执行一些时序关键的任务,此时可以使用PRIMASK寄存器。PRIMASK寄存器只能在特权状态访问。
PRIMASK用于禁止除NMI和 HardFault外的所有异常,它实际上是将当前优先级改为0(最高的可编程等级)。

如何访问PRIMASK寄存器?
1.如用C编程,可以使用CMSIS-Core提供的函数来设置和清除PRIMASK:
在这里插入图片描述
2.对于汇编编程,可以利用CPS(修改处理器状态)指令修改PRIMASK寄存器的数值。在这里插入图片描述
3.PRIMASK寄存器还可通过MRS和 MSR指令访问。
在这里插入图片描述
注意:虽然通过往PRIMASK寄存器写 1 屏蔽了中断,但是中断依然会被挂起,只不过得不到执行,当PRIMASK清零时被挂起的中断会立即指向,PRIMASK 只是屏蔽掉中断,而并不是不让中断源产生中断。

2)FAULTMASK

从行为来说,FAULTMASK和PRIMASK很类似,只是它实际上会将当前优先级修改为-1,这样一来 HardFault处理也会被屏蔽。当FAULTMASK置位时,只有NMI 异常处理才能执行。

FAULTMASK寄存器只能在特权状态访问,不过不能在NMI和 HardFault处理中设置。
如何访问FAULTMASK寄存器?
1.若在C编程中使用符合CMSIS的设备驱动,则可以使用下面的CMSIS-Core函数来设置和清除FAULTMASK
在这里插入图片描述
2.对于汇编编程,可以利用CPS(修改处理器状态)指令修改FAULTMASK寄存器的数值。
在这里插入图片描述
3.FAULTMASK寄存器还可通过MRS和 MSR指令访问。
在这里插入图片描述
注意:FAULTMASK会在退出异常处理时被自动清除,从NMI处理中退出时除外。
由于这个特点就可以在一个低优先级中断里面设置FAULTMASK为1,此时如果发生一个高优先级中断则该中断会被挂起,等低优先级中断处理完后,退出中断自清除FAULTMASK,因此,可以强制让高优先级处理在低优先级处理结束后开始执行。

3)BASEPRI

有些情况下,可能只想禁止优先级低于某特定等级的中断,此时,就可以使用BASEPRI寄存器。要实现这个目的,只需简单地将所需的屏蔽优先级写入BASEPRI寄存器。例如,若要屏蔽优先级小于等于0x60的所有异常,则可以将这个数值写入BASEPRI:

如何访问BASEPRI寄存器?
1.设置屏蔽
在这里插入图片描述
2.取消屏蔽
在这里插入图片描述

对与操作系统而言,PRIMASK 和 BASEPRI 对于暂时关闭中断是非常重要的。而FAULTMASK 则可以被操作系统 用于暂时关闭 fault 处理机能,这种处理在某个任务崩溃时可能需要。因为在任务崩溃时,常常伴随着一大堆 faults。在系统料理“后事”时,通常不再需要响应这些 fault——人死帐清。

  • 为了快速开关中断CM3专门设置了一条CPS指令

在这里插入图片描述

3.控制寄存器(CONTROL)

控制寄存器用于定义特权级别,还用于选择当前使用哪个堆栈指针
在这里插入图片描述

  • CONTROL[1]

1.在 Cortex‐M3 的 handler 模式中,CONTROL[1]总是 0。在线程模式中则可以为 0 或 1。(在线程模式可以使用MSP也可以使用PSP,而handler 模式(中断模式)只能使用MSP)
2.==仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。==改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的第2位,也能实现模式切换(在后面中断的处理的过程会详细讲)。

  • CONTROL[0]

仅当在特权级下操作时才允许写该位。一旦进入了用户级,唯一返回特权级的途径,就是触发一个(软)中断(因为中断是特权级),再由服务例程改写该位。
CONTROL 寄存器也是通过 MRS 和 MSR 指令来操作的:
在这里插入图片描述

二.处理器操作模式

1.handler与线程模式 特权级与用户级

Cortex‐M3 支持 2 个模式和两个特权等级。

在这里插入图片描述
上图的意思是:在 CM3 运行主应用程序时(线程模式),既可以使用特权级,也可以使用用户级;但是异常服务例程必须在特权级下执行,如下图所示。
在这里插入图片描述

处理器在特权模式与用户模式有何区别?

  • 1.处理器在特权模式下,程序可以访问所有范围的存储器(如果有 MPU,还要在 MPU 规定的禁地之外),并且可以执行所有指令,包括内核的所有寄存器。
  • 2.处理器在用户模式下,对系统控制寄存器 NVIC、SCB(系统控制寄存器)和特殊寄存器(通过 MSR/MRS 访问的寄存器)的访问将被阻止(除了 APSR,因为 APSR 是专门用于给应用程序标志位的);如果在用户模式下,访问了上述寄存器,将会抛出硬件异常 。

所以特权级和用户级的区别体现在对访问内核寄存器的限制。

正常情况下,系统复位后,处理器处于特权级+线程模式(因为系统复位后,一般都需要先配置内核寄存器)

在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。
用户级下的代码不能再试图修改 CONTROL[0]来回到特权级(因为CONTROI寄存器只能在特权访问等级进行修改操作,而读取操作则在特权和非特权访问等级都可以)。则它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级

在这里插入图片描述

在这里插入图片描述
若使用嵌入式操作系统,每次上下文切换时都可以重新编程CONTROL寄存器,以满足应用间不同的特权访问等级需要。

  • 当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式(由线程模式-handler模式-线程模式)的转换在这里插入图片描述

  • 但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末,both 处理器模式和特权等极都要发生变化。
    在这里插入图片描述

  • 为什么需要两个模式?
    引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码(中断服务函数的代码)。

  • 为什么要分特权级与用户级?

把代码按特权级和用户极分开对待,有利于使架构更加安全和健壮。例如,当某个用户代码出问题时,不会让它成为害群之马,因为用户级的代码是禁止写特殊功能寄存器和 NVIC中寄存器的。另外,如果还配有 MPU,保护力度就更大,甚至可以阻止用户代码访问不属于它的内存区域。

2.Cortex-M3 的双堆栈机制

CM3 的堆栈是分为两个:主堆栈和进程堆栈

在这里插入图片描述

  • CONTROL[1]

在 Cortex‐M3 的 handler 模式中,CONTROL[1]总是 0。在线程模式中则可以为 0 或 1。==仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。==改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的第二位。

1.对于未使用操作系统的裸机程序的多数简单应用,对于不具有操作系统的简单应用,可以在所有操作中只使用MSP,而不用管PSP,无须修改CONTROL寄存器的数值。整个应用可以运行在特权访问等级并且只使用MS。
在这里插入图片描述
2.对于具有RTOS的系统﹐异常处理(包括部分RTOS内核)使用MSP ,而应用任务则使用PSP。每个应用任务都有自己的栈空间。RTOS中的上下文切换代码在每次切换任务时都会更新PSP(指向下一个任务的栈)。
在这里插入图片描述

  • 这样做的优点?

为了避免系统堆栈因应用程序的错误使用而毁坏,你可以给应用程序(也就是操作系统中任务,一个任务一个栈)专门配一个堆栈,不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用 PSP,而异常服务例程则使用 MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程时由硬件处理。

  • CONTROL[1]=0 时,只使用 MSP,此时用户程序和异常 handler 共享同一个堆栈。这也是复位后的缺省使用方式
    在这里插入图片描述
  • 当 CONTROL[1]=1 时,线程模式将不再使用MSP ,而改用 PSP(而handler 模式永远使用 MSP)
    在这里插入图片描述
  • 在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,PUSH和POP指令实现的栈操作及使用SP(R13)的多数指令都会使用当前选择的栈指针,还可以利用MRS和 MSR指令直接访问MSP和 PSP。
    在这里插入图片描述

通过读取 PSP 的值,操作系统就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(到底在切换任务时要保存那些寄存器以及中断整个流程是我们后面需要啃的硬骨头,先明白基本的ARM架构的知识)。操作系统还可以修改 PSP,用于实现多任务中的任务上下文切换

三.指令集

在FreeRTOS的内核实现中,会有关于汇编指令的运用来实现某些寄存器的操作,对与汇编我们不需要掌握太深,只需了解一些基本的语法,和一些常用的汇编指令,而且也不需要去特别记忆知道是什么意思就好,那些不常见的汇编指令等碰见了在查就好了。
本文就简单介绍一些常用的汇编指令和一些基本语法。

1.ARM,Thumb,Thumb-2指令集的关系

ARM处理器一直支持两种形式上相对独立的指令集,它们分别是

  • 32位的ARM指令集。对应处理器状态:ARM状态
  • 16位的Thumb指令集。对应处理器状态:Thumb状态
    在程序的执行过程中,处理器可以动态地在两种执行状态之中切换。实际上,Thumb指令集在功能上是ARM指令集的一个子集,但它能带来更高的代码密度(指令从32位变成16位)。

而Thumb‐2是16位Thumb指令集的一个超集,在Thumb‐2中,16位指令首次与32位指令并存,结果在Thumb状态下可以做的事情一下子丰富了许多,同样工作需要的指令周期数也明显下降。
在这里插入图片描述
在过去,做 ARM 开发必须处理好两个状态。这两个状态是井水不犯河水的:32 位的ARM 状态和 16 位的 Thumb 状态。

  • 当处理器在 ARM 状态下时,所有的指令均是 32 位的,此时性能相当高。
  • 而在 Thumb 状态下,所有的指令均是 16 位的,代码密度(一条指令所需要的空间减少了, 因为代码经过编译连接形成二进制的指令都需要存储在ROM可能是flash上所以一条指令所占空间的大小决定了flash存储指令所占用的空间大小)提高了一倍。不过,thumb状态下的指令功能只是 ARM 下的一个子集,结果可能需要更多条的指令去完成相同的工作,导致处理性能下降。

所以ARM指令集与thumb指令集各有优缺点,为了取长补短,很多应用程序都混合使用 ARM 和 Thumb 代码段。然而,这种混合使用是有额外开销的,时间上的和空间上的都有,主要发生在状态切换之时。另一方面,ARM 代码和 Thumb 代码需要以不同的方式编译,这也增加了软件开发管理的复杂度。
在这里插入图片描述Cortex‐M3 内核干脆都不支持 ARM 指令,中断也在 Thumb 态下处
理(以前的 ARM 总是在 ARM 状态下处理所有的中断和异常),而新起之秀Thumb‐2 指令集,因为它同时兼容 32 位指令和 16 位指令,既可以节省空间,处理性能又强,而且易于使用。

而我们的Cortex‐M3 只支持Thumb‐2 指令集原因

  • 消灭了状态切换的额外开销,节省了执行时间和指令空间,取指都按32位处理,留下了更多带宽给数据传输。
  • 不再需要把源代码文件分成按 ARM 编译的和按 Thumb 编译的,软件开发的管理大大减负。
  • CM3支持绝大多数传统的Thumb指令,因此用Thumb指令写的汇编程序也可以在CM3运行,而且在使用Thumb‐2 指令集后无需将处理器状态在Thumb和ARM之间来回的切换了。

2.汇编语言基础

汇编语言:基本语法
在这里插入图片描述
1.标号是可选的,如果有,它必须顶格写。标号的作用是让汇编器来计算程序转移的地址。

2.操作码是指令的助记符,它的前面必须有至少一个空白符,通常使用一个“Tab”键来产生。操作码后面往往跟随若干个操作数,而第 1 个操作数,通常都给出本指令执行结果的存储地。不同指令需要不同数目的操作数,并且对操作数的语法要求也可以不同。

  • 立即数必须以“#”开头
    什么是立即数:在立即寻址方式指令中给出的数
    在这里插入图片描述

  • 注释均以”;”开头,它的有无不影响汇编操作,只是给程序员看的,能让程序更易理解。

汇编语言:统一的汇编语言
为了最有力地支持 Thumb‐2,引了一个“统一汇编语言(UAL)”语法机制。对于 16 位指令和 32 位指令均能实现的一些操作(常见于数据处理操作),有时虽然指令的实际操作数不同,或者对立即数的长度有不同的限制,但是汇编器允许开发者以相同的语法格式书写,并且由汇编器来决定是使用 16 位指令,还是使用 32 位指令。以前,Thumb 的语法和 ARM的语法不同,在有了 UAL 之后,两者的书写格式就统一了。

在 Thumb‐2 指令集中,有些操作既可以由 16 位指令完成,也可以由 32 位指令完成。例如,R0=R0+1 这样的操作,16 位的与 32 位的指令都提供了助记符为“ADD”的指令。在UAL 下,你可以让汇编器决定用哪个,也可以手工指定是用 16 位的还是 32 位的。
在这里插入图片描述
W(Wide)后缀指定 32 位指令。如果没有给出后缀,汇编器会先试着用 16 位指令以缩小代码体积,如果不行再使用 32 位指令。因此,使用“.N”其实是多此一举,不过汇编器可能仍然允许这样的语法。

其实在绝大多数情况下,==程序是用 C 写的,C 编译器也会尽可能地使用短指令(因为节省空间)。==然而,当立即数超出一定范围时,或者 32 位指令能更好地适合某个操作,将使用 32 位指令。

3.数据传送指令

处理器的基本功能之一就是数据传送。CM3 中的数据传送类型包括
1.两个寄存器间传送数据
2.寄存器与存储器间传送数据
3.寄存器与特殊功能寄存器间传送数据
4.把一个立即数加载到寄存器

在这里插入图片描述
1.读内存:LDR R0 [Addr]

在这里插入图片描述 - 2.写内存: STR R0 [SP,#4]
在这里插入图片描述

在这里插入图片描述
还可以一次读取/存储多个字。
在这里插入图片描述
Rd 后面的"!"表示要自增(Increment)或自减(Decrement)基址寄存器 Rd的值,时机是在每次访问前(Before)或访问后(After)。增/减单位:字(4字节)。
上表中,加粗的是符合 CM3 堆栈操作的 LDM/STM 使用方式。并且,如果 Rd 是 R13(即 SP),则与POP/PUSH 指令等效。(LDMIA‐>POP, STMDB ‐> PUSH)在这里插入图片描述

  • 把一个地址写到某寄存器中
    在这里插入图片描述

4.数据处理指令

  • 加减乘除

在这里插入图片描述

  • 逻辑操作指令
    在这里插入图片描述

5.函数调用与无条件转移指令

  • 最基本的无条件转移指令有两条:

在这里插入图片描述

在BX中,reg的最低位指示出在转移后,将进入的状态是ARM(LSB=0)还是Thumb(LSB=1)。因为Cortex‐M3 只支持Thumb状态,就必须保证 reg 的 LSB=1(最低位为1表示Thumb指令集),否则 fault 伺候

  • 调用函数时,需要保存返回地址,对应的指令是:

在这里插入图片描述
执行这些指令后,就把返回地址存储到 LR(R14)中了,从而才能使用”BX LR”等形式返回。
所以加后面加上X只不过是转移到寄存器给出的地址
调用完函数后具体如何返回请参考->《FreeRTOS-ARM架构与程序的本质》

注意:BLX指令还带有改变状态的功能(ARM与Thumb状态切换)。但是Cortex‐M3 只支持Thumb状态,因此寄存器的最低位必须是1(表示Thumb状态),以确保不会试图进入 ARM 状态。如果忘记置位 LSB,则 fault 伺候

6.访问特殊寄存器指令

这两条指令是访问特殊功能寄存器的“绿色通道”,但是必须在特权级下,除 APSR 外。指令语法如下:
在这里插入图片描述
Sreg:特殊寄存器
在这里插入图片描述
下面给出一个指定 PSP 进行更新的例子:
在这里插入图片描述

7.快速开关中断

在这里插入图片描述

四.中断时现场的保存与恢复

终于到了ROTS任务调度的关键之一,要想理解FreeRTOS任务调度器的实现,必须清楚中断的具体行为。

首先我们知道中断发生之后需要去调用中断服务函数,但在此之前需要做啥准备工作,要不然光去执行中断服务函数,那执行完如何返回呢?

下面会涉及到ARM架构的AAPCS标准,不熟悉的请看->《FreeRTOS-ARM架构与程序的本质》不然下面的分析会看的云里雾里。

1.保存中断现场

现在随便看一段简单的代码
在这里插入图片描述

下面是Add函数的汇编码
在这里插入图片描述
下面就来分析一下,假设在程序执行到Add函数中发生了中断,我们需要做些什么工作?
在这里插入图片描述
在这里插入图片描述
总结:
当CM3开始响应一个中断时,由硬件做三件事情:

  • 1.入栈: 把8个寄存器的值压入栈
    在这里插入图片描述

  • 2.取向量:从向量表中找出对应的服务程序入口地址
    在这里插入图片描述

  • 3.选择堆栈指针MSP/PSP,更新堆栈指针SP(因为入栈了8个寄存器),更新连接寄存器LR(在前面都已经将LR入栈之后,更新LR为一个特殊值用于触发中断返回),更新程序计数器PC(将服务程序入口地赋给PC寄存器准备跳转到中断服务函数中执行)

1.在RTOS中一个任务一个栈,还有一个主栈,所以所谓入栈是保存在哪个栈上?
其实很简单,当前的代码正在使用PSP(可能是某个任务的栈),则压入PSP(就压入某个任务的栈中),即使用线程堆栈;否则压入MSP指向的主堆栈。一旦进入了服务例程,就将一直使用主堆栈。

2.当然如果只是裸机的话,栈只有一个主栈,堆栈也只有一个主堆栈指针,所以压栈压栈压入的肯定是主栈。

在这里插入图片描述

2.取向量

当数据总线正在入栈操作,指令总线(I‐Code总线) 从向量表中找出正确的异常向量量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行

3.更新寄存器

入栈和取向量的工作都完毕之后,执行服务例程之前,还要更新一系列的寄存器:

  • SP:在入栈中会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程后,将由MSP负责对堆栈的访问。
  • PSR:IPSR位段(地处PSR的最低部分)会被更新为新响应的异常编号。
    在这里插入图片描述
  • PC:在向量取出完毕后,PC将指向服务例程的入口地址(跳转到中断服务函数中去执行)
  • LR:LR的用法将被重新解释,其值也被更新成一种特殊的值,称为“EXC_RETURN”,作用是触发中断返回。EXC_RETURN的二进制值除了最低4位外全为1,而其最低4位则有另外的含义(后面马上讲)

以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也伴随着更新了与之相关的若干寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。

4.中断返回

在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,这
是一个高28位全为1的值,只有[3:0]的值有特殊含义当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。

在这里插入图片描述
总结一下:
在这里插入图片描述

1.如果主程序在线程模式下运行, 并且在使用MSP时被中断,则在服务例程中LR=0xFFFF_FFF9(主程序被打断前的LR已被自动入栈)
在这里插入图片描述

2.如果主程序在线程模式下运行,并且在使用PSP时被中断,则在服务例程中LR=0xFFFF_FFFD(主程序被打断前的LR已被自动入栈)。

在这里插入图片描述
在中断嵌套时,更深层ISR所看到的LR总是0xFFFF_FFF1

当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先前的系统状态,才能使被中断的程序得以继续执行。从形式上看,有3种途径可以触发异常返回序列如下图,不管使用哪一种,都需要用到先前更新到LR中的特殊值。
在这里插入图片描述
触发中断返回之后做什么?

  1. 出栈:先前压入栈中的寄存器在这里恢复到CPU中的寄存器。内部的出栈顺序与入栈时的相对应,堆栈指针的值也改回去。
    在这里插入图片描述

  2. 更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。

5.栈的作用再总结

同几乎所有的处理器架构一样,Cortex-M处理器在运行时需要栈存储和栈指针(R13)。在栈这种存储器使用机制中,存储器的一部分可被用作后进先出的数据存储缓冲(栈就是一块特殊的内存而已)

ARM处理器将系统主存储器用于栈空间操作,且使用PUSH指令往栈中存储数据以及POP指令从栈中提取数据。每次PUSH和POP操作后,当前使用的栈指针都会自动调整。

请结合《FreeRTOS-ARM架构与程序的本质》里面的对栈的描述理解下列栈的作用。

1.当正在执行的函数使用到R4~R11寄存器来保存局部变量时,根据AAPCS标准需要将这些寄存器的值压入栈中,等函数结束时弹栈恢复寄存器。

2.往函数或子程序中信息传递(当参数数量超过4个需要使用栈来传递参数)

3.保存局部变量这个大家都知道咯

4.在中断产生时保存处理器状态(XPSR)和寄存器数值。

5.保存函数返回地址

五.ARM架构支持RTOS的特性

Cortex-M处理器在设计之初就对操作系统的支持,ARM架构实现了多个特性保证了操作系统(比如FreeRTOS)的设计的方便和高效。

  • 1.双堆栈指针:一个主堆栈指针MSP,一个进程(任务)堆栈指针PSP,MSP用于操作系统的内核,以及中断处理(使用主堆栈),PSP则用于任务栈。
  • 2.SysTick定时器:大多操作系统需要一个硬件定时器来产生操作系统需要的滴答中断,作为整个系统的时基,实现任务调度器,任务时间管理以及其它系统例行维护
  • 3.SVC和PendSV异常,这两种异常对于操作系统实现任务切换有着非常重要的作用。
  • 4.M3有两个执行等级:特权级与用户级,在应用任务中处于用户级限制了任务的访问权限(禁止访问特殊功能寄存器和 NVIC中寄存器的),还可同存储包含单元(MPU)一起使用,进一步提高操作系统的安全性。
  • 5.排他访问:排他加载和存储指令用于操作系统的信号量和互斥体操作。

低中断等待特性和指令集中的各种特性还有助于操作系统的高效运行。

1.双堆栈在系统中的运用

1.对于未使用操作系统的裸机程序的多数简单应用,对于不具有操作系统的简单应用,可以在所有操作中只使用MSP,而不用管PSP,无须修改CONTROL寄存器的数值。整个应用可以运行在特权访问等级并且只使用MS。
在这里插入图片描述
2.对于具有RTOS的系统﹐异常处理(包括部分RTOS内核)使用MSP ,而应用任务则使用PSP。每个应用任务都有自己的栈空间。RTOS中的上下文切换代码在每次上下文切换时都会更新PSP(指向下一个任务的栈)。
在这里插入图片描述

  • 在操作系统中运用双堆栈指针的好处?
    1.如果应用任务的错误导致栈破坏的,而破坏的是任务自己的栈而操作系统使用和其他任务的栈均不会被破坏
    2.每个任务有自己的栈,任务栈的大小容易估计,用于中断和嵌套中断处理的栈空间会被分配到主栈(因为中断必须使用主堆栈指针MSP)

其实很简单当前堆栈指针只能使用一个,SP(MSP,PSP)指针指向哪里就往哪里入栈,而MSP一直是指向主堆栈,PSP是指向某个任务的堆栈,所以切换任务的时候需要将PSP指向下一个任务的栈。

  • 当操作系统从线程模式启动时,可以利用SVC异常来启动第一个任务,进入SVC异常创建任务栈中的栈帧,且触发使用PSP的异常返回,当异常返回时任务启动。
    在这里插入图片描述
    在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常可由SysTick 异常触发。在上下文切换操作中需要:
    1.其他8个寄存器由硬件帮我们保存在栈里,寄存器R4~R11由我们自己保存在栈中,而这个所谓的栈就是当前任务的栈。
    在这里插入图片描述
    2.保存当前PSP数值(保存当前任务的栈的位置,等下一次切换到它的时候方便找到该任务的栈)
    3.将PSP设置为下一个任务的上一次SP数值(将PSP指向下一个任务的栈)
    4.恢复下一个任务的上一次保存在栈里的R4~R11
    5.利用异常返回切换任务,硬件自动恢复上次保存在栈中的8个寄存器

这样就完成了一个任务切换,下一个任务(在上一次打断的位置)继续运行因为硬件保存的8个寄存器中有一个PC寄存器记录这任务的上一次被打断的位置也就是返回地址。

下图为一个简单的上下文切换操作。
在这里插入图片描述

2.SVC异常

SVC(请求管理调用)和PendSV(可挂起的系统调用)异常对于OS(操作系统)设计非常重要。SVC的异常类型为11,且优先级可编程。

  • 为什么操作系统需要SVC异常?

1.对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,而且有些硬件资源可被设置为只支持特权访问(利用MPU),应用任务只能通过OS的服务访问这些受保护的硬件资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌人式系统会更加健壮和安全。
操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。

2.它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。

3.通过 SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。

4.开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用 SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致,各封皮函数会正确使用 SVC
指令来执行系统调用)

在这里插入图片描述
SVC异常由SVC指令产生,该指令需要一个立即数,这也是参数传递的一-种方式。SVC异常处理可以提取出参数并确定它需要执行的动作。例如:
在这里插入图片描述
在执行SVC处理时,可以在读取压栈的程序计数器(PC)数值后从该地址读出指令并屏蔽掉不需要的位,以确定SVC指令中的立即数。执行SVC的程序可以使用主栈也可以使用进程栈,因此在提取压栈的PC数值前,需要确定压栈过程使用的是哪个栈,此时可以查看进人异常处理时链接寄存器的数值。
在这里插入图片描述

在这里插入图片描述

3.PendSV异常

PendSV(可挂起的系统调用)异常对OS操作也非常重要,可以写人中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV异常,与SVC异常不同,它是不精确的。因此它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行

利用该特性,若将PendSV设置为最低的异常优先级(任何中断都可以打断),可以让PendSV异常处理在所有其他中断处理任务完成后执行,这对于上下文切换非常有用,也是各种OS设计中的关键。

  • 为什么不直接在SysTick中切换任务?

首先来看一下上下文切换的几个基本概念。在具有嵌人式OS的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行。

在这里插入图片描述
上图OS内核的执行由SysTick异常触发,每次它都会决定切换到一个不同的任务。

  • 直接使用SysTick切换任务带来的问题?

1.会导致其他中断(紧急的实物)延迟处理。
若中断请求(IRQ)在SysTick异常前产生,则SysTick异常可能会抢占IRQ处理。在这种情况下,如果去切换任务(会占用一定时间),则IRQ(紧急的中断事物)处理就会被延迟。

  • 不是说SysTick的优先级被设置成最低嘛,为什么SysTick抢占其他中断?
    因为中断优先级分组同样适用于内核中断,当内核中断与外部中断同时优先级设置成最低,则内核中断编号更低,(相同优先级情况下比中断编号),所以说就算SysTick优先级设置成最低同样比某些外部中断的优先级要高。

2.在SysTick中断内部取切换任务,又恰好是打断了其他的中断,则SysTick中断会尝试切换为线程模式,但是存在活跃中断服务,则使用错误异常会被触发。
在这里插入图片描述
为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比
较接近时,会发生“共振”(迟迟切换不了任务)。

而PendSV 完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它所有的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级(这个最低才是真正的最低)的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

在这里插入图片描述

1. 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
2. OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
3. 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
4. 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
5. 发生了一个中断,并且中断服务程序开始执行
6. 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
7. OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
8. 当 SysTick 退出后,回到先前被抢占的 ISR 中,ISR 继续执行
9. ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换
10. 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。

4.切换任务的过程

上下文切换操作由PendSV 异常处理执行,由于异常流程已经保存了寄存器R0~ R3、R12、LR.返回地址(PC)以及xPSR(一共8个寄存器由硬件帮我们保存任务栈中),PendSV只需将R4~R11保存到任务栈。
在这里插入图片描述
大概流程:
在这里插入图片描述

六.总结

本文基本是参考Cortex-M3权威指南(中文)结合自身理解而写,尽管这样还是写的稍显艰难,由于ARM架构的知识太过庞大,有些内容还是不够深入,不过要学习一个FreeRTOS应该八九不离十,随着学习的深入我会不断完善本文内容,如有错误敬请指正,还是那句话基础不牢地动山摇!!

  • 46
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rivencode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值