[ARM裸机开发] GPIO中断
中断是指 CPU 在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中止当前程序的执行,而转去为事件服务,待服务完毕,再返回到暂停处继续执行原来的程序。
中断系统在 51、STM32 单片机上都是存在的,并且都有中断向量表、中断控制器、中断服务函数这些概念。本篇文章将借 Cortex-A7 上的 GPIO 中断功能,学习了解 Cortex-A7 的中断系统。
1. Cortex-A7 中断系统
在 Cortex-A7 上也一样,中断处理是通过中断向量表这块内存区域来控制的:当发生中断时,ARM 内核执行表内的跳转指令。默认情况下,中断向量表存储在 0x00 至 0x1c 地址处。
需要注意:exception 译作异常,interrupt 译作中断。有时候两者混用,比如一般都称作“中断向量表”。
如图 1 所示,Cortex-A 有 7 种异常,可以将它们分为如下的四大类:
Interrupts : 有两种此类型的中断,分别为 IRQ interrupt 和 FIQ interrupt。FIQ 比 IRQ 优先级高。
Aborts : 终止可在指令预取失败(对应 Prefetch Abort)和数据访问失败(对应 Data Abort)时发生。终止可能来自外部存储系统,当指定的地址不对应实际存储器时会给出错误响应;终止也可能由内核的内存管理单元(MMU)产生,操作系统可以使用 MMU 的终止为应用程序动态分配内存。
Reset : CPU 复位就会产生复位中断。它是优先级最高的中断,不能被屏蔽。相关中断服务会进行初始化工作。
Exception generating instructions : 执行两类指令会产生异常:一类指令用于从运行在更高特权级别的软件请求服务(对应于 Supervisor Call);第二类是 CPU 无法识别的、没有定义的指令(对应于 UNDEFINED instruction)。
本篇文章暂只关注 Interrupts 下的 IRQ interrupt,对应偏移 0x18。
在了解了上述知识之后,可以先“改造”一下之前的 start.s 文件:设置中断向量表,每一个条目的内容为跳转到相应的处理函数中。Reset 中断的处理函数暂且还是原来的初始化内容,其余的中断处理函数暂设置为死循环。
- .global _start
- _start:
- ldr pc,=ResetHandler
- ldr pc,=UndefinedInstructionHandler
- ldr pc,=SupervisorCallHandler
- ldr pc,=PrefetchAbortHandler
- ldr pc,=DataAbortHandler
- ldr pc,=NotUsedHandler
- ldr pc,=IrqInterruptHandler
- ldr pc,=FiqInterruptHandler
- ResetHandler:
- mrs r0, cpsr
- bic r0, r0, #0x1f
- orr r0, r0, #0x13
- msr cpsr, r0
- ldr sp, =0x80200000
- b main
- UndefinedInstructionHandler:
- ldr r0,=UndefinedInstructionHandler
- bx r0
- SupervisorCallHandler:
- ldr r0,=SupervisorCallHandler
- bx r0
- PrefetchAbortHandler:
- ldr r0,=PrefetchAbortHandler
- bx r0
- DataAbortHandler:
- ldr r0,=DataAbortHandler
- bx r0
- NotUsedHandler:
- ldr r0,=NotUsedHandler
- bx r0
- IrqInterruptHandler:
- ldr r0,=IrqInterruptHandler
- bx r0
- FiqInterruptHandler:
- ldr r0,=FiqInterruptHandler
- bx r0
If a valid MOV or MVN instruction cannot be used, or if the label_expr syntax is used, the assembler places the constant in a literal pool and generates a PC-relative LDR instruction that reads the constant from the literal pool.
1.1 GIC 通用中断控制器
在上节了解到,异常类型很少,只有 7 种,那么诸多外设的中断如何识别和分配处理的呢?“文章” 出在 Cortex-A 的中断控制器上,Cortex-A 使用 v2 版本的 GIC(Generic Interrupt Controller,通用中断控制器)。GIC 抽象层面的示意图如图 2 所示,中断控制器负责接收外部中断,然后将收到的信号发送给 ARM 内核,有 4 种信号可供汇报情况:VFIQ、VIRQ、FIQ、IRQ。
VFIQ 和 VIRQ 是针对虚拟化的,暂不考虑。并且本篇文章也暂不考虑 FIQ,所以中断控制最终就上报给 ARM 内核一个 IRQ 信号,大大分散了工作量:通过 IRQ 信号,ARM 内核知道发生了 IRQ 中断,紧接着就可以直接从 GIC 那边读取经过预先处理好的中断信息,从而知道具体哪个外设产生了中断。这和代码的模块化处理是类似的,把中断管理的工作都交给 GIC,ARM 内核只负责接收处理好的通知和获取处理好的结果。
接下来把图 2 中 GIC 这个“黑盒”再放大一些细节,整体的逻辑块如图 3 所示。从图 3 中可以看出 GIC 分为两个逻辑块,Distributor(分发器) 和 CPU Interface(CPU 接口):
Distributor : 系统中的所有中断源都连接到分发器接口。分发器有寄存器来控制单个中断的属性,比如优先级、状态、安全性、路由信息和使能状态。分发器与连接的 CPU 接口确定哪个中断将被转发到 ARM 核心。
CPU Interface : ARM 内核通过 CPU 接口接收中断。转发到某一 ARM 内核的中断状态,CPU 接口可以托管寄存器来屏蔽、识别和控制。系统中的每个 ARM 内核都有单独的 CPU 接口。
从图 3 中还可以看到中断源也进行了分组,并且给定了范围。中断在软件层面由一个数字标识,称为中断号。一个中断号唯一对应一个中断源。软件中可以使用中断号来识别中断的来源,并调用相应的处理函数。中断来源被分为 3 种类型:
Software Generated Interrupt (SGI) : 此类中断是通过软件写入专用分发器寄存器显式生成的。它最常用于 ARM 内核之间的通信。SGI 可以针对所有 ARM 内核,也可以只针对系统中选择的一组 ARM 内核。中断号 0 - 15 是为此保留的。用于通信的确切中断号是由软件决定的。
Private Peripheral Interrupt (PPI) : 此类中断是某单一 ARM 内核的私有外设生成的。中断号 16 - 31 是为此保留的。它们识别此 ARM 内核私有的中断源,并且独立于其他内核上相同的中断源,比如每个 ARM 内核上的定时器。
Shared Peripheral Interrupt (SPI) : 此类中断是由可以通过 GIC 路由到多个 ARM 内核的外设产生的,即是各个内核所共享的。中断号 32 - 1020 是为此保留的。
1.2 CP15 协处理器
在说明 GIC 相关寄存器之前,要首先介绍一下 CP15 协处理器。因为 CP15 协处理器一般用于存储系统管理,但是在中断中也会用到:可以使能或禁止 MMU、I/D Cache 等;可以设置中断向量偏移;可以获取 GIC 寄存器组的基地址。
1.2.1 MRC/MCR 指令MRC 指令会使协处理器把一个值转移到 ARM 内核寄存器或者转移到条件标志。MRC 是一个通用的协处理器指令,其中 opc1、opc2、CRn 和 CRm 字段可以由协处理指令设计者自由使用。但是协处理 CP8-CP15 是留给 ARM 使用的,当协处理器在 p8-p15 范围内时,手册里定义了有效的指令。MRC 指令的格式如下:
- MRC<c> <coproc>, <opc1>, <Rt>, <CRn>, <CRm>{, <opc2>}
c : 指令执行的条件码。
coproc : 协处理的名字。通用协处理的名字是 p0-p15。
opc1 : 协处理器特定的操作码,范围为 0 到 7。
Rt : 目的 ARM 寄存器。
CRn : 包含第一个操作数的协处理器寄存器。
CRm : 附加的源或目的协处理器寄存器。
opc2 : 协处理器特定的操作码,范围为 0 到 7。如果省略,则视为 0。
有序集合 {CRn, opc1, CRm, opc2} 决定了寄存器的顺序。
简单的说,MRC 指令是从协处理器读相关寄存器。而 MCR 指令相反,是写入协处理器相关寄存器,指令格式与 MRC 一样,只是含义中的源和目的相反。
1.2.2 System Control RegisterSystem Control Register(SCTLR) 寄存器提供系统的顶层控制。如图 4 所示,SCTLR 寄存器对应的序列为 {c1, 0, c0, 0}。
如图 5 所示,SCTLR 寄存器的位定义众多,这边挑之后章节代码里需要用到的位进行介绍:
V (bit13) : 向量位。该位选择中断向量的基地址:为 0 时,代表一般中断向量,基地址为 0x00000000。软件可以使用 VBAR 寄存器重新映射这个基地址;为 1 时,代表高地址中断向量表,基地址为 0xFFFF0000,此时基地址不能被重映射。
I (bit12) : 指令缓存使能位。该位能控制全局的指令缓存使能:为 0 时,禁止指令缓存;为 1 时,使能指令缓存。
Z (bit11) : 分支预测使能位。RAO/WI(Read-As-One, Writes Ignored)。该位总是在 MMU 启用时启用。
SW (bit10) : SWP 和 SWPB 使能位。该位控制是否允许使用 SWP 和 SWPB 指令:为 0 时,表示 SWP 和 SWPB 指令未定义;为 1 时,表示 SWP 和 SWPB 指令能正常使用。
C (bit2) : 缓存使能位。该位是数据缓存和缓存一致性的全局使能位:为 0 时,禁用数据缓存和缓存一致性;为 1 时,使能数据缓存和缓存一致性。
A (bit1) : 对齐位。该位是内存对齐检查使能位:为 0 时,禁用对齐检查;为 1 时,使能对齐检查。
M (bit0) : 地址转换使能位。该位是 MMU 第一级地址转化的全局使能位:为 0 时,禁用地址转换;为 1 时,使能地址转换。
1.2.3 Vector Base Address Register
Vector Base Address Register(VBAR) 寄存器,在未选择高地址中断向量表时,可以保存未进入 Monitor 或 Hyp 模式的中断向量表基地址。如图 6 所示,VBAR 寄存器对应的序列为 {c12, 0, c0, 0}。
图 7 显示了 VBAR 寄存器的位定义,可以看到只需要定义 5-31 位,0-4 位是保留位,但要求写时必须为 0。UNK/SBZP 是 unknown on reads, Should-Be-Zero-or-Preserved on writes 的缩写。
1.2.4 Configuration Base Address Register
Configuration Base Address Register(CBAR) 寄存器,用于保存通过内存映射方式(memory-mapped)定义的 GIC 寄存器组的基地址。如图 8 所示,VBAR 寄存器对应的序列为 {c15, 4, c0, 0}。
图 9 显示了 CBAR 寄存器的定义,初看稍微有些别扭:bit31-15 对应 PERIPHBASE[31:15] ,这是对应上的;但是 bit7-0 对应 PERIPHBASE[39:32];bit8-14 为保留位,描述为 UNK/SBZP,写时必须为 0。从图 10 的 GIC 内存映射中可以看到,偏移是基于 PERIPHBASE[31:15] 的,并且后续代码中没有对 CBAR 的内容做特殊处理,这边猜测 PERIPHBASE 的 39:32 和 14:0 位都为 0。
1.3 GIC 编程
在 1.1 节中介绍好 GIC 架构之后,穿插了 1.2 节的一个主要原因就是需要获取 GIC 寄存器组的基地址。知道了 GIC 寄存器组的基地址以及图 10 的 GIC 内存内存分布,就能操作 GIC 定义的各个寄存器。
图 11 是分发器端的寄存器内存映射,图 12 是 CPU 接口端的寄存器内存映射,可以看到定义的寄存器非常多,没有针对性的过一遍文档效率并不高。好在 i.MUX 提供的官方 SDK 包里面附带了 GIC 相关的操作函数,主要集中在 core_ca7.h 这个文件中。下面的记录思路主要是根据 SDK 里面的库函数内容,以此找到使用的寄存器说明,以理解代码的意图。
1.3.1 SDK - GIC_Init()
首先介绍是 SDK 包里面的 GIC_Init 函数。第 6 行:__get_CBAR 函数获取 CBAR 寄存器内容的方法已经在 1.2.4 中讲过,可见的确 39:32 和 14:0 位都为 0;GIC_Type 结构体完全依据图 10、图 11、图 12 中的内容。即 gic 变量就是 GIC 的寄存器组。
第 8 行:使用到了 GICD_TYPER 寄存器。GICD_TYPER 寄存器提供 GIC 的配置信息。GICD_TYPER 寄存器的 bit4:0 表示 GIC 支持的最大中断数,如果值为 N,那么中断的最大数目为 32*(N+1)。即 irqRegs 变量为有多少组中断,一组 32 个。
第 13-14 行:使用到了 GICD_ICENABLER 寄存器。GICD_ICENABLER 寄存器为 GIC 支持的每个中断提供一个 clear-enable 位,写 1 到 clear-enable 位将禁止相应的中断从分发器转发到 CPU 接口端。即禁止了所有中断转发。
第 17 和 20 行:使用到了 GICC_PMR 和 GICC_BPR 寄存器,这两个寄存器共同作用于优先级。GICC_PMR 寄存器提供中断优先级过滤器,只有优先级高于该寄存器值(优先级越高值越小)的中断才向处理器发出信号。如图 13 所示,代码中设置优先级为 32 个级别;如图 14 所示,代码中设置 Binary point 为 2。
优先级这块有点搞不清楚。代码开头注释提及了 group0,看文档 group0 的优先级比 group1 高。这边不知道如何都使用 group0。
图 14 中又出现了 Group priority,这边的 “Group” 应该和 group0 中的含义不同。注释中的 “No subpriority” 和文档描述对应不太上,推测应该是 GICC_PMR 使用了 5bit 作为优先级,而这 5bit 全部作为 Group priority,就没有多余的 bit 用作 Subpriority。
第 23 行:使用到了 GICD_CTLR 寄存器。GICD_CTLR 寄存器控制是否将挂起的中断从分发器转发到 CPU 接口端。代码中开启 group0 中断的分发。
第 26 行:使用到了 GICC_CTLR 寄存器。GICC_CTLR 寄存器控制是否从 CPU 接口端发送中断信号到 CPU。代码中开启 group0 中断的发送。
- /* For simplicity, we only use group0 of GIC */
- FORCEDINLINE __STATIC_INLINE void GIC_Init(void)
- {
- uint32_t i;
- uint32_t irqRegs;
- GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
- irqRegs = (gic->D_TYPER & 0x1FUL) + 1;
- /* On POR, all SPI is in group 0, level-sensitive and using 1-N model */
- /* Disable all PPI, SGI and SPI */
- for (i = 0; i < irqRegs; i++)
- gic->D_ICENABLER[i] = 0xFFFFFFFFUL;
- /* Make all interrupts have higher priority */
- gic->C_PMR = (0xFFUL << (8 - __GIC_PRIO_BITS)) & 0xFFUL;
- /* No subpriority, all priority level allows preemption */
- gic->C_BPR = 7 - __GIC_PRIO_BITS;
- /* Enable group0 distribution */
- gic->D_CTLR = 1UL;
- /* Enable group0 signaling */
- gic->C_CTLR = 1UL;
- }
1.3.2 SDK - GIC_EnableIRQ()
GIC_EnableIRQ 函数使能某一 IRQ 中断。函数里使用到了 GICD_ISENABLER 寄存器,布局和用法和 1.3.1 节中介绍的 GICD_ICENABLER 寄存器是类似的。
GICD_ISENABLER 和 GICD_ICENABLER 寄存器还有各自别的用途。GICD_ISENABLER 寄存器可以用于确认支持的中断:先关闭分发器到 CPU 接口端的中断转发,接着向 GICD_ISENABLER 里的各位写使能位,接着再读取,如果哪位还是使能,则代表这个中断存在。GICD_ICENABLER 寄存器可用于发现那些永久使能的中断:首先向 GICD_ICENABLER 里各位写禁止位,接着再读取,如果哪位没有禁止则代表相应的中断是永久使能的;需要特别注意的是,禁止所有中断后,如果还要使能某个中断的话,需要重新写入 GICD_ISENABLER 寄存器。
- FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
- {
- GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
- gic->D_ISENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
- }
1.3.3 GICC_IAR 寄存器
CPU 可以通过读取 GICC_IAR 寄存器以得到信号中断的中断号。同时,读取操作作为对中断的应答。GICC_IAR 寄存器的位定义如图 15 所示,看名字也很简单明了,包括 CPU 号和中断号。
1.3.4 GICC_EOIR 寄存器
CPU 通过写 GICC_EOIR 寄存器通知 GIC 的 CPU 接口端已经完成了指定的中断处理。GICC_EOIR 的位定义与图 15 的 GICC_IAR 位定义一致,并且写入 GICC_EOIR 寄存器的值必须是最近一次从 GICC_IAR 寄存器获取的值。
1.3.5 最简单的中断处理流程
说是 “最简单” 的原因是此处理流程不包含中断嵌套。流程如下:
1. IRQ 异常由外部硬件触发,此时 ARM 内核会自动执行如下几个步骤:将当前执行模式下的 PC 寄存器内容存储在 LR_IRQ 中;将 CPSR 寄存器复制到 SPSR_IRQ 中;更新 CPSR 内容进入 IRQ 模式,并且 I 位设置为屏蔽额外的 IRQ;PC 值设置为中断向量表里的条目。
2. 在中断向量表中的 IRQ 条目处的指令被执行。
3. 中断处理程序需要保存被中断程序的上下文,即需要将被处理程序损坏的寄存器压栈。当处理程序执行完毕后,将这些寄存器出栈。
4. 中断处理程序需要确认处理哪个中断源(1.3.3 GICC_IAR 寄存器),并调用适当的设备驱动程序。
5. 通过将 SPSR_IRQ 复制到 CPSR,并恢复之前保存的上下文,让内核准备切换到之前的执行状态。最后 PC 从 LR_IRQ 恢复。
2. GPIO 中断编程
在介绍完了第 1 章的诸多预备知识之后,终于可以开始 GPIO 中断实验的编程了。为了连贯性,我会在涉及到第 1 章里介绍的内容时,进行章节标出。这章的内容主要涉及两大块内容,一块是通用中断驱动的编写,第二块是 GPIO 打开中断的设置。
2.1 中断驱动
这节介绍通用中断驱动的编写,首先需要继续完善之前片段 1 中没有写完的内容:ResetHandler 和 IrqInterruptHandler。
ResetHandler 函数如下代码所示,在 1.3.5 节中可以了解到中断涉及了两个运行模式,而模式下的部分寄存器包括栈都是独立的,所以需要单独设置。同时还需要关闭一些可能额外引发中断的功能,比如 MMU,这部分对于 1.2.2 节的内容。
- ResetHandler:
- cpsid i /* disable irq */
- /* disable I/DCache and MMU */
- mrc p15, 0, r0, c1, c0, 0
- bic r0, r0, #(0x1 << 12)
- bic r0, r0, #(0x1 << 2)
- bic r0, r0, #0x2
- bic r0, r0, #(0x1 << 11)
- bic r0, r0, #0x1
- mcr p15, 0, r0, c1, c0, 0
- /* irq sp */
- mrs r0, cpsr
- bic r0, r0, #0x1f
- orr r0, r0, #0x12
- msr cpsr, r0
- ldr sp, =0x80600000
- /* sys sp */
- mrs r0, cpsr
- bic r0, r0, #0x1f
- orr r0, r0, #0x1f
- msr cpsr, r0
- ldr sp, =0x80400000
- /* svc sp */
- mrs r0, cpsr
- bic r0, r0, #0x1f
- orr r0, r0, #0x13
- msr cpsr, r0
- ldr sp, =0x80200000
- cpsie i /* enable irq */
- b main
重点是 IrqInterruptHandler 函数,整体思路和 1.3.5 节一致:通过 GICC_IAR 寄存器获得 CPU 号和中断号,然后传递给 C 语言写的中断函数,处理完毕后写 GICC_EOIR 寄存器。返回到原本模式是通过 SUBS 指令实现的,其中的后缀 S 表示将保存的 SPSR 重新写回 CPSR。
IrqInterruptHandler 函数里多了中断嵌套的想法,但是以下代码是否能支持中断嵌套感觉有待商榷,因为在 1.3.5 节中可以了解到 CPSR 的 I 位在产生中断时会进行设置为屏蔽,而以下代码中都没有将 I 位使能。
IrqInterruptHandler 函数中将具体的中断处理放在 SVC 模式下处理,感觉如果没有实现中断嵌套的话,直接放在 IRQ 模式下也是可以的。模式间转化需要注意寄存器是否共用,如图 16 所示,白底灰字的寄存器表示和 User 模式供用,墨蓝底色的寄存器是影子寄存器,各个模式下有单独的 “备份”。
- IrqInterruptHandler:
- push {lr} /* lr_irq */
- push {r0-r3, r12}
- mrs r0, spsr
- push {r0}
- mrc p15, 4, r1, c15, c0, 0 /* gic base addr */
- add r1, r1, #0x2000 /* cpu interface base addr */
- ldr r0, [r1, #0xc] /* GICC_IAR */
- push {r0, r1}
- cps #0x13
- push {lr} /* lr_svc */
- ldr r2, =CIrqHandler
- blx r2
- pop {lr}
- cps #0x12
- pop {r0, r1}
- str r0, [r1, #0x10] /* GICC_EOIR */
- pop {r0}
- msr spsr_cxsf, r0
- pop {r0-r3, r12}
- pop {lr}
- subs pc, lr, #4
C 语言部分写的中断处理函数如下所示,并不复杂,就是根据中断号,在之前初始化好的函数指针数组里执行相应的中断处理函数即可。
- void CIrqHandler(unsigned int giccIar)
- {
- uint32_t intNum = giccIar & 0x3ff;
- if (intNum >= NUMBER_OF_INT_VECTORS)
- return;
- irqNesting++;
- irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
- irqNesting--;
- }
中断驱动部分最后剩下的内容就是初始化函数,见如下代码中的 int_init 函数。int_init 函数首先使用 SDK 包里面的 GIC_Init 函数初始化 GIC,这部分已经在 1.3.1 节中介绍过了。接着重映射中断向量表的基地址,这在 1.2.3 节中已经介绍过。最后是初始化一下各个中断处理函数,一开始全都设置为默认的 default_irqhandler 函数。具体中断处理函数等到要使用到的时候,再通过 system_irqtable_register 函数 “注册”。
- static sys_irq_info_t irqTable[NUMBER_OF_INT_VECTORS];
- void default_irqhandler(unsigned int giccIar, void* param)
- {
- while (1);
- }
- void system_irqtable_register(IRQn_Type irq, system_irq_handler_t handler, void* userParam)
- {
- irqTable[irq].irqHandler = handler;
- irqTable[irq].userParam = userParam;
- }
- void system_irqtable_init()
- {
- int i = 0;
- irqNesting = 0;
- for (i = 0; i < NUMBER_OF_INT_VECTORS; i++)
- {
- system_irqtable_register(i, default_irqhandler, NULL);
- }
- }
- void int_init()
- {
- GIC_Init();
- __set_VBAR(0x87800000);
- system_irqtable_init();
- }
2.2 GPIO 中断设置
通用的中断驱动写好之后,剩下的就是 GPIO 中断设置的工作。本章通过 GPIO 中断实现按键事件的通知,以控制 LED 灯的亮灭。LED 灯的驱动已经在之前的文章中介绍过了:[ARM裸机开发] NXP官方SDK包使用以及BSP工程管理。按键的驱动也大同小异,只不过是多了 GPIO 中断相关寄存器的初始化设置。
如图 17 所示,按键连接着 GPIO1_18。初始化工作包括:
1. IOMUXC_SetPinMux 函数设置管脚复用;IOMUXC_SetPinConfig 函数进行 IO 设置。
2. 通过 IMR 寄存器首先关闭 GPIO 中断使能。GDIR 寄存器设置 GPIO 方向为输入。
3. 通过 EDGE_SEL 寄存器关闭双边缘触发中断,因为只需要下降沿触发。因为 GPIO18 对应的第二组,所以使用 ICR2 寄存器设置为下降沿触发。通过 IMR 寄存器再次使能 GPIO 中断。
4. 注册按键驱动对应的中断处理函数;GIC_EnableIRQ 函数开启相对应的 SPI 中断。

- void gpio1_io18_irqhandler(unsigned int giccIar, void* param)
- {
- static uint32_t led_status = 1;
- delay(10);
- if ( ((GPIO1->DR >> 18) & 0x1) == 0)
- {
- led_switch(LED0, led_status);
- led_status = !led_status;
- }
- /* clear */
- GPIO1->ISR |= (1 << 18);
- }
- void bnt_init(void)
- {
- IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
- IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xf080);
- /* disable int */
- GPIO1->IMR &= (~(1 << 18));
- /* input */
- GPIO1->GDIR &= (~(1 << 18));
- /* interrupt configure */
- GPIO1->EDGE_SEL &= (~(1 << 18)); /* disable */
- GPIO1->ICR2 |= (3 << 4); /* falling_edge */
- GPIO1->IMR |= (1 << 18); /* enable int */
- /* GIC */
- system_irqtable_register(GPIO1_Combined_16_31_IRQn, gpio1_io18_irqhandler, NULL);
- GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
- }
gpio1_io18_irqhandler 函数为按键驱动对应的中断处理函数,处理完毕后需要写 ISR 寄存器清除中断标志。同时需要注意的是,开发板教材里说明中断需要快进快出,忌讳在中断处理函数中使用延时处理,后续会使用定时器来实现按键消抖。
最后遗留的一个问题是传给 GIC_EnableIRQ 的参数,即 GPIO1_18 对应的 SPI 中断号是多少。在 1.1 节了解到 SPI 中断号对应的范围是 32 - 1020,这部分是厂商自行规定的,所以具体定义要在 i.MUX 手册里找。i.MUX 支持 128 个 SPI 中断,因为数量众多,图 18 摘录了部分内容。从图中可以看到偏移 67 对应的 GPIO1_18 的中断,16-31 管脚共用,再加上 32,即 99 是 GPIO1_18 的中断号。

在实验过程中,一开始 CPU 主频设置的为 696MHz,运行出错。改为 528MHz 就正常了,不明原因。
3.总结
中断这章内容非常零碎,涉及 GIC、CP15 以及 GPIO 中断设置,并且有非常多的寄存器。中断嵌套相关内容在 ARM 编程手册的 12.1.3 节有介绍,后续应该有机会参阅 linux 这方面的实现。