软件开发技能:嵌入式系统优先级详解

嵌入式ARM 2020-08-04
在嵌入式软件开发中,我们不可避免的需要接触优先级的概念,掌握优先级的概念对于设计一个好的软件系统尤为重要。
本篇笔记的主要内容有以下几个方面:
1、中断优先级
2、操作系统中的任务优先级
3、同等优先级处理
4、中断嵌套

今天鱼鹰借助大家熟悉的 STM32F103 平台和各位聊聊其中的密事。
首先,我们从一个裸机系统的变量自加开始说起。
这里有三个变量,A、B、C,其中 B 变量除了在 main 函数中自加外,还会在中断处理函数中进行自加。
这里面考虑了B的两种情况,一是先执行 main  中的自加再执行中断的自加,二是先执行中断的自加再进行 main 的自加。
不管发生哪种情况,当程序执行到 C 位置时,B 的值都是一样的。
当然,以上分析是从 C 语言的角度进行分析的,如果以汇编的视角进行分析(自加操作在汇编中分为三个步骤),你会发现,这里面其实还有第三种情况:
这里面的 B’ 可以认为是寄存器,即变量的 B 的副本。
正因为副本的存在,在main 函数的写入过程中导致丢失了中断中 B 的自加操作。
对于程序而言,就好像根本没有进入中断一样!
这就是全局变量的使用隐患。
但是善于分析的道友可能会提出这样一个疑问,为什么上面只考虑了main 函数中的B++被打断的可能,却没有考虑中断(橙色部分)的B++被打断的可能,是鱼鹰忽略了吗?
不不,其实这里面就涉及到了今天的主题,优先级
在裸机系统中,中断优先级高于main函数的处理,也就是说,一且中断来临, 不管main函数执行到哪个位置,都会优先处理中断程序,只有中断程序执行完成后,才会继续执行main函数,所以中断的 B++ 不可能被main 函数打断!

这里插两个问题:
怎么进入中断函数的?
当中断请求(中断请求可以认为是种电平信号,在寄存器中就表现为某一位的标志位)来临时,硬件负责把部分寄存器存储到栈(一种特殊的数据结构)中,这里面就包含了PC寄存器(用于指示下一条指令执行的位置),之后从向量表中找到中断处理函数的入口地址,开始进入中断处理函数中执行。
怎么回到原来的位置?
因为在进入中断前已经保存了PC等其他寄存器的值,所以只要在执行完中断处理函数后,将之前的保存到栈中的值恢复回来,那么CPU就可以继续从被打断的指令开始继续执行。
更多相关的中断行为请查看《权威指南》,鱼鹰不再细说。

回到刚才的优先级话题,正因为中断的执行优先级比main高,所以中断中的 B++ 不会被打断,这也是为什么有些时候,我们不需要对中断中的变量进行临界保护的原因所在。

在 Cortex-M3 内核中,中断分为可屏蔽不可屏蔽中断,同时又有可编程优先级不可编程优先级之分。
所谓可屏蔽,就是说这个中断是可以屏蔽掉的,即使发生了中断,也不会让CPU执行中断里面的程序。
比如我们的定时器中断,如果我们没有开启相应的中断的话,即使定时器溢出中断来了,那也不会进入中断处理函数处理的。
不可屏蔽,就是说这个中断是不可以屏蔽的,比如复位中断(是不是不可思议,代码执行的第一条指令竟然是中断处理函数里面的),如果复位中断都被屏蔽了,那么系统也就别想运行了。
可编程 ,意味着这个中断的优先级可以由软件修改(不可编程,即优先级固定死了,不能修改)。
中断的优先级在设置时又有抢占式优先级非抢占式优先级两种设置(根据单片机不同,抢占式和非抢占式可设置的位数不同,并且可以分配各自的位数,即所谓的中断分组,如STM32F103 共有四位,通过设置中断分组来决定抢占式和非抢占的位数)。
抢占优先级,即如果中断 1 的优先级比中断 2 的抢占优先级高的话,一旦中断1发出中断请求,即使已经在中断 2 执行了,也会强制进入中断 1 执行,这个类似于 main 函数与中断的关系,只不过这里两个都是中断而已。
在抢占优先级相同情况下,非抢占优先级就会开始起作用了。
如果中断 1 和中断 2 的抢占优先级设置成一样,而非抢占优先级不一样,此时如果两个中断同时发出请求,那么优先处理非抢占优先级高的中断。
但是如果不是同时发生呢?那么就会依次处理中断请求,在其中一个中断处理过程中,是不可以被另一个中断打断的,同时如果本中断再来一个请求,也不会重新进入中断函数处理。
中断本身不可打断自身的处理,换句话说中断不会执行到一半时又因为自身新的中断请求来临而重新再次进入本中断处理函数执行。

