经过前面文章的学习,对于 RT-Thread 处理多任务或者说线程的处理机制,基本上入门了。
但是,还有一个重要的部分,那就是 RT-Thread 如何处理中断。
说到中断,大家都不会陌生,对于裸机编程,很简单,编写指定的中断服务函数就可以了。
如果工程建立在 RTOS 基础之上,中断是如何管理的呢?本文带你了解 RT-Thread 的中断处理过程,以及如何添加中断服务程序和相关的注意事项。
目录
中断管理
什么是中断?简单的解释就是系统正在处理某一个正常事件,忽然被另一个需要马上处理的紧急事件打断,系统转而处理这个紧急事件,待处理完毕,再恢复运行刚才被打断的事件。
生活中,我们经常会遇到这样的场景:
当你正在专心看书的时候,忽然来了一个电话,于是记下书的页码,去接电话,接完电话后接着刚才的页码继续看书,这是一个典型的中断的过程。
电话是老师打过来的,让你赶快交作业,你判断交作业的优先级比看书高,于是电话挂断后先做作业,等交完作业后再接着刚才的页码继续看书,这是一个典型的在中断中进行任务调度的过程。
这些场景在嵌入式系统中也很常见,当 CPU 正在处理内部数据时,外界发生了紧急情况,要求 CPU 暂停当前的工作转去处理这个 异步事件。
处理完毕后,再回到原来被中断的地址,继续原来的工作,这样的过程称为中断。实现这一功能的系统称为 中断系统,申请 CPU 中断的请求源称为 中断源。
中断是一种异常,异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时进行处理,轻则系统出错,重则会导致系统毁灭性地瘫痪。所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环。
如下图是一个简单的中断示意图。

中断处理与 CPU 架构密切相关,所以,本章会先介绍 ARM Cortex-M 的 CPU 架构,然后结合 Cortex-M CPU 架构来介绍 RT-Thread 的中断管理机制,读完本章,大家将深入了解 RT-Thread 的中断处理过程,如何添加中断服务程序(ISR)以及相关的注意事项。
Cortex-M CPU 架构基础
不同于老的经典 ARM 处理器(例如:ARM7, ARM9),ARM Cortex-M 处理器有一个非常不同的架构,Cortex-M 是一个家族系列,其中包括 Cortex M0/M3/M4/M7 多个不同型号,每个型号之间会有些区别,例如 Cortex-M4 比 Cortex-M3 多了浮点计算功能等,但它们的编程模型基本是一致的,因此本介绍中断管理和移植的部分都不会对 Cortex M0/M3/M4/M7 做太精细的区分。
本节主要介绍和 RT-Thread 中断管理相关的架构部分。
寄存器简介
Cortex-M 系列 CPU 的寄存器组里有 R0~R15 共 16 个通用寄存器组和若干特殊功能寄存器,如下图所示。
通用寄存器组里的 R13 作为堆栈指针寄存器 (Stack Pointer,SP);
R14 作为连接寄存器 (Link Register,LR),用于在调用子程序时,存储返回地址;
R15 作为程序计数器 (Program Counter,PC),其中堆栈指针寄存器可以是主堆栈指针(MSP),也可以是进程堆栈指针(PSP)。

特殊功能寄存器包括程序状态字寄存器组(PSRs)、中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)、控制寄存器(CONTROL),可以通过 MSR/MRS 指令来访问特殊功能寄存器,例如:
MRS R0, CONTROL ; 读取 CONTROL 到 R0 中
MSR CONTROL, R0 ; 写入 R0 到 CONTROL 寄存器中
程序状态字寄存器里保存算术与逻辑标志,例如负数标志,零结果标志,溢出标志等等。
中断屏蔽寄存器组控制 Cortex-M 的中断使能。
控制寄存器用来定义特权级别和当前使用哪个堆栈指针。
如果是具有浮点单元的 Cortex-M4 或者 Cortex-M7,控制寄存器也用来指示浮点单元当前是否在使用,浮点单元包含了 32 个浮点通用寄存器 S0~S31 和特殊 FPSCR 寄存器(Floating point status and control register)。
操作模式和特权级别
Cortex-M 引入了操作模式和特权级别的概念,分别为线程模式和处理模式,如果进入异常或中断处理则进入处理模式,其他情况则为线程模式。

