目录

一、概述

二、影子栈指针

三、SVC异常

四、PendSV异常

五、实际的上下文切换

六、排他访问

七、总结

一、概述

        Cortex-M处理器在设计之初就考虑对OS的支持,处理器架构具有多个特性,如影子栈指针、SysTick定时器、SVC异常、PendSV异常、排他访问等。

影子栈指针:在OS中,MSP(主栈)用于内核中断处理,而PSP(线程栈)应用于任务中。

SysTick定时器:该定时器用于任务管理和上下文切换,处理器也可以在不同的时间片内处理不同的任务。

SVC和PendSV异常:这两个在OS中起到非常关键的作用,用于上下文切换的实现等。

排他访问:排他加载和存储指令用于OS中的信号量和互斥体的操作。

二、影子栈指针

        在Cortex-M处理器中,存在两个指针,分别是MSP(主栈指针)和PSP(进程栈指针),可以通过CONTROL的bit[1]设置线程模式。在开发过程中如果使用操作系统,那么异常处理(包括OS内核)使用MSP(主栈)。而应用任务则使用PSP栈,每个任务都有属于自己的栈空间,每次OS切换上下文的时候,都会更新PSP指向的任务栈。这样在设计的过程中会有几个优点。

        当某个任务的栈被破坏掉,OS内核使用的栈和其他任务的栈不会受到影响,因此可以提高系统的可靠性。

        在分配任务栈的过程中不需要考虑ISR和嵌套中断处理的栈空间问题,因为这些属于异常处理内核部分,使用MSP进行操作,会被分配到主栈中。

        在不同任务之间的切换,一般被称为上下文切换(在文章任务切换的本质有源码分析),其通常在PendSV异常处理中执行,该异常可由SysTick异常触发。在上下文切换操作需要:

(1)将寄存器的当前状态保存到当前栈中。

(2)保存当前PSP数值。

(3)将PSP设置为下一个任务的上一次SP数值。

(4)恢复下一个任务的上一次的寄存器。

(5)利用异常返回切换任务。

三、SVC异常

        SVC(请求管理调用)和PendSV(可挂起的系统调用)异常对于OS设计非常重要。SVC处理必须出现在SVC指令后执行,除非同一个时间有更高优先级的中断出现。应用任务只能运行在非特权访问等级,如果任务需要对受保护的硬件资源进行访问,就可以通过触发SVC异常的方式进入特权访问等级,该指令需要一个立即数即可。所以在启动第一个任务的章节中,我们可以看到源码使用SVC 0 这样的立即数,来达到进入内核的目的,启动第一个任务。

四、PendSV异常

        PendSV(可挂起的系统调用)异常对于OS操作系统非常重要,其异常编号为14且具有可编程的优先级,可以写入中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV异常。与SVC异常不同,它的挂起状态可在更高优先级的异常处理内设置,且会在更高优先级处理完成后执行,利用此特性可以将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理任务完成后执行。如下图是一个上下文切换的简单示例。

        OS内核执行条件触发:应用任务中SVC指令的执行。如应用任务需要等待时间或者数据,则可以调用系统服务切换下一个任务。同时在OS代码中,任务调度器可以决定是否应该执行上下文切换,由SysTick异常触发,每次它都会决定切换到下一个不同的任务。基于这个条件,我们来看一个非常有一个的事件。

        如中断请求(IRQ)在SysTick异常前产生,则SysTick异常可能会抢占IRQ处理。在这种情况下,OS不应该执行上下文切换,否则,ISR处理就会被延时。这个时候,我们就知道为什么要把PendSV异常优先级设置最低,并且基于它的可挂起特性,我们就会在SysTick异常中挂起PendSV异常,这样退出SysTick异常的时候,ISR优先级高于PendSV异常,当ISR中断程序执行完成后,便会通过PendSV异常进行上下文切换,这样就不会延时ISR中断程序。根据下图来看一下具体流程。

(1)A任务调用SVC进行任务切换。

(2)OS收到请求,准备进行上下文切换,且挂起PendSV异常。

(3)当CPU退出SVC时,会立即进入PendSV且进行上下文切换。

(4)当PendAV完成并返回线程时,OS会执行任务B,这就切换完成了。

(5)中断产生且进入中断处理。

(6)在运行中断处理程序时,SysTick异常产生了(OS节拍)。

