✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
个人主页:@rivencode的个人主页
系列专栏:玩转FreeRTOS
推荐一款模拟面试、刷题神器,从基础到大厂面试题点击跳转刷题网站进行注册学习
由于FreeRTOS操作系统所涉及的ARM架构的知识较多,而且这是知识对理解FreeRTOS的本质和底层实现至关重要,仿佛ARM架构是为操作系统量身定制一般,所以ARM架构的知识的重要性我就不说了,本篇文章主要是对操作系统底层实现所用到ARM架构的知识进行汇总,所以本篇文章参考《Cortex-M3权威指南》,也是对我上篇文章《FreeRTOS-ARM架构与程序的本质》的补充。
CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通
用目的”的,特殊功能寄存器有预定义的功能,而且必须通过专用的指
令来访问。
R0 - R3 : 子程序传递参数(函数传参保存参数)
R4 - R11:子程序保存局部变量
R12:子程序间的scratch寄存器,记为IP 暂存寄存器
R13:堆栈指针(别名SP):栈指针寄存器。在进入子程序时,和退出子程序时,值必须相等。(意思是调用函数申请栈帧(供函数使用的一段内存)退出函数释放栈帧)
在 CM3 处理器内核中共有两个堆栈指针,于是也就支持两个堆栈。 当引用 R13(或写作 SP)时,你引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问(MRS,MSR 指令)。这两个堆栈指针分别是:
需要注意:
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 异常。
除了寄存器组中的寄存器外,处理器中还存在多个特殊寄存器,这些寄存器表示处理器状态、定义了操作状态和中断/异常屏蔽。
在使用C等高级编程语言开发简单的应用时,需要访问这些寄存器的情形不多。在操作系统或需要高级中断屏蔽特性
时这些特殊寄功能寄存器就派上用场了。
Cortex‐M3 中的特殊功能寄存器包括:
它们只能被专用的 MSR 和 MRS(特殊)指令访问,而且它们也没有存储器地址。
程序状态寄存器在其内部又被分为三个子状态寄存器:
通过 MRS/MSR 指令,这 3 个 PSRs 即可以单独访问,也可以组合访问(2 个组合,3 个组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”
1.EPSR寄存器中24位为1表示thumb状态,清零会触发系统异常。
2.IPSR寄存器的0~8位用来表示正在执行的中断服务程序的中断号。
PRIMASK,FAUITMASK和 BASEPRI寄存器都用于异常或中断屏蔽这些特殊寄存器可基于优先等级屏蔽异常,只有在特权访问等级(什么是特权模式后面会讲解)对它们进行操作(非特权状态下的写操作会被忽略,而读出则会返回0)。它们默认全部为0,也就是屏蔽(禁止异常/中断)不起作用。
下面这个图一定要认真看完:
上面提到MNI异常还有硬件中断是中断向量表中优先级比较高的中断,而NMI是不可屏蔽中断。
NMI中断:不可屏蔽中断,产生这个中断的时候,表示系统发生了致命的错误,通知CPU发生了灾难性事件,如电源掉电、总线奇偶位出错等所以一定不屏蔽。
关于更多中断内容请参考:《中断-NVIC与EXTI外设详解(超全面)》
在许多应用中,可能都需要暂时禁止所有中断以执行一些时序关键的任务,此时可以使用PRIMASK寄存器。PRIMASK寄存器只能在特权状态访问。
PRIMASK用于禁止除NMI和 HardFault外的所有异常,它实际上是将当前优先级改为0(最高的可编程等级)。
如何访问PRIMASK寄存器?
1.如用C编程,可以使用CMSIS-Core提供的函数来设置和清除PRIMASK:
2.对于汇编编程,可以利用CPS(修改处理器状态)指令修改PRIMASK寄存器的数值。
3.PRIMASK寄存器还可通过MRS和 MSR指令访问。
注意:虽然通过往PRIMASK寄存器写 1 屏蔽了中断,但是中断依然会被挂起,只不过得不到执行,当PRIMASK清零时被挂起的中断会立即指向,PRIMASK 只是屏蔽掉中断,而并不是不让中断源产生中断。
从行为来说,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,因此,可以强制让高优先级处理在低优先级处理结束后开始执行。
有些情况下,可能只想禁止优先级低于某特定等级的中断,此时,就可以使用BASEPRI寄存器。要实现这个目的,只需简单地将所需的屏蔽优先级写入BASEPRI寄存器。例如,若要屏蔽优先级小于等于0x60的所有异常,则可以将这个数值写入BASEPRI:
对与操作系统而言,PRIMASK 和 BASEPRI 对于暂时关闭中断是非常重要的。而FAULTMASK 则可以被操作系统 用于暂时关闭 fault 处理机能,这种处理在某个任务崩溃时可能需要。因为在任务崩溃时,常常伴随着一大堆 faults。在系统料理“后事”时,通常不再需要响应这些 fault——人死帐清。
1.在 Cortex‐M3 的 handler 模式中,CONTROL[1]总是 0。在线程模式中则可以为 0 或 1。(在线程模式可以使用MSP也可以使用PSP,而handler 模式(中断模式)只能使用MSP)
2.==仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。==改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的第2位,也能实现模式切换(在后面中断的处理的过程会详细讲)。
仅当在特权级下操作时才允许写该位。一旦进入了用户级,唯一返回特权级的途径,就是触发一个(软)中断(因为中断是特权级),再由服务例程改写该位。
CONTROL 寄存器也是通过 MRS 和 MSR 指令来操作的:
Cortex‐M3 支持 2 个模式和两个特权等级。
上图的意思是:在 CM3 运行主应用程序时(线程模式),既可以使用特权级,也可以使用用户级;但是异常服务例程必须在特权级下执行,如下图所示。
处理器在特权模式与用户模式有何区别?
所以特权级和用户级的区别体现在对访问内核寄存器的限制。
正常情况下,系统复位后,处理器处于特权级+线程模式(因为系统复位后,一般都需要先配置内核寄存器)
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。
用户级下的代码不能再试图修改 CONTROL[0]来回到特权级(因为CONTROI寄存器只能在特权访问等级进行修改操作,而读取操作则在特权和非特权访问等级都可以)。则它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级。
若使用嵌入式操作系统,每次上下文切换时都可以重新编程CONTROL寄存器,以满足应用间不同的特权访问等级需要。
当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式(由线程模式-handler模式-线程模式)的转换
为什么需要两个模式?
引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码(中断服务函数的代码)。
为什么要分特权级与用户级?
把代码按特权级和用户极分开对待,有利于使架构更加安全和健壮。例如,当某个用户代码出问题时,不会让它成为害群之马,因为用户级的代码是禁止写特殊功能寄存器和 NVIC中寄存器的。另外,如果还配有 MPU,保护力度就更大,甚至可以阻止用户代码访问不属于它的内存区域。
CM3 的堆栈是分为两个:主堆栈和进程堆栈
在 Cortex‐M3 的 handler 模式中,CONTROL[1]总是 0。在线程模式中则可以为 0 或 1。==仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。==改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的第二位。
1.对于未使用操作系统的裸机程序的多数简单应用,对于不具有操作系统的简单应用,可以在所有操作中只使用MSP,而不用管PSP,无须修改CONTROL寄存器的数值。整个应用可以运行在特权访问等级并且只使用MS。
2.对于具有RTOS的系统﹐异常处理(包括部分RTOS内核)使用MSP ,而应用任务则使用PSP。每个应用任务都有自己的栈空间。RTOS中的上下文切换代码在每次切换任务时都会更新PSP(指向下一个任务的栈)。
为了避免系统堆栈因应用程序的错误使用而毁坏,你可以给应用程序(也就是操作系统中任务,一个任务一个栈)专门配一个堆栈,不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用 PSP,而异常服务例程则使用 MSP。这两个堆栈指针的切换是全自动的,就在出入异常服务例程时由硬件处理。
通过读取 PSP 的值,操作系统就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(到底在切换任务时要保存那些寄存器以及中断整个流程是我们后面需要啃的硬骨头,先明白基本的ARM架构的知识)。操作系统还可以修改 PSP,用于实现多任务中的任务上下文切换
在FreeRTOS的内核实现中,会有关于汇编指令的运用来实现某些寄存器的操作,对与汇编我们不需要掌握太深,只需了解一些基本的语法,和一些常用的汇编指令,而且也不需要去特别记忆知道是什么意思就好,那些不常见的汇编指令等碰见了在查就好了。
本文就简单介绍一些常用的汇编指令和一些基本语法。
ARM处理器一直支持两种形式上相对独立的指令集,它们分别是
而Thumb‐2是16位Thumb指令集的一个超集,在Thumb‐2中,16位指令首次与32位指令并存,结果在Thumb状态下可以做的事情一下子丰富了许多,同样工作需要的指令周期数也明显下降。
在过去,做 ARM 开发必须处理好两个状态。这两个状态是井水不犯河水的:32 位的ARM 状态和 16 位的 Thumb 状态。
所以ARM指令集与thumb指令集各有优缺点,为了取长补短,很多应用程序都混合使用 ARM 和 Thumb 代码段。然而,这种混合使用是有额外开销的,时间上的和空间上的都有,主要发生在状态切换之时。另一方面,ARM 代码和 Thumb 代码需要以不同的方式编译,这也增加了软件开发管理的复杂度。
Cortex‐M3 内核干脆都不支持 ARM 指令,中断也在 Thumb 态下处
理(以前的 ARM 总是在 ARM 状态下处理所有的中断和异常),而新起之秀Thumb‐2 指令集,因为它同时兼容 32 位指令和 16 位指令,既可以节省空间,处理性能又强,而且易于使用。
而我们的Cortex‐M3 只支持Thumb‐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 位指令。
处理器的基本功能之一就是数据传送。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)
在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 伺候
这两条指令是访问特殊功能寄存器的“绿色通道”,但是必须在特权级下,除 APSR 外。指令语法如下:
Sreg:特殊寄存器
下面给出一个指定 PSP 进行更新的例子:
终于到了ROTS任务调度的关键之一,要想理解FreeRTOS任务调度器的实现,必须清楚中断的具体行为。
首先我们知道中断发生之后需要去调用中断服务函数,但在此之前需要做啥准备工作,要不然光去执行中断服务函数,那执行完如何返回呢?
下面会涉及到ARM架构的AAPCS标准,不熟悉的请看->《FreeRTOS-ARM架构与程序的本质》不然下面的分析会看的云里雾里。
下面是Add函数的汇编码
下面就来分析一下,假设在程序执行到Add函数中发生了中断,我们需要做些什么工作?
总结:
当CM3开始响应一个中断时,由硬件做三件事情:
3.选择堆栈指针MSP/PSP,更新堆栈指针SP(因为入栈了8个寄存器),更新连接寄存器LR(在前面都已经将LR入栈之后,更新LR为一个特殊值用于触发中断返回),更新程序计数器PC(将服务程序入口地赋给PC寄存器准备跳转到中断服务函数中执行)
1.在RTOS中一个任务一个栈,还有一个主栈,所以所谓入栈是保存在哪个栈上?
其实很简单,当前的代码正在使用PSP(可能是某个任务的栈),则压入PSP(就压入某个任务的栈中),即使用线程堆栈;否则压入MSP指向的主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
2.当然如果只是裸机的话,栈只有一个主栈,堆栈也只有一个主堆栈指针,所以压栈压栈压入的肯定是主栈。
当数据总线正在入栈操作,指令总线(I‐Code总线) 从向量表中找出正确的异常向量量,然后在服务程序的入口处预取指。由此可以看到各自都有专用总线的好处:入栈与取指这两个工作能同时进行
在入栈和取向量的工作都完毕之后
,执行服务例程之前,还要更新一系列的寄存器:
以上是在响应异常时通用寄存器的变化。另一方面,在NVIC中,也伴随着更新了与之相关的若干寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。
在进入异常服务程序后,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中的特殊值。
触发中断返回之后做什么?
更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
同几乎所有的处理器架构一样,Cortex-M处理器在运行时需要栈存储和栈指针(R13)。在栈这种存储器使用机制中,存储器的一部分可被用作后进先出的数据存储缓冲(栈就是一块特殊的内存而已)。
ARM处理器将系统主存储器用于栈空间操作,且使用PUSH指令往栈中存储数据以及POP指令从栈中提取数据。每次PUSH和POP操作后,当前使用的栈指针都会自动调整。
请结合《FreeRTOS-ARM架构与程序的本质》里面的对栈的描述理解下列栈的作用。
1.当正在执行的函数使用到R4~R11寄存器来保存局部变量时,根据AAPCS标准需要将这些寄存器的值压入栈中,等函数结束时弹栈恢复寄存器。
2.往函数或子程序中信息传递(当参数数量超过4个需要使用栈来传递参数)
3.保存局部变量这个大家都知道咯
4.在中断产生时保存处理器状态(XPSR)和寄存器数值。
5.保存函数返回地址
Cortex-M处理器在设计之初就对操作系统的支持,ARM架构实现了多个特性保证了操作系统(比如FreeRTOS)的设计的方便和高效。
低中断等待特性和指令集中的各种特性还有助于操作系统的高效运行。
1.对于未使用操作系统的裸机程序的多数简单应用,对于不具有操作系统的简单应用,可以在所有操作中只使用MSP,而不用管PSP,无须修改CONTROL寄存器的数值。整个应用可以运行在特权访问等级并且只使用MS。
2.对于具有RTOS的系统﹐异常处理(包括部分RTOS内核)使用MSP ,而应用任务则使用PSP。每个应用任务都有自己的栈空间。RTOS中的上下文切换代码在每次上下文切换时都会更新PSP(指向下一个任务的栈)。
其实很简单当前堆栈指针只能使用一个,SP(MSP,PSP)指针指向哪里就往哪里入栈,而MSP一直是指向主堆栈,PSP是指向某个任务的堆栈,所以切换任务的时候需要将PSP指向下一个任务的栈。
这样就完成了一个任务切换,下一个任务(在上一次打断的位置)继续运行因为硬件保存的8个寄存器中有一个PC寄存器记录这任务的上一次被打断的位置也就是返回地址。
SVC(请求管理调用)和PendSV(可挂起的系统调用)异常对于OS(操作系统)设计非常重要。SVC的异常类型为11,且优先级可编程。
1.对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,而且有些硬件资源可被设置为只支持特权访问(利用MPU),应用任务只能通过OS的服务访问这些受保护的硬件资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌人式系统会更加健壮和安全。
(操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。)
2.它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。
3.通过 SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。
4.开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用 SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致,各封皮函数会正确使用 SVC
指令来执行系统调用)
SVC异常由SVC指令产生,该指令需要一个立即数,这也是参数传递的一-种方式。SVC异常处理可以提取出参数并确定它需要执行的动作。例如:
在执行SVC处理时,可以在读取压栈的程序计数器(PC)数值后从该地址读出指令并屏蔽掉不需要的位,以确定SVC指令中的立即数。执行SVC的程序可以使用主栈也可以使用进程栈,因此在提取压栈的PC数值前,需要确定压栈过程使用的是哪个栈,此时可以查看进人异常处理时链接寄存器的数值。
PendSV(可挂起的系统调用)异常对OS操作也非常重要,可以写人中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV异常,与SVC异常不同,它是不精确的。因此它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
利用该特性,若将PendSV设置为最低的异常优先级(任何中断都可以打断),可以让PendSV异常处理在所有其他中断处理任务完成后执行
,这对于上下文切换非常有用,也是各种OS设计中的关键。
首先来看一下上下文切换的几个基本概念。在具有嵌人式OS的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行。
上图OS内核的执行由SysTick异常触发,每次它都会决定切换到一个不同的任务。
1.会导致其他中断(紧急的实物)延迟处理。
若中断请求(IRQ)在SysTick异常前产生,则SysTick异常可能会抢占IRQ处理。在这种情况下,如果去切换任务(会占用一定时间),则IRQ(紧急的中断事物)处理就会被延迟。
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,同时系统再次进入线程模式。
上下文切换操作由PendSV 异常处理执行,由于异常流程已经保存了寄存器R0~ R3、R12、LR.返回地址(PC)以及xPSR(一共8个寄存器由硬件帮我们保存任务栈中),PendSV只需将R4~R11保存到任务栈。
大概流程:
本文基本是参考Cortex-M3权威指南(中文)结合自身理解而写,尽管这样还是写的稍显艰难,由于ARM架构的知识太过庞大,有些内容还是不够深入,不过要学习一个FreeRTOS应该八九不离十,随着学习的深入我会不断完善本文内容,如有错误敬请指正,还是那句话基础不牢地动山摇!!
结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题
点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)