如果抢占优先级和非抢占优先级都设置成一样呢,此时如果两个中断同时发生,又该选择哪个先执行,随机吗?
这里就涉及到硬件优先级了。
在上图中,每一个中断其实都是有固定的默认优先级的,这个优先级肯定不同,所以当抢占优先级和非抢占优先级一样的情况下,在中断同时发生时,先执行默认优先级高的。
看图:
讲到了中断,就不可不说如何禁止中断的问题了。
在常规操作中,我们会使用禁止全局中断来禁止中断的处理,一旦禁止了全局中断,那么除了不可屏蔽中断外,所有的中断都会被屏蔽掉,即如果在禁止中断后发生了中断,也不会再执行。
但是一旦中断打开了,那么之前被屏蔽的中断就会立刻开始执行(有一个中断挂起位,代表中断的发生,只有CPU执行了中断处理函数,并清零相应标志位,该挂起位才会清除)。
如果在关闭中断的过程中发送了两次中断,比如外部中断发生了两次,那么在开启中断后,也只会响应一次中断,因为挂起位就只有那么一位(不像队列一样可以保留多个标志位)。

对于一般功能而言,禁止全局中断确实有用,对于保护全局变量也非常有效,但是对于整个系统而言会有一定的影响。
如果禁止中断的时间很短,那么确实无关紧要,但是一旦需要禁止较长的时间(毫秒级别),对于那些需要及时处理的中断而言,就是一个不可忽视的延迟。
而在操作系统中,为了保护那些全局变量,禁止中断的操作时有发生,那么是否有一种方法可以 屏蔽部分中断 ,而让高优先级的中断不被屏蔽呢?
有的,在 Cor tex-M3 内核中,有一个寄存器专门干这事,即 BASEPRI。
当设置该寄存器时,将屏蔽所有优先级不高于某个具体值的中断。
比如设置该寄存器为 3,那么优先级0~ 2的中断不会被屏蔽。
所以在操作系统中,我们可以修改禁止中断的代码,使其不会屏蔽高优先级的中断,对于高优先级中断来说,可增加实时性。
uCOS II 中默认是直接全局禁止中断的(可以修改它),但是 FreeRTOS 是可以禁止部分中断的,使用的就是上述寄存器,当然这个功能需要单片机本身支持才行。

以上就是中断优先级的内容,如果只会裸机的话,那么以上内容就差不多了,但是如果是操作系统,那么需要再增加一个任务优先级的概念。
所谓任务,你也可以认为是一种中断,只不过,这种特殊的中断优先级低于所有的硬件触发的中断。

中断的优先级凌驾于所有任务之上。
也就是说,一旦中断来临,不管CPU正在执行哪个任务,在全局中断开启的情况下,都会立刻执行中断里的程序。
在中断中,可以进行中断嵌套,所谓的中断嵌套即当前中断被另一个更高优先级的中断所打断(即抢占),被打断的中断必须在高优先级任务执行完成后才会继续执行。而在嵌入式实时操作系统中,为了更好的处理实时任务,一般而言也会设置成可抢占的任务(亦称可剥夺)。
中断的优先级处理是由内核进行管理的,这里的内核是指单片机内核,比如STM32F103的内核是Cortex-M3(更准确的说是由 NVIC 管理)。
一旦设置好相应的寄存器之后,只要中断来了,那么就会自动处理中断程序,这些工作由硬件完成,它会在多个中断同时来临时选择最高的优先处理;也会在中断执行时,如果有一个更高优先级的中断来临时,打断当前中断的执行而先执行更高优先级的中断。
但是操作系统是纯软件行为,那么操作系统的任务优先级又是谁管理的?又是如何管理的呢?
答案就在Systick中断。
既然要管理所有任务的优先级,即在合适时选择运行优先级最高的任务,那么操作系统本身必然需要有能剥夺所有任务执行的能力,而中断是凌驾于任务之上的,可以在任何时候剥夺任务的执行,从而获得CPU的使用权,所以选择中断作为操作系统的核心是合适的。

但是中断那么多,选择什么中断比较合适呢?没有比 Systick 中断更合适的了,因为它就是为此而生的。
Systick说白了就是一个定时器,但是和普通定时器不同的是,功能比较单一,就是一个计数器而已,所以使用它管理任务是合适的,不会占用其他定时器。

