老板说我技术需要有长进,不能只做一个crud boy。 于是我选来选去,终于选定了,来学习操作系统。因为操作系统一直被看做是计算机软件的基石。
本系列是我学习操作系统的笔记,操作系统是以AliOS Things为例子。其他的操作系统也是差不多。
本文主要是讲操作系统的中断管理,后面会有更多的操作系统内容介绍。
主要介绍AliOS Things中断相关的概念。由于中断处理与CPU架构密切相关,所以本本文会基于HaaS100开发板/HaaS EDU K1所使用的ARM Cortex-M3的CPU架构,来介绍AliOS Things的中断管理机制。读完本文,大家将深入了解AliOS Things的中断处理过程、如何添加中断服务程序(ISR)以及相关的注意事项。
国庆假期学门新技术,拒绝只做crud boy, 就从操作系统开始 - 定时器管理
在嵌入式系统中,当中央处理器CPU正在处理某件事的时候,外部发生了某一事件,请求CPU迅速处理,CPU 暂时中断当前的工作,转入处理所发生的事件,处理完后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。
下图是一个简单的中断处理示意图。
中断处理示意图
在嵌入式系统中,中断常常被称为异步异常。从中断处理示意图也可以看出,中断或者异常都是指任何打断处理器正常执行,并且迫使处理器进入另外一个指令流执行的事件。异常通常可以分成两类:同步异常和异步异常。由内部事件(像处理器指令运行产生的事件)引起的异常称为同步异常,如指令读写访问了一个非法地址、代码中出现了除0等错误,都会导致(同步)异常。异步异常与当前的指令流执行无关,异步异常事件来源于外部,比如按下一个按键,产生了一个异步事件,这个异步事件通常也被称为异步异常。
中断是一种异步异常,是导致处理器脱离正常运行流程,转向执行特殊代码的异步事件。在嵌入式系统中,中断处理与系统实时性紧密相关。正确的中断处理是避免系统错误、提高系统稳定性和实时性的重要手段。
首先明确中断向量和中断向量表的概念。
中断向量:所有中断处理程序(ISR)的入口。
中断向量表:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
以ARM Cortex-M系列处理器为例,所有中断都采用中断向量表的方式进行处理。中断的处理过程就是外界硬件发生了中断后,CPU到中断控制器读取中断向量,并且查找中断向量表,找到对应的中断服务程序 (ISR)的首地址,然后跳转到对应的ISR去做相应处理。如图所示:
中断向量表示意图
Cortex-M中断控制器名为NVIC(Nested Vectored Interrupt Controller嵌套向量中断控制器)。NVIC共支持1至240个外部中断输入(通常外部中断写作IRQs)。具体的数值由芯片厂商在设计芯片时决定。此外,NVIC还支持不可屏蔽中断(NMI)输入。NMI的实际功能也是由芯片制造商决定。在某些情况下,NMI无法由外部中断源控制。
NVIC与CPU内核息息相关,CPU的所有中断机制都由NVIC实现:
Cortex-M内核和NVIC关系示意图
NVIC除了响应外部中断外,还具有以下几个功能:
1)可嵌套中断支持
可嵌套中断指的是当一个中断发生时,硬件会自动比较该中断的优先级是否比当前的中断优先级更高。如果发现来了更高优先级的中断,处理器就会中断当前的中断服务例程(或者是普通程序),而服务新来的中断——即发生了中断抢占。此时,操作系统将先保存当前中断服务函数的上下文环境,并且转向处理高优先级中断,当高优先级中断处理完后,才能继续执行被抢占的低优先级中断。
2) 动态优先级调整支持
软件可以在运行时期更改中断的优先级。如果在某ISR中修改了自己所对应中断的优先级,而且这个中断又有新的实例处于悬起中(pending),也不会自己打断自己。
3) 中断可屏蔽
Cortex-M中断控制器即可以屏蔽优先级低于某个阈值的中断/异常,也可以全局开关中断。
AliOS Things将中断处理程序分为中断进入、用户中断服务处理程序、中断退出三部分,如图所示:
中断进入
中断进入的主要工作是保存CPU中断现场,这部分与CPU架构相关,不同CPU架构的实现方式有差异。对于Cortex-M来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行任务的上下文自动保存在中断栈中,保存的上下文寄存器和顺序如下图所示:
同时,通知内核进入中断状态,调用krhino_intrpt_enter()函数,其作用是把全局变量g_intrpt_nested_level加1,用它来记录中断嵌套的次数。硬件随即执行用户中断服务程序ISR。
中断退出
执行完用户ISR后,系统首先通知内核离开中断状态,通过调用krhino_intrpt_exit()函数,把全局变量g_intrpt_nested_level减1。然后判断是否需要发生任务切换,判断的理由是当前的任务调度队列中是否有高优先级的任务处于就绪状态,如果有高优先级任务,则会发生任务切换,CPU会选择优先级的任务开始运行。
中断中进行任务切换和恢复上下文的工作,与具体的CPU架构相关。具体可以参考代码cpu_intrpt_switch()的具体实现。
在Cortex-M内核以及NVIC的芯片设计中,已经内建了对中断嵌套的全力支持,用户只需要为每个中断适当地建立优先级, 如果在执行中断服务程序的过程中,如果出现高优先级中断,当前服务程序的执行将被打断,以执行高优先级的中断服务,当高优先级中断服务执行完毕后,被打断的中断服务继续执行。如图所示:
Cortex-M内核会自动入栈和出栈,用户无需担心在中断发生嵌套时,会使寄存器的数据损毁,从而可以放心地执行服务例程。
为了把操作系统和底层硬件平台相关的实现隔离,AliOS Things提供了一组中断相关的接口。如下图所示:
全局开关中断
全局关中断可以让处理器不响应中断。在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时再进行响应。在嵌入式系统中,当前任务独占CPU访问临界区资源的最简单的一种方式即是通过全局开关中断来实现。关闭中断后,整个系统已经不在响应哪些可以触发任务重新调度的外部事件,这样就可以保证当前任务不会被其他事件打断 ,除非这个任务主动放弃了CPU的控制权。
当需要进入临界区,暂时关闭整个系统的中断,随即执行临界区代码后恢复中断,可成对调用下面的函数接口:
RHINO_CRITICAL_ENTER()
RHINO_CRITICAL_EXIT()
调用关闭中断RHINO_CRITICAL_ENTER()时,系统的中断状态会保存在一个临时变量里,调用恢复中断RHINO_CRITICAL_EXIT()时会恢复之前的中断状态,保证这两个接口使用前后,系统中断状态前后一致。即如果调用前系统的中断是关闭的,那么调用后也应该是关闭状态,不应该被打开;同理,如果调用前系统的中断是打开的,那么调用后也应该是打开状态,不应该被关闭。
使用全局开发中断操作临界区的方法可以应用于任何场合,可以说是最强大、最高效的同步方法。但需要注意的是,在关中断期间,系统不再响应任何中断,也就不能响应外部事件。所以全局关中断对系统的实时性是有影响的,一般用于短暂的临界区代码。
下面以SysTick中断为例,在系统启动代码中,需要将SysTick_Handler中断服务程序添加到中断向量表中。
SysTick_Handler 在AliOS Things的实现demo如下
void SysTick_Handler ( void )
{
/* 进入中断, 中断嵌套的次数加1 */
krhino_intrpt_enter();
/* tick中断服务程序*/
krhino_tick_proc();
/* 退出中断,中断嵌套的次数减1,同时会检查是否发起调度 */
krhino_intrpt_exit();
}
AliOS Things作为嵌入式RTOS,其中断原理是比较基础的,没有Linux上“上半部下半部”、“用户态中断”的概念。但是理解中断,尤其是结合芯片架构,对于理解一个嵌入式系统是非常重要的。
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号。
更多技术与解决方案介绍,请访问HaaS官方网站https://haas.iot.aliyun.com。