(7)OS执行重要操作,然后挂起PendSV异常并准备进行上下文切换。

(8)当SysTick异常退出时,会返回到中断服务程序。

(9)当中断服务程序结束后,PendSV异常开始执行实际的上下文切换操作。

(10)当PendSV完成后,程序返回到线程等级模式,这次就会回到A并继续执行了。

        PendSV异常也可以应用于不是OS环境下的操作,比如有多个ISR中断,高优先级的ISR中断需要处理的实践过程,这样就延时了别的低优先级ISR中断,此时就可以用到PendSV异常来分担高优先级的ISR次要工作。将关键的工作交给高优先级的ISR来处理,而剩余所需的处理工作交给PendSV异常来处理,这样既不在高优先级的中断中耽误时间,又不影响处理。

五、实际的上下文切换

        关于更详细的源码部分请阅读“任务切换的本质”这篇博客,上下文切换操作实际由PendSV异常处理执行,由于异常流程已经保存寄存器R0 ~ R3、R12、LR、返回地址以及xPSR,PendSV只需要将R4 ~ R11保存到进程栈中即可,如下图是不具备浮点数操作的单片机所保存的相关栈数据。

        如果使用支持操作浮点寄存器的单片机来说,就需要增加上下文切换流程中的压栈寄存器数量了,同时也需要操作EXC_RETURN寄存器的第四位表示异常栈帧是否包含浮点寄存器。如下图是具备浮点寄存器的任务切换流程。

六、排他访问

        排他访问,排斥他的访问,在多线程中,一个线程访问一个资源后,就不可被其他资源再次访问,自然需要进行排他访问操作。因此,允许任务将资源“锁定”,并在不需要的时候将其“释放”掉。基于这个特性,一般被称作“信号量”或“互斥锁”。如下图是一个计数型信号量的举例,任务ABCD分别占用四个通道后无空余通道,任务E无法锁定,当其中任意一条任务释放时,即可被任务E锁定。

        计数变量的减小并非原子性的,这是因为操作往往需要三条指令:读 --> 减 -->写 ,如果在读写操作之间产生了上下文切换,就会出现如下图的问题。

        最简单的方法就是在处理信号量时禁止上下文切换,不过这样的弊端是会加大中断等待时间,而且只能用于单处理器设计。两个运行在不同处理器上的任务可能会同时尝试减小信号量变量,这是个问题。

        为使信号量可以在单独处理器和多处理器环境中都能工作,Cortex - M3和Cortex - M4处理器支持一种名为排他访问的特性。处理器内部由一个名为本地监控的小的硬件单元。一般情况下,它处于开放访问状态。在执行完排他加载指令后,它就会切换到排他访问状态。只有在本地监控处于排他访问状态且总线系统未响应排他失败时排他存储才能执行。满足如下任何一条执行,排他读写会失败。

(1)执行了CLREX指令,本地监控切换到开放访问状态。

(2)产生了上下文切换。

(3)前面没有执行LDREX。

(4)外部硬件通过总线接口上的边带信号向处理器返回排他访问状态。

        若排他存储得到一个失败状态,则存储器中不会产生实际的写操作,这是因为它会被处理器内核或外部硬件阻止。内核内置一个排他标识寄存器,通过硬件监控内存访问。当执行LDREX时,标识置位;若其他任务或中断修改了内存区域,标识自动清零,确保后续STREX操作失败。

同步原语指令介绍。

(1)LDREX(加载排他指令):从内存加载数据,并标记该地址为“排他访问”状态。

(2)STREX(存储排他指令):尝试将数据写回内存。若该地址未被其他任务修改(即排他状态仍有效),则写入成功并返回0;否则返回1,需重新执行操作。

(3)CLREX:清除排他状态标识。

排他访问的实现步骤

(1)加载数据:使用LDREX指令读取共享资源到寄存器,并激活排他监控器。

(2)修改数据:在寄存器中完成数据修改。

(3)尝试存储:使用STREX指令尝试将修改后的数据写回内存。若返回值为0(成功),操作完成则进入下一步,否则回到第一步重新执行。

(4)循环检测:通过循环结构实现重试机制,直至操作成功。

七、总结

        本文介绍了OS的特性,使我们对于OS内核操作理解的更深刻。PendSV的挂起调用方式对于中断的处理会变得很有意思。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