那么Systick又是如何管理任务的呢?
一般而言,Systick 会设置成几毫秒中断一次,在每次中断时,Systick处理程序(即操作系统内核)都会从所有的任务中选择最高优先级的任务执行,也就是说,系统总是运行最高的任务。
而这个特性也就导致你的高优先级任务不可以无限执行而不主动释放CPU,因为一旦高优先级任务无限执行了,那么低优先级任务将永远得不到执行机会,这就给人一种死机的假象。
可能有道友会疑惑,为什么空闲任务不需要调用系统延时函数去主动释放CPU的使用权呢?
那是因为空闲任务本身优先级就是所有任务中最低的,如果它主动释放 CPU 了,而其他任务都处于挂起状态,那么操作系统又该让谁去执行呢?
所以,空闲任务需要永远处于运行状态
从这个角度来说,操作系统主要的功能就是定时从所有任务中寻找最高优先级的任务,然后让该任务得到运行机会(使用PendSV 中断切换到任务中,模拟中断切换过程),功能类似于中断管理器。
而正因为操作系统只会寻找最高优先级的任务来执行(对于实时操作系统是这样,有些操作系统可能先来先处理的策略),所以任务本身主动释放 CPU 就显得尤为重要了。

最常用的主动释放 CPU 的函数就是系统延时函数了,调用这个函数后,任务将延时一段时候才回来继续执行,而在延时过程中,操作系统就可以调用其他任务执行了,正因为如此,操作系统才显得高效。

虽然操作系统需要中断来剥夺所有任务的执行,从而拥有 CPU 的控制权,但是一般而言,它的优先级却是所有中断中最低的,因为它的优先级只需要高于任务即可,如果设置的更高,那么就会影响到真正需要高优先处理的中断,因为Systick中断的处理还是比较频繁和繁重的,如果设置的太高,那么在Systick处理时,更低优先级的中断将无法处理,这可不是我们想看到的结果。
而如果设置成中断优先级最低的话,既可以剥夺任务的执行,又可以在高优先级中断来临时及时处理中断,让系统的实时得到提高。
与 Systick 配套的中断,还有一个 PendSV 中断,这个优先级一般和 Systick 设置成一样,一般而言该中断的触发是由操作系统内核主动触发的(在切换任务时软件触发该中断),而不像 Systick 一样,定时被动触发,关于两个中断更具体描述可参考《Cortex-M3 权威指南》。

既然中断可以设置成优先级一样的,那么任务应该也可以才对,确实一般的操作系统都可以设置相同优先级的任务(uCOS II 不可以, uCOS III 和 FreeRTOS 、RT-Thread可以),那么操作系统又是如何处理同等优先级的任务?
一般而言,在任务初始化时,会设置任务的时间片,这个时间片就是在任务优先级相同的情况下才会发生作用。
比如,任务 1 设置 5 个时间片(即Systick中断时间),任务 2 设置 10 个时间片,如果两个任务的优先级一样,那么在 15 个时间片内,任务 1 将执行 5 个时间片,之后切换到任务 2 执行10个时间片,来回往复。
那么比任务1 和任务 2 优先级更高的任务该什么时候执行呢?答案是随时,即只要高优先级任务有需要,那么不管任务 1 和 任务 2 是否主动释放 CPU,都会被操作系统强制切换到高优先级任务中执行(由 Systick完成,所以可能会有一点延时)。
那么优先级比它们低的任务呢?这个就靠它们的自觉了,如果它们自觉的主动释放CPU(比如调用系统延时函数),那么低优先级任务就有执行机会,否则,低优先级任务将不会执行!
该用一张图来说明整个系统的优先级关系了:

如何设置任务优先级?
很多人设计任务优先级时都会从 0、1、2、3 这样的顺序来设置,实际上,这种设置是不合理的,因为一旦后面需求变化了,要从中加入一个中间的优先级,那么很可能在加入后程序出现问题了。
其实我们可以从 Cortex-M3 的中断优先级得到启发,即空开部分优先级不使用,留待后面扩展用,比如设计优先级时可以设置成 3、5、7、9、11,留出最高的0~2用于可能的高优先级任务,中间空出一个或两个优先级用于扩展,这样一旦后面需要增加其他优先级的任务,会显得异常简单(可能会有额外的一点内存损耗,但却是值得的)。

本文授权转载自公众号“鱼鹰谈单片机”,作者鱼鹰Osprey


-END-




推荐阅读



【01】深度:嵌入式系统的软件架构设计!
【02】使用git管理嵌入式软件版本
【03】嵌入式软件测试的10条秘诀
【04】嵌入式软件和硬件谁更重要?
【05】嵌入式工程师结合经历聊硬件工程师和软件工程师哪个更有前途?


免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除
嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论
热门推荐
相关推荐
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