Cortex-M 有两个运行级别,分别为特权级和用户级,线程模式可以工作在特权级或者用户级,而处理模式总工作在特权级,可通过 CONTROL 特殊寄存器控制。工作模式状态切换情况如上图所示。
Cortex-M 的堆栈寄存器 SP 对应两个物理寄存器 MSP 和 PSP,MSP 为主堆栈,PSP 为进程堆栈,处理模式总是使用 MSP 作为堆栈,线程模式可以选择使用 MSP 或 PSP 作为堆栈,同样通过 CONTROL 特殊寄存器控制。复位后,Cortex-M 默认进入线程模式、特权级、使用 MSP 堆栈。
嵌套向量中断控制器
Cortex-M 中断控制器名为 NVIC(嵌套向量中断控制器),支持中断嵌套功能。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行位置的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。

当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样会打断当前运行的中断服务程序,然后把这个中断服务程序上下文的 PSR、PC、LR、R12、R3-R0 寄存器自动保存到中断栈中。
PendSV 系统调用
PendSV 也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。PendSV 异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发 PendSV 异常,在 PendSV 异常处理函数中进行上下文切换。
在下一章《内核移植》中会详细介绍利用 PendSV 机制进行操作系统上下文切换的详细流程。
RT-Thread 中断工作机制
中断向量表
中断向量表是所有中断处理程序的入口,如下图所示是 Cortex-M 系列的中断处理过程:把一个函数(用户中断服务程序)同一个虚拟中断向量表中的中断向量联系在一起。当中断向量对应中断发生的时候,被挂接的用户中断服务程序就会被调用执行。

在 Cortex-M 内核上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理,每个中断服务程序必须排列在一起放在统一的地址上(这个地址必须要设置到 NVIC 的中断向量偏移寄存器中)。中断向量表一般由一个数组定义或在起始代码中给出,默认采用起始代码给出:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset 处理函数
DCD NMI_Handler ; NMI 处理函数
DCD HardFault_Handler ; Hard Fault 处理函数
DCD MemManage_Handler ; MPU Fault 处理函数
DCD BusFault_Handler ; Bus Fault 处理函数
DCD UsageFault_Handler ; Usage Fault 处理函数
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD SVC_Handler ; SVCall 处理函数
DCD DebugMon_Handler ; Debug Monitor 处理函数
DCD 0 ; 保留
DCD PendSV_Handler ; PendSV 处理函数
DCD SysTick_Handler ; SysTick 处理函数
… …
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
… …
请注意代码后面的 [WEAK] 标识,它是符号弱化标识,在 [WEAK] 前面的符号(如 NMI_Handler、HardFault_Handler)将被执行弱化处理,如果整个代码在链接时遇到了名称相同的符号(例如与 NMI_Handler 相同名称的函数),那么代码将使用未被弱化定义的符号(与 NMI_Handler 相同名称的函数),而与弱化符号相关的代码将被自动丢弃。
以 SysTick 中断为例,在系统启动代码中,需要填上 SysTick_Handler 中断入口函数,然后实现该函数即可对 SysTick 中断进行响应,中断处理函数示例程序如下所示:
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
中断处理过程
RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户终端服务程序、中断后续程序三部分,如下图:

中断前导程序
中断前导程序主要工作如下:
1)保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
对于 Cortex-M 来说,该工作由硬件自动完成。
当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括 PSR、PC、LR、R12、R3-R0 寄存器。
2)通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable(


990

被折叠的 条评论
为什么被折叠?



