RT-thread内核之异常与中断
一、什么是中断?
中断有两种,一种是CPU本身在执行程序的过程中产生的,一种是由CPU外部产生的。 cpu外部中断,就是通常所讲的“中断”(interrupt)。对于执行程序来说,这种“中断”的发生完全是异步的,因为不知道什么时候会发生。CPU对其的响应也完全是被动的, 可以通过“关中断”指令关闭对其的响应。 然而由软件产生的中断一般是由专设的指令,如X86中的“INT n”在程序中有意产生的, 是主动的,同步的。只要CPU执行一条INT指令,在开始执行下一条指令之前一定会进入中 断服务程序。这种主动的中断称为“陷阱”(trap)
从物理学的角度看,中断是一种电信号,由硬件设备产生,并直接送入中断控制器的输入引脚上,然后再由中断控制器向处理器发送相应的信号。处理器一经检测到该信号,便中断自己当前正在处理的工作,转而去处理中断。此后,处理器会通知 OS 已经产生中断。这样,OS 就可以对这个中断进行适当的处理。不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标识,这些值通常被称为中断请求线(IRQ)。
中断可分为同步(synchronous)中断和异步(asynchronous)中断:
1. 同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。
2. 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。
二、什么是异常?
同步中断又称为异常(exception);异步中断则被称为中断(interrupt)。我们通常讲的中断指的都是异步中断,即cpu外部中断。
1.中断可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。
2.异常可分为故障(fault)、陷阱(trap)、终止(abort)三类。
这些类别之间的异同点如下:
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 返回到当前指令
终止 不可恢复的错误 同步 不会返回
三、中断处理过程
当中断产生时,处理器将按如下的顺序执行:
• 保存当前处理机状态信息
• 载入异常或中断处理函数到PC寄存器
• 把控制权转交给处理函数并开始执行
• 当处理函数执行完成时,恢复处理器状态信息
• 从异常或中断中返回到前一个程序执行点
中断使得CPU可以在事件发生时才予以处理,而不必让CPU连续不断地查询是否有相应的事件发生。通过两条特殊指令:关中断和开中断可以让处理器不响应或响应中断(在关闭中断期间,通常处理器会把新产生的中断挂起,当中断打开时立刻进行响应)。在执行中断服务例程的过程中,如果有更高优先级别的中断源触发中断,由于当前处于中断处理上下文环境中,根据不同的处理器构架可能有不同的处理方式:比如新的中断等待挂起直到当前中断处理离开后再行响应,但这在硬实时环境中不允许发生;或者新的高优先级中断打断当前中断处理过程,而去直接响应这个更高优先级的新中断源,这称为中断嵌套,而中断是否能够嵌套,一般由MCU处理器的中断机制决定,如stm32中只有新中断的抢占优先级比当前中断高时,才能打断当前中断进入新的中断服务函数。
在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中),再调用中断服务例程进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务例程函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文运行中断处理函数。中断栈可以保存在打断的线程栈中,当从中断中退出时,返回相应的线程继续执行。中断栈也可以与打断线程栈完全分离开来,即每次进入中断时,在保存完被打断的线程上下文后,切换到新的中断栈中独立运行;在中断退出时,再做相应的上下文恢复。
使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。RT-Thread采用的方式是提供独立的中断栈,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间,从而提高了内存空间的利用率,且随着任务的增加,这种减少内存占用的的效果也越明显。以stm32的cotex-M3/M4为例,cotex-M3/M4中拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个:
主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核,以及异常与中断处理。
进程堆栈指针(PSP):由用户的应用程序代码(如线程切换)使用。堆栈指针的最低两位永远是0,这意味着堆栈总是4 字节对齐的。
由此可以看出,在一个实际运行系统里有两大部分:一是操作系统和中断,一是用户应用程序。它们使用的资源是不一样的,从中断(线程调度时产生的中断)返回到用户应用程序线程时,系统使用的堆栈指针也从MSP变成PSP。
四、与操作系统相关的中断接口:在src/irq.c中
void rt_interrupt_enter(void); void rt_interrupt_leave(void); 当整个系统被中断打断,进入中断处理函数时,OS需要知道当前已经进入到中断状态。 rt_interrupt_enter函数用于通知OS,当前已经进入了中断状态;rt_interrupt_leave函数用于通知OS,已经离开中断状态。通常来说,OS需要知道这样的运行状态,这样在中断服务例程中,如果调用了OS相关的调用,OS好及时调整相应的行为,例如进行任务切换时应该采取中断中任务切换的策略,而不是立即进行切换。但是如果中断服务例程很显然、很必然地不会去调用OS相关的函数,此时也可以不调用rt_interrupt_enter/leave函数。 rt_uint8_t rt_interrupt_get_nest(void); 获取当前中断嵌套计数值,大于0说明处于中断服务中,大于1说明存在中断嵌套
五、与MCU相关的中断接口:以stm32f4为例
关闭中断:在libcpu/arm/cotex-m4/context_rvds.S中用汇编语言实现 rt_base_t rt_hw_interrupt_disable(void); 函数返回中断前的系统中断状态。 当系统关闭了中断时,就意味着当前线程/代码不会被其他事件所打断(因为整个系统已经不再对外部事件响应),也就是当前线程不会被抢占(因为线程切换时用到了PendSV_Handler),除非这个线程主动让出处理器。 打开中断:在libcpu/arm/cotex-m4/context_rvds.S中用汇编语言实现 void rt_hw_interrupt_enable(rt_base_t level); 调用这个函数接口将恢复调用rt_hw_interrupt_disable前的中断状态,level是上一次关闭中断时返回的值。打开中断往往是和关闭中断成对使用的,用于恢复关闭中断前的状态。 注意:调用这个接口并不代表着肯定打开中断,而是恢复关闭中断前的状态,如果调用rt_hw_interrupt_disable()前是关中断状态,那么调用此函数后依然是关中断状态。