前面多次提到过,实时操作系统中应用程序的运行有两条路线:一条是线程线,可能有许多个线程,由内核调度运行;另一条是中断线,线程被某种中断打断后,转去运行中断服务程序,随后返回原处继续运行。因此,梳理归纳中断的基本概念及处理过程,有助于对实时操作系统下程序运行过程的理解。
异常(Exception)是CPU强行从正在执行的程序切换到由某些内部或外部条件所要求的处理线程上,这些线程的紧急程度优先于CPU正在执行的线程。引起异常的外部条件通常来自外围设备、硬件断点请求、访问错误和复位等;引起异常的内部条件通常为指令、不对界错误、违反特权级和跟踪等。一些文献把硬件复位和硬件中断都归为异常,把硬件复位看作是一种具有最高优先级的异常,而把来自CPU外围设备的强行线程切换请求称为中断(Interrupt),软件上表现为将PC指针强行转到中断服务程序入口地址执行。CPU对复位、中断、异常具有同样的处理过程,本书随后在谈及这个处理过程时统称为中断。
可以引起CPU产生中断的外部器件被称为中断源。中断产生并被响应后,CPU暂停当前正在执行的程序,并保存当前CPU状态(即CPU内部寄存器)在栈中,随后转去执行另一个处理程序,执行结束后,恢复中断之前的状态,使得中断前的程序得以继续执行。CPU被中断后转去执行的程序,称为中断服务程序。
一个CPU通常可以识别多个中断源。给CPU能够识别的每个中断源编号,这个号码就叫中断向量号,一般是连续编号,如0,1,…, n 。当第 i ( i =0,1,…, n )个中断发生后,需要找到与之对应的中断服务程序,实际上只要找到对应中断服务程序的首地址即可。为了更好地找到中断服务程序的首地址,人们把各个中断服务程序的首地址放在一段连续的地址中 ,并且按照中断向量号顺序存放,这个连续存储区称为中断向量表,这样一旦知道发生中断的中断向量号,就可以迅速地在中断向量表的对应位置取出相应的中断服务程序的首地址,把这个首地址赋给PC,那么程序就会转去执行这个中断服务程序。中断服务程序的返回语句不同于一般子函数的返回语句,它是中断返回语句,遇到它,CPU可从栈中恢复CPU中断前的状态,并返回原处继续运行。
从数据结构角度来看,中断向量表是一个指针数组,内容是中断服务程序的首地址。因为中断向量表是连续存储区,与连续的中断向量号相对应,故通常情况下,在程序书写时,中断向量表按中断向量号从小到大的顺序填写ISR的首地址,不能遗漏。即使某个中断不需要使用,也要在中断向量表对应的项中填入默认的ISR首地址(一般设置为0),因为中断向量表是连续存储区,与连续的中断向量号相对应。默认中断服务程序的内容,一般为直接返回语句,即没有任何功能。默认中断服务程序的存在,不仅是给未用中断的中断向量表项“补白”,也可以使未用中断误发生后有个去处,就是直接返回原处。
在ARM Cortex-M微处理器中,还有一个非内核中断请求的编号,称为IRQ号。IRQ号将内核中断与非内核中断稍加区分。对于非内核中断,IRQ中断号从0开始递增,而对于内核中断,IRQ中断号从-1开始递减。
在进行CPU设计时,一般定义了中断源的优先级。若CPU在程序执行过程中,有两个以上中断同时发生,则优先级最高的中断先得到响应。
根据中断是否可以通过程序设置的方式被屏蔽,可将中断划分为可屏蔽中断和不可屏蔽中断两种。可屏蔽中断是指可通过程序设置的方式决定不响应该中断,即该中断被屏蔽;不可屏蔽中断是指不能通过程序方式关闭的中断。
中断处理的基本过程分为中断请求、中断检测、中断响应等过程。
当某一中断源需要CPU为其服务时,它将会向CPU发出中断请求信号(一种电信号)。中断控制器获取中断源硬件设备的中断向量号 ,并通过识别中断向量号,将对应硬件模块的中断状态寄存器中的“中断请求位”置位,以便让CPU知道产生了何种中断请求。
对于具有指令流水线的CPU,它在指令流水线的译码或者执行阶段识别异常,若检测到一个异常,则强行中止后面尚未达到该阶段的指令。对在指令译码阶段检测到的异常和与执行阶段有关的指令异常来说,由于引起的异常与该指令本身无关,指令并没有得到正确执行,所以该类异常保存的PC值是指向引起该异常的指令,以便异常返回后重新执行。对于中断和跟踪异常(异常与指令本身有关),CPU在执行完当前指令后才识别和检测这类异常,故该类异常保存的PC值是指向要执行的下一条指令。
可以这样理解,CPU在每条指令结束的时候将会检查中断请求或者系统是否满足异常条件,为此,多数CPU专门在指令周期中使用了中断周期。在中断周期中,CPU将会检测系统中是否有中断请求信号。若此时有中断请求信号,则CPU将会暂停当前执行的线程,转而去对中断请求进行响应;若系统中没有中断请求信号,则继续执行当前线程。
中断响应的过程是由系统自动完成的,对用户来说是透明的操作。在中断的响应过程中,CPU会先查找中断源所对应的中断模式是否允许产生中断。若中断模块允许中断,则响应该中断请求。中断响应的过程要求CPU保存当前环境的上下文于栈中。通过中断向量号找到中断向量表中对应的中断服务程序的首地址,转而去执行中断服务程序。在中断处理术语中,“上下文”与简单地理解为CPU内部寄存器,其含义是在中断发生后,由于CPU在中断服务程序中也会使用CPU内部寄存器,所以需要在调用中断服务程序之前,将CPU内部寄存器保存至指定的RAM地址(栈)中,在中断结束后再将该RAM地址中的数据恢复到CPU内部寄存器中,从而使中断前后程序的“执行现场”没有任何变化。
本节以ARM Cortex-M微处理器为例,从一般意义上给出中断编程的要点。
(1)理解初始中断向量表。在工程框架的“...\03_MCU\startup”文件夹下,存在由汇编语言编写的启动文件startup_xxx.s,其内含中断向量表。一个MCU所能接纳的所有中断源的中断服务程序首地址(或名称)在此体现。
中断向量表一般位于工程的启动文件中。例如:
其中,除第一项外的每一项都代表着某个中断服务程序的首地址,第一项代表栈顶地址,一般是程序可用RAM空间的最大值+1。此外,对于未实例化的中断服务程序,由于在程序中不存在具体的函数实现,也就不存在相应的函数地址。因此,在启动文件中一般会采用弱定义的方式,将默认未实例化的中断服务程序的起始地址指向一个默认中断服务程序的首地址。例如:
其中,默认中断服务程序的内容一般为直接返回语句,即没有任何功能,也有的程序开发人员使用一个无限循环语句。前面提到过,默认中断服务程序的存在,不仅是给未用中断的中断向量表项“补白”,也可以使未用中断误发生后有个去处,最好为直接返回原处。
(2)确定对哪个中断源编程。在进行中断编程时,必须明确对哪个中断源进行编程。该中断源的中断向量号是多少,有时还需知道对应的IRQ号,以便设置。
(3)宏定义中断服务程序名。可以根据程序的可移植性,重新给默认的中断服务程序名起个别名,随后使用这个别名。
(4)编写中断服务程序。在isr.c文件中安排中断服务程序,使用已经命名好的别名。在中断服务程序中,一般先关闭总中断,退出前再开放总中断。
(5)在实时操作系统下中断初始化问题。在实时操作系统下,中断向量表被复制到RAM中。因此,中断服务程序名必须在初始化中重新加载,同时使对应中断源开放总中断。这样,当该中断产生时,就会执行对应的中断服务程序。