本章我们以按键为例讲解在驱动程序中如何使用中断,这里主要介绍在驱动中如何使用中断。对于中断的概念及GIC 中断控制器相关内容,则借鉴其他基于cortex-A7 内核的芯片进行简单讲解,因为中断相关的内容,大多都于ARM 内核紧密联系在一起,这部分内容比较通用。
ARM 内核通用中断管理
由于MP157 为AMP(非对称多处理) 架构,包含了Cortex-M 及Cortex-A 两款ARM 内核,所以拥有两套中断系统。相比于Cortex-M 核使用的NVIC,Cortex-A 核中的中断控制系统更复杂,它的中断管理器使用的是GIC V2,GIC V2 的实现方式与我们熟知的Cortex-M 核中使用的NVIC 差别较大。本章简单讲解Cortex-A 核的GIC 基本结构以及实现方法,更详细的介绍可以参考《ARM®Generic Interrupt Controller》。
关于中断背景知识内容,我们简单讲两点:
- 掌握GIC V2 的实现原理
- 了解中断的种类及部分代码细节
GIC 简介
GIC 是Generic Interrupt Controller 的缩写,直译为通用中断控制器。它由ARM 公司提出设计规范,并给出一个实际的控制器设计参考,比如GIC-400。最终芯片厂商可以自己实现GIC 或者直接购买ARM 提供的设计。目前的设计规范中最常用的,有3 个版本V2.0、V3.1、V4.1,MP157使用的是GIC V2.0 设计标准设计的GIC。
ARM 提出的GIC V2.0 标准下的功能框图如下所示。
ST 为MP157 根据GIC V2.0 标准设计的GIC 功能框图如下。
GIC V2.0 最多支持8 个处理器(processor0~ processor7,MP157 按照这个规范设计了只支持双核的GIC 控制器)。不同处理器的GIC 功能是相同的,我们只看其中一个即可。GIC 主要分为分发器(Distributor)和CPU 接口(CPU interface/Virtual CPU interface)。下面重点讲解着两部分。
分发器
分发器简介
分发器用于管理CPU 所有中断源,确定每个中断的优先级,管理中断的屏蔽和中断抢占。最终将优先级最高的中断转发到一个或者多个CPU 接口。CPU 的中断源分为三类,讲解如下:
- SPI(Shared peripheral interrupts),SPI 是共享外设中断,共享外设也就是我们在常用的串口中断、DMA 中断等等,对于多个CPU 而言,它们操作的都是同一个外设,从某些角度来说它们的中断就是共享的。在上图中SPI 中断编号为(32~ 1019),这是GIC V2.0 规范中支持的最大SPI 的ID 数量,那么实际芯片支持的共享外设中断数量由芯片设计厂商决定,MP157 的GIC 支持的共享外设中断数量为256 个(中断编号为32~287)。那么实际使用的SPI 中断请求,如下所示。
图片摘自MP157 的参考手册中的第21 章《Interrupt list》下的《21.2 GIC Interrupts》小节。
- SGI,SGI 是软件中断,PPI 是CPU 私有中断。SGI 中断, 共有16 个中断,中断编号为0~15,SGI 一般用于CPU 之间通信。
- PPI,PPI 有16 个中断,中断编号为16~31,SGI 与PPI 中断都属于CPU 私有中断,每个CPU都有各自的SGI 和PPI,这些中断被存储在GIC 分发器中。CPU 之间通过中断号和CPU 编号唯一确定一个SGI 或PPI 中断。
分发器提供了一些编程接口或者说是寄存器,我们可以通过分发器的编程接口实现如下操作,稍后将介绍这些操作对应的寄存器。
- 全局的开启或关闭CPU 的中断。
- 控制任意一个中断请求的开启和关闭。
- 设置每个中断请求的中断优先级。
- 指定中断发生时将中断请求发送到那些CPU。
- 设置每个”外部中断”的触发方式(边缘触发或者电平触发)。
分发器相关寄存器介绍
上一小节提到GIC 分发器提供了一些编程接口,”编程接口”可以认为是寄存器,这里将简单介绍这些寄存器,因为我们程序中很少会去修改它。更详细的内容请参考《ARM® Generic InterruptController》4.3 Distributor register descriptions。
GIC 分发器映射表如下所示。
表分发器编程接口
首先我们简单介绍上表,在表中只给出了寄存器相对于GIC 分发器基地址的偏移地址,GIC 基地址保存在另外的寄存器中,使用到是我们将详细介绍。”默认值”选项中有”待定”手册原文是”IMPLEMENTATION DEFINED”, 原因是这张表格摘自《ARM® Generic Interrupt Controller》,它不针对具体的芯片,这些寄存器的默认值由芯片厂商决定。
表格项”地址偏移”部分值是一个范围比如”中断使能寄存器”地址偏移为”0x100-0x17C”,原
因是”中断使能寄存器”有很多,地址偏移范围是”0x100-0x17C”。
部分寄存器简单介绍如下:
中断使能寄存器GICD_ISENABLERn
中断使能寄存器与中断屏蔽寄存器(GICD_ICENABLERn)是一一对应的,GIC 分发器将中断的使能与屏蔽分开设置。
中断使能寄存器如下所示。
仅从《ARM® Generic Interrupt Controller》可知共有1020 个(0~1019)中断号即1020 个中断,很显然要分别控制每一个中断,中断使能寄存器(GICD_ISENABLER)肯定不止一个。从表54‑1 可知中断使能寄存器偏移地址为0x100-0x17C,中断使能寄存器从GICD_ISENABLER0 到GICD_ISENABLERn 依次排布在这段地址空间。
在程序中我们是通过中断号区分不同的中断,假如我们已知中断号为m,那么我们怎么开启或关闭中断m 呢?计算过程如下:
(1) 计算要设置那个中断使能寄存器(假设为n),n = m / 32。例如中断号m = 34 则中断使能寄存器为GICD_ISENABLER1。寄存偏移地址为(0x100 + (4*n)) = 0x120。
(2) 计算要设置哪一位,接步骤①,假设设置位为q,则q = m %32,m = 34, 则开启中断号为34 的中断就需要设置GICD_ISENABLER1[2]。
中断使能寄存器支持按位读、写,读出的数据是终端店额当前状态,为0 则中断禁用,为1 则中断启用。对中断使能寄存器写1 则开启中断,写0 无效。
中断优先级设置寄存器GICD_IPRIORITYRn
与中断使能寄存器一样GICD_IPRIORITYRn 是一组寄存器,根据表54‑1 可知组寄存器位于0x400-0x7F8 偏移地址处。中断优先级设置寄存器如下所示。
从上图可以看出每个中断标号占用8 位,数值越小中断优先级越高。下面介绍如何根据中断编号找到对应的中断优先级设置寄存器。假设中断编号为m,中断优先级寄存器为n,中断优先级设置位偏移为offset,则n = m /4。寄存器偏移地址为(0x400 + (4*n))。在寄存器中的偏移为offset= m%4。以m = 65 为例,n = 65/4 =16,所以中断优先级设置寄存器为GICD_IPRIORITYR16,offset(n) = 65%4 = 1,中断号65 对应的寄存器为GICD_IPRIORITYR16[8~15].
中断处理目标CPU 寄存器GICD_ITARGETSRn
根据之前讲解GIC 支持多大8 个CPU,在多核处理器中,中断可以通过该寄存器设置处理该中断的从CPU。例如中断A 发生了,通过该寄存器可以将中断A 发送到CPU0 或发送到CPU1。中断处理目标寄存器如下图所示。
每个中断对应8 位,位0~7 分别代表CPU0~CPU7 如下所示,一个中断也可以同时发送到多个CPU,例如中断A 对应的寄存器设置为0x03,则中断A 发生后将会发送到CPU0 和CPU1。
同样,中断处理目标CPU 寄存器GICD_ITARGETSRn 是一组寄存器,知道中断号经过简单计算之后就可以找到对应的寄存器,这里设中断编号为m,中断处理目标CPU 寄存器为n,中断处理目标CPU 寄存器位偏移为offset,则n = m /4。在寄存器中的偏移为offse t= m%4。以m = 65 为例,n = 65/4 =16,所以中断处理目标CPU 寄存器为GICD_ITARGETSR16,offset = 65%4 = 1,中断处理目标CPU 寄存器为GICD_ITARGETSR16[8~15]。
CPU 接口
CPU 接口简介
CPU 接口为链接到GIC 的处理器提供接口,与分发器类似它也提供了一些编程接口,我们可以通过CPU 接口实现以下功能:
- 开启或关闭向处理器发送中断请求.。
- 确认中断(acknowledging an interrupt)。
- 指示中断处理的完成。
- 为处理器设置中断优先级掩码。
- 定义处理器的抢占策略
- 确定挂起的中断请求中优先级最高的中断请求。
简单来说,CPU 接口可以开启或关闭发往CPU 的中断请求,CPU 中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到CPU。在任何时候CPU 都可以从其GICC_Hppir(CPU接口寄存器) 读取当前活动的最高优先级。
CPU 接口寄存器介绍
同GIC 分发器,GIC 的CPU 接口模块同样提供了一些编程接口,”编程接口”在这里就是一些寄存器,GPU 接口寄存器有很多,我们只介绍几个常用的寄存器,其他寄存使用到时再详细介绍,CPU 接口寄存器列表如下表所示。
表CPU 接口寄存器
结合上表常用的CPU 接口寄存器介绍如下:
CPU 接口控制寄存器GICC_CTLR
中断优先掩码寄存器GICC_PMR
在上一小节我们讲解了GIC 分发器的中断优先级设置寄存器GICD_IPRIORITYRn,每个中断占8 位。这里的中断优先级掩码寄存器GICC_PMR 用8 位代表一个中断阈值。高于这个优先级的中断才能被送到CPU。GICC_PMR 寄存器如下所示。
从上图可以看出GICC_PMR 寄存器后8 位(0~7) 用于设置一个优先级, 它的格式与
GICD_IPRIORITYR 寄存器相同。设置生效后高于此优先级的中断才能发送到CPU。需要注意的是8 位寄存器只有高4 位有效。与STM32 一样,这四位还将分为”抢占优先级”和”子优先级”。讲解优先级分组时再详细介绍。
中断优先级分组寄存器GICC_BPR
中断优先级分组寄存器用于将8 位的优先级分成两部分,一部分表示抢占优先级另外一部分表示自优先级,这和STM32 的中断优先级分组相同。GICC_BPR 寄存器如下所示。
中断优先级分组寄存器的后三位用于设置中断优先级分组,如下表所示。
表中断优先级分组
每个中断拥有8 为中断优先级设置位,但是只有高4 位有效,所以表54‑3 中GICC_BPR [2:0] 设置为1 到3 是相同的,即只有16 级抢占优先级没有子优先级。
中断确认寄存器GICC_IAR
中断确认寄存器GICC_IAR 保存当前挂起的最高优先级中断,寄存器描述如下图所示。
GICC_IAR 寄存器共有两个字段,CPUID[10:12] 保存请求中断的CPU ID。对于多核的CPU 来说,在处理中断的时候会使用到该位保存的信息。interrupt ID[0:9] 用于记录当前挂起的最高优先级中断,读取该寄存器,如果结果是1023 则表示当前没有可用的中断,常见的几种情况如下所示:
(1) 在GIC 分发器中禁止了向CPU 发送中断请求。
(2) 在GIC 的CPU 接口中禁止了向CPU 发送中断请求。
(3) CPU 接口上没有挂起的中断或者挂起的中断优先级小于等于GICC_PMR 寄存器设定的优先级值。
CP15 协处理器
在上一小节的GIC 寄存器讲解部分我们只给出了”偏移地址”,GIC 的基地址保存在CP15 协处理器中。我们修改系统控制寄存器以及设置中断向量表地址都会用到CP15 协处理器。
CP15 协处理器简介
CP15 寄存器是一组寄存器编号为C0~c15。每一个寄存器根据寄存器操作码(opc1 和opc2)和CRm 又可分为多个寄存器,
以C0 为例,如下图所示。
从上图可以看出根据opc1、CRm、opc2 不同CRn(c0)寄存器分为了多个寄存器,我们修改c0寄存器时将会用到opc1、CRm、opc2,它们的含义如下表所示。(表格摘自Cortex-A7 Technical-ReferenceManua,Table 4-1,更准确的解释请参考官方原文)。
表协处理器寄存器说明
CP15 协处理器寄存器的访问
对于A7 内核,我们通常会在芯片的启动文件中用到CP15 协处理寄存器,第一处是系统复位中断服务函数开始处,这里通过CP15 修改系统控制寄存器,第二处是获取GIC 控制器的基地址。稍后我们将介绍着两处代码,首先我们先学习如何读、写CP15 协处理器寄存器。
CP15 寄存器只能使用MRC/MCR 寄存器进行读、写。
(1) 将CP15 寄存器(c0~c15)的值读取到通用寄存器(r0 ~ r12)。
mrc {cond} p15, <Opcode_1>, , , , <Opcode_2>
(2) 将通用寄存器(r0~r12) 的值写回到CP15 寄存器(c0 ~ c15)
mcr {cond} p15, <Opcode_1>, , , , <Opcode_2>
CP15 寄存器读、写指令说明如下:
- cond:指令执行的条件码,忽略则表示无条件执行命令。
- Opcode_1:寄存器操作码1 ,对应Op1 选项。
- Rd:通用寄存器,当为mrc 时,用于保存从CP15 寄存器读取得到的数据。当为mcr 时,用于保存将要写入CP15 寄存器的数据。
- CRn:要读、写的CP15 寄存器(c0~c15),对应的CRn 选项。
- CRm:寄存器从编号,对应CRm 选项。
- Opcode_2:寄存器操作码2,对应的Op2 选项。
CP15 读、写实例说明
这里我们参考NXP 同为A7 核处理器的裸机启动代码讲解,代码里有两处使用到了CP15 寄存器,包括本小节要使用的GIC 基地址。说明如下:
复位中断服务函数中修改系统控制寄存器
通常情况下系统刚刚启动时为防止cache、中断、mmu 对初始化造成不必要的影响,需要在复位中断服务函数中暂时关闭这些功能,如下所示。
列表1: 裸机复位中断处理代码
Reset_Handler:
cpsid i /* Mask interrupts */
/* Reset SCTlr Settings */
mrc p15, 0, r0, c1, c0, 0 /* Read CP15 System Control register */
bic r0, r0, #(0x1 << 12) /* Clear I bit 12 to disable I Cache */
bic r0, r0, #(0x1 << 2) /* Clear C bit 2 to disable D Cache */
bic r0, r0, #0x2 /* Clear A bit 1 to disable strict alignment */
bic r0, r0, #(0x1 << 11) /*Clear Z bit 11 to disable branchprediction */
bic r0, r0, #0x1 /* Clear M bit 0 to disable MMU */
mcr p15, 0, r0,c1,c0,0 /* Write value back to CP15 System Controlregister */
...
结合以上代码,我们只看”mrc p15, 0, r0, c1, c0, 0”,不难看出,这里读取的CP15 标号为c1 的寄存器,该寄存器介绍如下图所示。
结合”mrc {cond} p15, <Opcode_1>, , , , <Opcode_2> “指令不难看出这里就是读取的SCTLR(系统控制寄存器)。
- 第11 行:“mcr p15, 0, r0,c1,c0,0”将修改过的r0 寄存器值写入到系统控制寄存器。
在IRQ 中断服务函数中获取GIC 控制器基地址
GIC 基地址获取相关代码如下所示
列表2: 获取GIC 基地址
/******************* 第三部分******************************/
MRC P15, 4, r1, C15, C0, 0 /* Get GIC base address */
ADD r1, r1, #0x2000 /* r1: GICC base address */
LDR r0, [r1, #0xC] /* r0: IAR */
对比”mrc {cond} p15, <Opcode_1>, , , , <Opcode_2> “指令格式可知,CRn、CRm、Opcode_1、Opcode_2 分别为c15、c0、4、0。C15 寄存器介绍如下图所示。
结合上图可知”MRC P15, 4, r1, C15, C0, 0”读取的是CBAR 寄存器。GIC 基地址保存在CBAR 寄存器中。
ARM 异常类型(中断向量表)
在前面我们知道了CP15 协处理器协调系统的启动过程,那么在芯片启动中,还需要对中断进行管理。下面我们来了解一些具体的中断内容。
查找一个芯片有哪些类型的中断,最简单的方法是查看启动文件,这点和STM32 单片机一样。当然也可以学习Cortex-A7 内核的手册。最简单的方法还是直接百度ARM 体系结构学习。
这里我们简单参考一款A7 内核的芯片的启动文件。启动代码的中断向量表部分代码如下所示,其他部分省略。
列表3: 启动代码
__vector_table
ARM
LDR PC, Reset_Word ; Reset
LDR PC, Undefined_Word ; Undefined instructions
LDR PC, SVC_Word ; Supervisor Call
LDR PC, PrefAbort_Word ; Prefetch abort
LDR PC, DataAbort_Word ; Data abort
DCD 0 ; RESERVED
LDR PC, IRQ_Word ; IRQ interrupt
LDR PC, FIQ_Word ; FIQ interrupt
DATA
Reset_Word DCD __iar_program_start
Undefined_Word DCD Undefined_Handler
SVC_Word DCD SVC_Handler
PrefAbort_Word DCD PrefAbort_Handler
DataAbort_Word DCD DataAbort_Handler
IRQ_Word DCD IRQ_Handler
FIQ_Word DCD FIQ_Handler
...
__iar_program_start
CPSID I ; Mask interrupts
; Reset SCTLR Settings
MRC P15, 0, R0, C1, C0, 0 ; Read CP15 System Control register
BIC R0, R0, #(0x1 << 12) ; Clear I bit 12 to disable I Cache
BIC R0, R0, #(0x1 << 2) ; Clear C bit 2 to disable D Cache
BIC R0, R0, #0x2 ; Clear A bit 1 to disable strict alignment
BIC R0, R0, #(0x1 << 11) ; Clear Z bit 11 to disable branch prediction
BIC R0, R0, #0x1 ; Clear M bit 0 to disable MMU
; Write value back to CP15 System Control register
MCR P15, 0, R0, C1, C0, 0
; Set up stack for IRQ, System/User and Supervisor Modes
; Enter IRQ mode
CPS #0x12
LDR SP, =SFE(ISTACK) ; Set up IRQ handler stack
; Enter System mode
CPS #0x1F
LDR SP, =SFE(CSTACK) ; Set up System/User Mode stack
; Enter Supervisor mode
CPS #0x13
LDR SP, =SFE(CSTACK) ; Set up Supervisor Mode stack
LDR R0, =SystemInit
BLX R0
CPSIE I ; Unmask interrupts
; Application runs in Supervisor mode
LDR R0, =__cmain
BX R0
PUBWEAK Undefined_Handler
PUBWEAK SVC_Handler
PUBWEAK PrefAbort_Handler
PUBWEAK DataAbort_Handler
PUBWEAK IRQ_Handler
PUBWEAK FIQ_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
EXTERN SystemIrqHandler
ARM
Undefined_Handler
B . ; Undefined instruction at address LR-Off \
(Off=4 in ARM mode and Off=2 in THUMB mode)
SVC_Handler
B . ; Supervisor call from Address LR
PrefAbort_Handler
B . ; Prefetch instruction abort at address LR-4
DataAbort_Handler
B . ; Load data abort at instruction address LR-8
IRQ_Handler
PUSH {LR} ; Save return address+4
PUSH {R0-R3, R12} ; Push caller save registers
MRS R0, SPSR ; Save SPRS to allow interrupt reentry
PUSH {R0}
MRC P15, 4, R1, C15, C0, 0 ; Get GIC base address
ADD R1, R1, #0x2000 ; R1: GICC base address
LDR R0, [R1, #0xC] ; R0: IAR
PUSH {R0, R1}
CPS #0x13 ; Change to Supervisor mode to allow interrupt reentry
PUSH {LR} ; Save Supervisor LR
LDR R2, =SystemIrqHandler
BLX R2 ; Call SystemIrqHandler with param IAR
POP {LR}
CPS #0x12 ; Back to IRQ mode
POP {R0, R1}
STR R0, [R1, #0x10] ; Now IRQ handler finished: write to EOIR
POP {R0}
MSR SPSR_CXSF, R0
POP {R0-R3, R12}
POP {LR}
SUBS PC, LR, #4
FIQ_Handler
B . ; Unexpected FIQ
END
我们不具体讲解汇编启动文件实现,仅仅从中提取我们需要的信息。
- 第1-10 行:这是我们要找的中断向量表,从这部分可知这个”表”共有8 项,除去一个保
留项(RESIVED)剩余7 个有效项。各项介绍如下:
– Res(reset) 复位中断,即系统上电或者硬件复位,根据之前讲解系统复位后默认运行在
SVC(特权模式)模式,我们裸机默认工作在该模式。
– Undefined_Word(未定义指令异常中断),如果CPU 检测到无法识别的指令时会进入未定义指令异常中断。这种情况下系统已经无法继续运行,只能通过硬件复位或者看门狗复位系统。
– Supervisor Call(系统调用中断),这种中断用于带linux 操作系统的情况,Linux 内核(即驱动程序)运行在SVC(特权模式),而Linux 应用程序运行在usr 模式。应用程序中如果需要调用驱动程序,就需要首先通过系统调用中断切换到SVC(特权模式),即我们常说的从”用户(应用)空间”切换到”内核空间”。
– Prefetch abort(指令预取失败中断),这种中断的解释就是它的名字。在CPU 执行当前指令时会”预取”下一个要执行的指令。如果”取指”失败就会进入该中断。CPU 无
法获取指令,所以这种情况下可以认为系统”挂了”。
– Data abort(数据访问终止中断),同样这种中断的解释就是它的名字。CPU 读取数据
终止,就是说系统读数据错误、读不到数据,所以这种中断后系统也是不正常的。
– IRQ(中断)与FIQ(快速中断),IRQ 与FIQ 稍微复杂,简单理解理解为我们常用的外设中断(串口中断、DMA 中断、外部中断等等)都将经过IRQ 或FIQ 传送到CPU。稍后将会详细介绍IRQ 与FIQ。
-
第14-20 行:设置中断向量表,以第15 行为例,使用”DCD”伪指令将”Reset_Word”(复位中断)跳转地址设置为”__iar_program_start”即复位中断发生后将会跳转到”__iar_program_start”位置执行即第三部分。
-
第24-68 行:程序入口,复位中断发生后程序将会跳转到这里执行,这里是程序的入口。具体代码我们暂时不关心。
-
第70-81 行:从上到下依次为Undefined_Word(未定义指令异常中断)、Supervisor Call(系统调用中断)、Prefetch abort(指令预取失败中断)、Data abort(数据访问终止中断)的中断跳转地址,可以看到他们都会跳转到”B .”即死循环,程序将会卡死在这里。
-
第83-114 行:IRQ(中断),IRQ 中断发生后程序将会跳转到这里执行,这里是后面小节讲解的重点,这里暂时跳过。
-
第116-119 行:FIQ(快速中断),裸机程序只使用了IRQ,所以这里将FIQ 中断执行代码设置为(B .)即死循环。
这里我们重点理解前面提到的7 种异常模式以及其他中断的上报处理流程,这在ARM 体系结构中是非常重要的。
共享中断实现
上一小节介绍了中断类型,我们可以发现虽然中断向量表中定义了7 个中断,但是其中5 个中断发生后直接进入死循环,仅剩下复位中断和IRQ 中断。我们常用的外设中断如串口中断、DMA中断等等怎么实现呢?很容易猜到是通过IRQ,所有外设中断(共享中断)发生时都会进入IRQ中断,在IRQ 中断处理函数中进一步区分具体的外设中断。
我们可以从GIC 功能框图中的到印证。如下图所示。
结合上图介绍,无论是SPI 中断、PPI 中断、还是SGI 中断,它们都链接到了CPU 接口,而CPU接口接口输出到CPU 的只有两个FIQ 和IRQ(VFIQ 和VIRQ 这里没有用到,暂时忽略)。中断标号为0~1019 的任意一个中断发生后CPU 都会跳转到FIQ 或IRQ 中断服务函数去执行。在前面的代码中默认关闭了FIQ,只使用了IRQ。
根据之前讲解,GIC 控制器为我们提供了两个编程接口,分别为分发器寄存器和CPU 接口寄存器。
下面将讲解一款A7 内核的芯片共享中断的底层处理实现,大家能理解即可。
列表4: IRQ 共享中断实现
IRQ_Handler:
push {lr} /* Save return address+4 */
push {r0-r3, r12} /* Push caller save registers */
MRS r0, spsr /* Save SPRS to allow interrupt reentry */
push {r0}
MRC P15, 4, r1, C15, C0, 0 /* Get GIC base address */
ADD r1, r1, #0x2000 /* r1: GICC base address */
LDR r0, [r1, #0xC] /* r0: IAR */
push {r0, r1}
CPS #0x13 /* Change to Supervisor mode to allow interrupt reentry */
push {lr} /* Save Supervisor lr */
LDR r2, =SystemIrqHandler
BLX r2 /* Call SystemIrqHandler with param GCC */
POP {lr}
CPS #0x12 /* Back to IRQ mode */
POP {r0, r1}
STR r0, [r1, #0x10] /* Now IRQ handler finished: write to EOIR */
POP {r0}
MSR spsr_cxsf, r0
POP {r0-r3, r12}
POP {lr}
SUBS pc, lr, #4
.size IRQ_Handler, . - IRQ_Handler
.align 2
.arm
.weak FIQ_Handler
.type FIQ_Handler, %function
结合代码讲解如下:
同函数调用类似,进入中断函数之前要将程序当前的运行状态保存保存到”栈”中。中断执行完成后能够恢复进入中断之前的状态。
- 第3、4 行:保存当前状态,同函数调用类似,进入中断函数之前要将程序当前的运行状态保存保存到”栈”中。中断执行完成后能够恢复进入中断之前的状态。
- 指令”push {lr}”将lr 寄存器”入栈”,根据之前讲解当进行函数调用或发生中断时sp(程序计数寄存器,保存当前程序执行位置(Thumb)加4) 的值会自动保存到lr 寄存器中。lr 的值将做为函数会中断返回的地址。
- 指令”push {r0-r3, r12}”将r0~r3 寄存器以及r12 寄存器”入栈”。r0~r3 和r12 是通用寄存
器,在函数中它们可以用于任何用途,但是在函数调用或函数返回时它们用于传入函数参数以及传出返回值等等。中断可能发生在程序的任意时刻,所以进入中断之前也要保存这些信息。 - 第6、7 行:于保存spsr(备份程序状态寄存器)。SPRS 是特殊功能寄存器不能直接访问,指令”MRS r0, spsr”用于将spsr 寄存器的值保存到r0 寄存器,然后使用”push {r0}”指令将spsr 寄存器的值保存到”栈”中。
- 第9-11 行:获取GIC 基地址以及GICC_IAR 寄存器的值。这部分代码使用到了CP15 协处理器,在54.2 CP15 协处理器章节已经介绍,这里不再赘述,简单说明各指令的作用。指令”MRC P15, 4, r1, C15, C0, 0”将GIC 基地址保存到r1 寄存器中。指令”ADD r1, r1,#0x2000”在GIC 基地址基础上增加0x2000,得到GICC(GIC 的cpu 接口寄存器基地址) 基地址。指令”LDR r0, [r1, #0xC]”读取GICC_IAR 寄存器的值到r0 寄存器。
- 第13 行:将GICC 基地址和GICC_IAR 寄存器值入栈。第三部分代码将GICC 基地址保存在了r1 寄存器,将GICC_IAR 寄存器的值保存在了r0 寄存器,中断执行完成后我们还要用到这些内容,所以这里将他们”入栈”保存。
- 第15 行:切换到Supervisor 模式。
- 第17-20 行:跳转到”SystemIrqHandler”函数执行共享中断对应的中断服务函数。指令”push{lr}”保存当前的链接寄存器,即保存程序的返回地址。指令”LDR r2, =SystemIrqHandler”用于将函数”SystemIrqHandler”地址保存到r2 寄存器中。指令”BLX r2”是有返回的跳转,程序将会跳转到”SystemIrqHandler”函数执行。去掉不必要的条件编译后如下所示。
列表5: systemIrqHandler 共享中断处理函数
__attribute__((weak)) void SystemIrqHandler(uint32_t giccIar)
{
uint32_t intNum = giccIar & 0x3FFUL;
/* Spurious interrupt ID or Wrong interrupt number */
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{
return;
}
irqNesting++;
/* Now call the real irq handler for intNum */
irqTable[intNum].irqHandler(giccIar, irqTable[intNum].userParam);
irqNesting--;
}
结合代码,各部分讲解如下:
-
第1 行:SystemIrqHandler 函数有一个入口参数”giccIar”,它是GICC_IAR 寄存器的值。在前面的代码中,我们将GICC_IAR 寄存器的值保存到了R0 寄存器,跳转到SystemIrqHandler函数之后R0 寄存器的值作为函数参数。
-
第3 行:获取中断的中断编号。中断编号保存在GICC_IAR 寄存器的后10 位(0~9)如下所示。
- 第6-9 行:判断中断标号是否有效。根据之前讲解如果中断无效,则读取得到的中断号为1023。”NUMBER_OF_INT_VECTORS”是芯片支持的最大中断号加一,大于等于这个值的中断编号也被认为无效。
那么既然获取到了中断标号,我们就知道发生了什么中断了,再根据中断标号找到对应的中断处理函数,就完成了中断的调用流程。
GIC 知识小结
GIC 可以说是ARM 芯片对中断管理的得力助手,它有两部分实现:分发器、CPU 接口。
分发器负责收集来自于外设或芯片内部的各种中断事件,并基于它们的中断特性(优先级、是否使能等等)对中断进行分发处理。
在中断分发后,就由CPU 接口接管接下来的工作,在CPU 接口中,中断被统一派发到IRQ 或FIQ,触发IRQ 或FIQ 中断,在IRQ 或FIQ 中断服务函数中,通过CP15 协处理器获取到GIC 中储存的中断分发信息,并基于这些信息,再去执行如DMA 中断、串口中断等的对应中断服务函数。这样一个简单的中断触发流程就走完了。
设备树中的中断信息以及中断基本函数介绍
在前面,我们通过GIC 控制器的背景知识讲解,以及部分汇编代码对中断过程的处理,知道了中断的使用过程。那么前面说到的这一些底层细节,在Linux 系统中已经替我们封装好了大部分的工作内容,我们需要做的只是简单地在这个框架下使用。
MP157 的中断来源有四类,分别是GIC、EXTI、PWR、GPIO,它们的关系在底层是并行的,各自负责一块中断功能,比如GIC 负责各项通用外设的中断比如串口、DMA,EXTI、PWR 则主要负责那些能将系统从休眠模式唤醒的中断管理,至于GPIO 则是GPIO 电平检测的中断了。
虽然这些中断分为了四类,但是它们的实现是各有交叉的,也就是说使用GIC 中断管理器,也可以管理GPIO 触发的中断。
大家参考下图所示:
相关内容参考:
《ST Interrupt overview》
设备树中的中断相关内容
让我们先来了解一下设备树是如何描述整个中断系统信息的。
GIC 中断控制器
打开/arch/arm/boot/dts/ 目录下的stm32mp157c.dtsi 设备树文件,找到“interrupt-controller”节点,如下所示。
列表6: 中断interrupt-controller 节点
intc:interrupt-controller@a0021000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0xa0021000 0x1000>,
<0xa0022000 0x2000>;
};
- compatible:compatible 属性用于平台设备驱动的匹配
- reg:reg 指定中断控制器相关寄存器的地址及大小
- interrupt-controller:声明该设备树节点是一个中断控制器。
- #interrupt-cells :指定使用该中断控制器的节点要用几个cells 来描述一个中断,可理解为用几个参数来描述一个中断信息。在这里的意思是在intc 节点的子节点将用3 个参数来描述中断。
学过前面内容的同学们想必对GIC 中断控制器并不陌生,GIC 架构分为了:分发器(Distributor)和CPU 接口(CPU Interface), 上面设备树节点就是用来描述整个GIC 控制器的。
一个GIC 中断控制器的使用实例,RCC 外设:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges;
rcc: rcc@50000000 {
compatible = "st,stm32mp1-rcc", "syscon";
reg = <0x50000000 0x1000>;
#clock-cells = <1>;
#reset-cells = <1>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
};
};
RCC 外设是soc 下的子节点,soc 指定了interrupt-parent 为intc。那么RCC 外设也就是使用了GIC控制器中断控制器,并用interrupts 描述了它使用的资源。
- interrupts:具体的中断描述信息,在该节点使用的中断控制器intc,规定了使用三个cells来描述子控制器的信息。三个参数表示的含义如下:
第一个参数用于指定中断类型,在GIC 的中断的类型有三种(SPI 共享中断、PPI 私有中断、SGI 软件中断),我们使用的外部中断均属于SPI 中断类型。
第二个参数用于设定中断编号,范围和第一个参数有关。PPI 中断范围是[0-15],SPI 中断范围是[0-256]。
第三个参数指定中断触发方式,参数是一个u32 类型,其中后四位[0-3] 用于设置中断触发
类型。每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使
用,如下所示:
列表7: 中断触发方式设置
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
其中第三个参数的[8-15] 位,在PPI 中断中还用于设置“CPU 屏蔽”。在多核系统中这8 位用于设置PPI 中断发送到那个CPU, 一位代表一个CPU, 为1 则将PPI 中断发送到CPU0, 否则屏蔽。
如下示例:
timer {
compatible = "arm,armv7-timer";
interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>;
interrupt-parent = <&intc>;
always-on;
};
GIC 外设管理的中断非常多,使用起来相比于GPIO 中断控制器也要麻烦,所以我们本节的按键实验就不使用GIC 中断控制器作为按键的中断父节点,我们使用GPIO 中断控制器。
EXTI 中断控制器
GPIO 中断控制器是在EXTI 中断控制器节点上实现的,我们来看看EXTI 中断控制器。
在stm32mp157c.dtsi 文件中直接搜索节点标签“exti”即可找到“exti 中断控制器”
列表8: exti 中断控制器
exti:interrupt-controller@5000d000 {
compatible = "st,stm32mp1-exti", "syscon";
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x5000d000 0x400>;
hwlocks = <&hsem 1>;
/* exti_pwr is an extra interrupt controller used for
* EXTI 55 to 60. It's mapped on pwr interrupt
* controller.
*/
exti_pwr: exti-pwr {
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&pwr>;
st,irq-number = <6>;
};
};
结合以上代码介绍如下:
- interrupt-controller:声明该设备树节点是一个中断控制器,只要是中断控制器都要用该标签声明。
- #interrupt-cells:用于规定该节点的中断控制器将使用2 个参数来描述控制器的信息。
GPIO 使用的中断控制器
在stm32mp157-pinctrl.dtsi 文件中可找到GPIO 使用的中断控制器,如下所示:
soc {
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0>;
pins-are-numbered;
gpiob: gpio@50003000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x1000 0x400>;
clocks = <&rcc GPIOB>;
st,bank-name = "GPIOB";
status = "disabled";
};
};
soc 节点即片上外设“总节点”,翻阅源码可以发现该节点很长,我们使用的gpio 外设包含soc 的子节点pinctrl 里面。
GPIO 也可作为中断控制器,其父节点pinctrl 声明了它们的中断控制器是<&exti> 节点,此节点在前面有展示。
soc 节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键,使用的是gpiob13,所以这里以gpiob 为例介绍。在stm32mp157-pinctrl.dtsi 文件中直接搜索gpiob,就可找到gpiob 对应的设备树节点。
在gpiob 节点中描述了一些属性:
- interrupt-controller:声明该节点是一个中断控制器
- #interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。
按键设备树节点
这里我们将设备树编写成插件的形式,方便使用,如想将节点信息直接添加到主设备树中,直接拷贝设备树插件的节点内容到主设备树即可。
按键中断的设备树插件在下文中给出。
中断相关函数
编写驱动之前,我们需要了解中断的使用接口函数。
request_irq 中断注册函数
列表9: 申请中断
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev)
参数:
-
irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间
中它代表一个唯一的中断编号。 -
handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。
-
flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:
列表10: 中断触发方式
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
#define IRQF_SHARED 0x00000080 ---------