在嵌入式系统开发中,中断是十分重要的知识点,在大部分单片机构建的应用产品中,基本都是以前后台方式(大循环加中断)的方式来实现功能,在主循环中处理应用,并在中断中处理外部的触发信号,以及对响应时间有要求的应用,如用于时间相关处理的定时器中断,对按键响应的外部中断,用于通讯的收发和异常处理的串口中断,SPI中断, 网络中断等。另外,对于大部分RTOS来说,如Cortex-M系统中的systick中断和PendSV中断,又是实现基于队列和任务调度算法的RTOS的核心;对于嵌入式Linux应用来说,中断也是处理CPU的突发事件的主要方法,包含对于溢出,除零等异常情况的通知,多核之间通讯交互,外部设备的请求处理等。因此理解中断的背后执行逻辑,对于单片机和嵌入式Linux的开发都有重要意义,虽然中断的目的对于单片机和嵌入式Linux设备基本一致,但因为Linux系统与裸机开发的应用场景不同,以及两种芯片设计上的差异,Cortex-A系列和M系列的中断设计是有很大不同的,所以下面就分两部分讲解嵌入式开发中的中断机制。
1.1 单片机中的中断机制
在单片机开发中,对于中断的表示方法也因为内核的不同有很大的差异,如51使用中断号来表示指定中断,而ARM Cortex-M内核中则使用中断向量表的方式配合内核中的NVIC控制器来实现中断的处理,不过考虑到目前的主流单片机方案,因此以典型的Cortex-M3内核说明单片机中的中断控制机制, 另外Cortex-M系列中的其它内核中的中断流程也基本一致。
1.1.1 中断向量表
对于Cortex-M3内核,支持最大编号包含0 ~ 255的中断类型,其中0 ~ 15为系统异常,主要处理系统执行中产生的复位,错误,主动触发的SVC,异常等, 编号16~255则是由芯片设计厂商自定义设计,用于满足芯片功能需求的中断(芯片厂商可以自由定制, 不超过最大编号且不重复即可), 这两部分共同组成了单片机的向量表,参考<Cortex-M3权威指南中的说明>,向量表的格式如下:
反映在软件实现就是在startup_xxx.s启动文件中定义的中断向量表,具体结构如下:
其中External Interrupts就是由厂商定义的中断类型,另外中断号为0的位置为空,设计上就用来存储堆栈指针。
在芯片在上电的过程中就是执行复位机制 根据SCB->VTOR查询向量表,找到Reset_Handler入口,并加载__initial_sp到堆栈指针R13中,后续就可以正常的工作了。
在上述结构中,系统中断是在内核定义时确定的,外部中断在芯片设计时被确定,将中断编号和指定外设的中断触发信号绑定,就构建了完整的中断向量表。
1.1.2 中断执行流程
对于指定的外设,如何在中断触发时找到指定的中断入口函数,这就涉及到模块内部的中断机制以及NVIC的功能设计,下面就是典型的单片机中的中断执行流程:
-
外设或者模块本身根据实际的硬件情况产生触发信号,置位模块内相应状态位,如外部中断的触发,串口数据接收或发送完成等事件,转换为模块内部的状态的变化,并置位相应状态位,总结流程就是触发信号->模块内部状态变化->修改状态标志位。
-
模块根据内部状态变化,在结合中断使能寄存器相关的配置,将状态变化通过中断信号提交到中断向量控制器(也就是NVIC)中。
-
NVIC控制器根据中断信号定义的编号信息,置位ISER中的相应中断Pending位,在配合ICER的使能状态,如果当前触发的为执行中的最高优先级中断,则查询中断向量表并执行。
上述就是Cortex-M系列中断的主要执行流程,根据这样的流程,我们在软件中的中断执行应用也包含模块中断的使能和内核中断的使能,如对于UART模块中断配置中的
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_EnableIRQ(USART1_IRQn);
就是执行上述流程中的模块内部和NVIC内部的中断使能工作,了解了这些,就可以基本知道从外设提供信号到中断触发的全部流程。
不过对于实际的应用,还有个说明是内核如何找到指定的中断函数, NVIC会根据中断信号,置位相应的中断Pending标志位,对于Cortex-M内核,这些标志位的和向量表中的编号是一一对应的,如Cortex-M3内核中为8*32bit的寄存器即256bit,每一个bit对应一个中断向量表中的编号位置。
通过上面我们了解到,可以通过SCB->VTOR寄存器的值找到中断向量表的首地址,而中断的编号和其在向量表中的位置是一一对应的,以中断向量表的首地址为Start,中断编号为num为例,则中断的入口地址为
Saddr = Start + num*4;
同时内核将修改后的地址赋值给PC指针,即可实现软件代码的执行路径改变,这方面的知识在软件中并不常用,只有在带bootloader的升级应用或允许设备在SDRAM或SRAM这种地址运行时,需要了解并进行修改,才能保证代码中断的正常执行。
1.1.3 中断优先级
中断中的另一个重要知识就是中断优先级和中断抢占,在上述我们讲述的主要是中断对于主循环的打断和流程改变,但实际应用中,有多个中断同时执行的情况,这就需要通过优先级来满足不同中断的执行需求,一般来说,通讯的不及时处理会导致FIFO溢出,接收超时等问题,导致通讯失败,一些特殊检测的设备,如过压,过流,保护的响应,如果处理不及时,会导致电路和芯片的损毁,这些应用的中断需要立即响应,中断优先级就是为了满足中断执行的响应快慢要求不同而设计的,在内核中,通过
SCB->SHP设置系统中断的优先级
NVIC->IP设置厂商自定义中断的优先级
SCB->AIRCR(应用程序中断及复位控制寄存器)设置寄存器的分组
在说明优先级相关的知识下面,要先了解几个概念。
优先级等级: 决定中断执行顺序的等级,M3规定优先级值越小,优先级越高
中断嵌套: 当一个中断打断另一个中断的执行流程时,优先执行时,被称为中断嵌套。
上图就是典型的发生中断嵌套的过程,当触发中断事件后,流程如下:
- 中断#1判断为触发,进行入栈操作,同时系统模式从线程切换到Handler模式,开始执行中断服务函数ISR1
- 当优先级更高的中断#2判断为触发,则打断中断#1的执行,再次执行入栈操作,系统模式不变,执行中断服务函数ISR2
- ISR2执行完毕后,进行出栈操作,执行ISR1的后续部分,系统模式不变
- ISR1执行完毕后,进行出栈操作,后续进行被打断的主循环中,继续执行,同时系统模式从Handler切换为线程模式,整个中断嵌套的流程执行完毕。
抢占优先级: 优先级的寄存器高位bit[7:n],声明抢占优先级高的中断可以打断低优先级中断,进行中断嵌套。
亚优先级: 优先级的寄存器高位bit[n-1:m], 声明亚优先级高的中断,能够优先执行,但不能打断其它中断,需要等低优先级中断结束后才能执行。
M3内核规定最大有8bit的优先级等级(具体支持级数由芯片公司设计时确认),其中至少有1位为亚优先级,按照这样设计,内核规定最大的强占优先级级为128级。下面以3bit可用的优先等级为例:
其中低5位不可用,但仍可以被分配给亚优先级,如此就有2^3=8个等级的抢占优先级。
中断流程,中断向量表,中断嵌套,优先级,这些就是单片机中需要了解和掌握的中断相关的知识,这部分其实在产品开发中十分重要,如进行以太网,Uart等硬件交互的时候,确保底层FIFO不会溢出,对于定时器,如何保证时间的精确有效而不被其它应用干扰,如何规划避免风险,这都需要对中断机制和优先级部分有着深入的考虑,所以这部分知识是十分值得了解掌握的。
1.2 嵌入式Linux设备中的中断机制
在上述流程,我们大致对于单片机中的中断有了比较清晰的认识,也可以理解在启动文件startup_xxx.s中中断向量表的含义,以及中断触发后找到中断入口的具体流程。不过对于嵌入式Linux系统来说,虽然有一定的参考作用,单从原理和应用来说差别还是蛮大的。
1.2.1 中断机制
下面是Cortex-A7中的向量中断表,不过和M3相比,仅支持7个异常中断(一个未使用)。
详细表格如下:
0x00 | 复位中断(Reset) | 特权(Supervisor) |
0x04 | 未定义指令中断(Undefined Instruction) | 未定义指令(undef) |
0x08 | 软中断(Software Interrupt, SWI) | 特权(Supervisor) |
0x0C | 指令预取中止中断(Prefetch Abort) | 中止(Abort) |
0x10 | 数据访问中止中断(Data Abort) | 中止(Abort) |
0x14 | 未使用(No Used) | / |
0x18 | IRQ中断(IRQ Interrupt) | 外部中断(IRQ) |
0x1C | FIQ中断(FIQ Interrupt) | 快速中断(FIQ) |
上图就是Cortex-A7芯片支持的中断的全部类型,其中0x00~0x10为系统中断,当芯片执行出现异常,或者由SWI指令主动触发时,就会执行这些中断,一般由Linux内核管理,这里可以了解下,其中关键的有以下几点。
复位中断: CPU上电复位后会进入该中断,一般会执行系统硬件的初始化工作,如初始化堆栈指针,配置硬件接口访问外部的DDR等。
软中断: 由SWI指令触发的中断,一般Linux系统调用会通过SWI指令触发软中断,从而进入内核空间。
基于对单片机的了解,芯片都是支持多个外设的,特别是对于Cortex-A系统芯片,往往更加复杂,那么仅依靠7个中断如何支持外部中断的需求?这就要提到IRQ和FIQ了, 对于Cortex-A系列芯片来说,任意一个外部中断如IO_Interrupt, Legacy_Interrupt等,都会触发IRQ或FIQ中断,进入对应的中断函数,,并在此函数中通过软件读取寄存器的值来判断具体发生了什么中断,管理外部信号到中断触发的结构被称为GIC(Generic Interrupt Controller),而中断又根据源头的不同,分为
SGI(Software Generated Interrupt): 软件触发中断,如用于多核之间主动通讯的中断,通过软件向GICD_SGIR 中写入数据触发的中断,中断ID编号可选1~15
PPI(private Perpherial Interrupt): 私有外设中断, 每个CPU核心的特有中断,中断编号可选为16~31
SPI(Shared Perpherial interrupt): 共享外设中断,这些中断可以指定到任意一个CPU内核,可通过接口irq_set_affinity接口指定中断最后响应的内核, 中断编号可选为32-1019。
另外对于Cortex-A7芯片来说,中断使能包含IRQ或FIQ总中断使能,以及ID0-ID1019可选的中断源使能两部分;优先级为32bit的数据,支持抢占优先级和子优先级,这部分和Cortex-M系列基本是一致的,可参考上节相应说明这里不在赘述。
1.2.2 中断处理
在理解Cortex-A7内核处理机制之前,需要了解到芯片内核状态分类如下:
- user mode:用户模式,用户空间AP执行所处于的模式。
- superiver mode: 超级模式,或者SVC模式,大部分Linux内核执行代码处于该模式下。
- IRQ mode: 中断模式,触发中断后,处理器进入的模式。
参考上面中断表格,还包含Abort mode来处理上面提到的Data Abort和prefetch Abort异常。下面以在用户进程执行下的IRQ中断来演示大部分中断的执行流程。
- 和上面的单片机的流程类似,当有外部触发信号到达,并且所有中断相关的使能都打开的情况下,中断控制器GIC就会根据配置好的硬件信息,将IRQ(或FIQ)的中断触发信息告知指定的Core。
- 处理器感知到该信号后到达时,对于进行irq模式前的系统状态值如cpsr寄存器值,PC指针进行保存(分别保存到SPSR和LR寄存器中)
- 置位相应的中断状态标志位
- 根据上面的表格计算中断向量的入口位置,将PC设置为该值并跳转,
总结下来,在中断发生时,内核的硬件处理包含置位中断信息,保存中断前关键状态,进入IRQ模式,然后在跳转到中断向量的入口,后续就由软件接口进行后续处理。
对于软件部分的处理,则包含irq模式,svc/usr模式处理和应用代码处理。
- irq模式主要进行了r0,lr以及cpsr的保存,压栈处理(Irq模式下的堆栈设定为12字节)
- 根据进入中断前的系统模式,维护中断处理表格,进行后续的中断处理,根据中断前模式的不同分别执行不同的入口函数_irq_usr(用户模式入口函数)和irq_svc(superior模式入口函数),同时将系统模式切换到SVC模式
- 以用户模式为例,主要实现流程包含:保存用户现场, 执行中断向量irq_hander,这是软件部分的核心处理,在其中实现将当前硬件中断系统的状态转换为定义好的软件IRQ Number, 然后调用IRQ Number的处理函数即可。
- 执行完中断相关的处理函数,将进入中断时候保存的现场恢复到实际的ARM寄存器中,返回到中断触发时执行的流程,即实现了中断返回。
1.2.3 中断应用
对于内核中,涉及到中断访问的接口主要由中断申请和释放的函数组成
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
void free_irq(unsigned int irq, void *dev);
irq为要申请中断的中断号,由CPU分配的软件中断号
handler即为中断触发时执行的回调函数,一般handler实现如下
static irqreturn_t key0_handler(int irq, void *dev_id)
{
//中断的具体实现
//...
return IRQ_RETVAL(IRQ_HANDLED);
}
flags为指定中断实际触发的执行条件,可以是IRQF_TRIGGER_RISING, IRQF_TRIGGER_FALLING等。
name为中断指定的名称,在cat /proc/interrupts下显示,如key0就是通过申请的中断。
dev则为传递到中断执行的参数,一般用于IRQF_SHARED中断执行时,区分不同的中断,大部分情况为设备结构体,dev会传递给中断处理函数中的第二位。
当然,也可以通过devm_request_irq来申请中断,可以在驱动卸载时不用主动调用free_irq显示释放中断请求。
此外,也可以通过disable_irq(非中断函数中), disable_irq_nosync(中断函数中)和enable_irq来管理中断的开关。
在单片机中,我们了解到中断的执行会打断其它应用的执行,所以中断的动作应该尽可能的短,如果具体的操作过长,会把代码分为两部分,其中时间相关比较紧要的在中断中执行,而非必要的则移动到主循环中执行,如UART通讯中,数据接收往往通过中断或者DMA获取,而协议解析和应用处理则在主循环执行,那么对于嵌入式Linux来说,在驱动中也采用类似的机制,把中断的应用拆成两部分执行,一般称为顶半部和底半部机制执行,其中顶半部即为上述的中断回调函数key0_handler,其在内核模式下调用,因此会影响到系统的切换。所以一般来说仅进行简单的事件触发动作,具体的应用则在底半部执行,通过常见的tasklet,工作队列,软中断或软件定时器机制,避免在硬件中断流程中执行大量的代码,从而影响到Linux系统的执行和切换,这里tasklet为例:
//底半部应用部分执行
static void tasklet_do_func(unsigned long data)
{
printk(KERN_INFO"key interrupt tasklet do:%ld!\r\n", data);
}
DECLARE_TASKLET(tasklet_func, tasklet_do_func, 0);
//顶半部中断向量执行
static irqreturn_t key0_handler(int irq, void *dev_id)
{
/*触发事件*/
tasklet_schedule(&tasklet_func);
return IRQ_RETVAL(IRQ_HANDLED);
}
如此,当执行按键时,即可正常触发中断显示如下:
理解到这,我们对嵌入式Linux中的中断触发和在内核的应用已经有了初步的了解,那么我们是如何获取外设对应的硬件中断号,这就需要涉及设备树中的中断信息节点,以imx6ull为例,在3.2 Cortex A7 interrupts章节中,定义了内部的中断ID,以按键对应的GPIO1_18引脚中断为例,其中
基于上述结构,我们定义gpio1作为GPIO1上对应的总的中断控制入口(即中断控制器), 实现如下:
//定义中断控制器节点
gpio1: gpio@0209c000 {
//其它已经注释
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-controller;
#interrupt-cells = <2>;
};
//具体的中断节点
key {
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
在具体的代码实现中,可通过函数
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
即可找到引脚对应的中断线号。
1.3 总结
至此,我们对嵌入式应用中的中断机制进行了解读,当然这些都是理论知识的说明,对于嵌入式应用中,如何结合实际情况,配置合适的中断优先级,并实现应用从而满足产品的需求才是最重要的部分。这部分经验是需要实践积累和总结的,不过理解了中断实现的背后机制,在实践中知其然也知其所以然,以理论配合应用来学习,也是嵌入式开发的最佳提升之道。