[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。

图1 中断向量表 - 来自 Cortex 编程手册

在了解了上述知识之后,可以先“改造”一下之前的 start.s 文件:设置中断向量表,每一个条目的内容为跳转到相应的处理函数中。Reset 中断的处理函数暂且还是原来的初始化内容,其余的中断处理函数暂设置为死循环。

片段 1 中断向量表
  1. .global _start
  2.  
  3. _start:
  4.     ldr pc,=ResetHandler
  5.     ldr pc,=UndefinedInstructionHandler
  6.     ldr pc,=SupervisorCallHandler
  7.     ldr pc,=PrefetchAbortHandler
  8.     ldr pc,=DataAbortHandler
  9.     ldr pc,=NotUsedHandler
  10.     ldr pc,=IrqInterruptHandler
  11.     ldr pc,=FiqInterruptHandler
  12.  
  13. ResetHandler:
  14.     mrs r0, cpsr
  15.     bic r0, r0, #0x1f
  16.     orr r0, r0, #0x13
  17.     msr cpsr, r0
  18.  
  19.     ldr sp, =0x80200000
  20.     b main
  21.  
  22. UndefinedInstructionHandler:
  23.     ldr r0,=UndefinedInstructionHandler
  24.     bx r0
  25.  
  26. SupervisorCallHandler:
  27.     ldr r0,=SupervisorCallHandler
  28.     bx r0
  29.  
  30. PrefetchAbortHandler:
  31.     ldr r0,=PrefetchAbortHandler
  32.     bx r0
  33.  
  34. DataAbortHandler:
  35.     ldr r0,=DataAbortHandler
  36.     bx r0
  37.  
  38. NotUsedHandler:
  39.     ldr r0,=NotUsedHandler
  40.     bx r0
  41.  
  42. IrqInterruptHandler:
  43.     ldr r0,=IrqInterruptHandler
  44.     bx r0
  45.  
  46. FiqInterruptHandler:
  47.     ldr r0,=FiqInterruptHandler
  48.     bx r0 
LDR 伪指令:

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 示意图

接下来把图 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 是为此保留的。

图3 GIC 逻辑块 - 来自 GIC v2 手册

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 Register

System Control Register(SCTLR) 寄存器提供系统的顶层控制。如图 4 所示,SCTLR 寄存器对应的序列为 {c1, 0, c0, 0}

图4 c1 寄存器汇总 - 来自 MPCore 参考手册

如图 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 时,使能地址转换。

图5 SCTLR 寄存器 - 来自 MPCore 参考手册

1.2.3 Vector Base Address Register

Vector Base Address Register(VBAR) 寄存器,在未选择高地址中断向量表时,可以保存未进入 Monitor 或 Hyp 模式的中断向量表基地址。如图 6 所示,VBAR 寄存器对应的序列为 {c12, 0, c0, 0}

图6 c12 寄存器汇总 - 来自 MPCore 参考手册

图 7 显示了 VBAR 寄存器的位定义,可以看到只需要定义 5-31 位,0-4 位是保留位,但要求写时必须为 0。UNK/SBZP 是 unknown on reads, Should-Be-Zero-or-Preserved on writes 的缩写。

图7 VBAR 寄存器 - 来自 ARM 参考手册

1.2.4 Configuration Base Address Register

Configuration Base Address Register(CBAR) 寄存器,用于保存通过内存映射方式(memory-mapped)定义的 GIC 寄存器组的基地址。如图 8 所示,VBAR 寄存器对应的序列为 {c15, 4, c0, 0}

图8 c15 寄存器汇总 - 来自 MPCore 参考手册

图 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。

图9 CBAR 寄存器 - 来自 MPCore 参考手册
图10 GIC 内存映射表 - 来自 MPCore 参考手册

1.3 GIC 编程

在 1.1 节中介绍好 GIC 架构之后,穿插了 1.2 节的一个主要原因就是需要获取 GIC 寄存器组的基地址。知道了 GIC 寄存器组的基地址以及图 10 的 GIC 内存内存分布,就能操作 GIC 定义的各个寄存器。

图 11 是分发器端的寄存器内存映射,图 12 是 CPU 接口端的寄存器内存映射,可以看到定义的寄存器非常多,没有针对性的过一遍文档效率并不高。好在 i.MUX 提供的官方 SDK 包里面附带了 GIC 相关的操作函数,主要集中在 core_ca7.h 这个文件中。下面的记录思路主要是根据 SDK 里面的库函数内容,以此找到使用的寄存器说明,以理解代码的意图。

图11 Distributor 寄存器内存映射 - 来自 GIC v2 手册
图12 CPU interface 寄存器内存映射 - 来自 GIC v2 手册

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_PMRGICC_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。

图13 GICC_PMR 寄存器 - 来自 GIC v2 手册
图14 binary point - 来自 GIC v2 手册

第 23 行:使用到了 GICD_CTLR 寄存器。GICD_CTLR 寄存器控制是否将挂起的中断从分发器转发到 CPU 接口端。代码中开启 group0 中断的分发。

第 26 行:使用到了 GICC_CTLR 寄存器。GICC_CTLR 寄存器控制是否从 CPU 接口端发送中断信号到 CPU。代码中开启 group0 中断的发送。

  1. /* For simplicity, we only use group0 of GIC */
  2. FORCEDINLINE __STATIC_INLINE void GIC_Init(void)
  3. {
  4.   uint32_t i;
  5.   uint32_t irqRegs;
  6.   GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  7.  
  8.   irqRegs = (gic->D_TYPER & 0x1FUL) + 1;
  9.  
  10.   /* On POR, all SPI is in group 0, level-sensitive and using 1-N model */
  11.  
  12.   /* Disable all PPI, SGI and SPI */
  13.   for (i = 0; i < irqRegs; i++)
  14.     gic->D_ICENABLER[i] = 0xFFFFFFFFUL;
  15.  
  16.   /* Make all interrupts have higher priority */
  17.   gic->C_PMR = (0xFFUL << (8 - __GIC_PRIO_BITS)) & 0xFFUL;
  18.  
  19.   /* No subpriority, all priority level allows preemption */
  20.   gic->C_BPR = 7 - __GIC_PRIO_BITS;
  21.  
  22.   /* Enable group0 distribution */
  23.   gic->D_CTLR = 1UL;
  24.  
  25.   /* Enable group0 signaling */
  26.   gic->C_CTLR = 1UL;
  27. }

1.3.2 SDK - GIC_EnableIRQ()

GIC_EnableIRQ 函数使能某一 IRQ 中断。函数里使用到了 GICD_ISENABLER 寄存器,布局和用法和 1.3.1 节中介绍的 GICD_ICENABLER 寄存器是类似的。

GICD_ISENABLERGICD_ICENABLER 寄存器还有各自别的用途。GICD_ISENABLER 寄存器可以用于确认支持的中断:先关闭分发器到 CPU 接口端的中断转发,接着向 GICD_ISENABLER 里的各位写使能位,接着再读取,如果哪位还是使能,则代表这个中断存在。GICD_ICENABLER 寄存器可用于发现那些永久使能的中断:首先向 GICD_ICENABLER 里各位写禁止位,接着再读取,如果哪位没有禁止则代表相应的中断是永久使能的;需要特别注意的是,禁止所有中断后,如果还要使能某个中断的话,需要重新写入 GICD_ISENABLER 寄存器。

  1. FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
  2. {
  3.   GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  4.  
  5.   gic->D_ISENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
  6. }


1.3.3 GICC_IAR 寄存器

CPU 可以通过读取 GICC_IAR 寄存器以得到信号中断的中断号。同时,读取操作作为对中断的应答。GICC_IAR 寄存器的位定义如图 15 所示,看名字也很简单明了,包括 CPU 号和中断号。

图15 GICC_IAR 寄存器 - 来自 GIC v2 手册

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 节的内容。

  1. ResetHandler:
  2.     cpsid i /* disable irq */
  3.  
  4.     /* disable I/DCache and MMU */
  5.     mrc p15, 0, r0, c1, c0, 0
  6.     bic r0, r0, #(0x1 << 12)
  7.     bic r0, r0, #(0x1 << 2)
  8.     bic r0, r0, #0x2
  9.     bic r0, r0, #(0x1 << 11)
  10.     bic r0, r0, #0x1
  11.     mcr p15, 0, r0, c1, c0, 0
  12.  
  13.     /* irq sp */
  14.     mrs r0, cpsr
  15.     bic r0, r0, #0x1f
  16.     orr r0, r0, #0x12
  17.     msr cpsr, r0
  18.     ldr sp, =0x80600000
  19.  
  20.     /* sys sp */
  21.     mrs r0, cpsr
  22.     bic r0, r0, #0x1f
  23.     orr r0, r0, #0x1f
  24.     msr cpsr, r0
  25.     ldr sp, =0x80400000
  26.  
  27.     /* svc sp */
  28.     mrs r0, cpsr
  29.     bic r0, r0, #0x1f
  30.     orr r0, r0, #0x13
  31.     msr cpsr, r0
  32.     ldr sp, =0x80200000
  33.  
  34.     cpsie i /* enable irq */
  35.  
  36.     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 模式供用,墨蓝底色的寄存器是影子寄存器,各个模式下有单独的 “备份”。

  1. IrqInterruptHandler:
  2.     push {lr}   /* lr_irq */
  3.     push {r0-r3, r12}
  4.  
  5.     mrs r0, spsr
  6.     push {r0}
  7.  
  8.     mrc p15, 4, r1, c15, c0, 0 /* gic base addr */
  9.     add r1, r1, #0x2000 /* cpu interface base addr */
  10.     ldr r0, [r1, #0xc] /* GICC_IAR */
  11.     push {r0, r1}
  12.  
  13.         cps #0x13
  14.         push {lr} /* lr_svc */
  15.         ldr r2, =CIrqHandler
  16.         blx r2
  17.         pop {lr}
  18.    
  19.     cps #0x12
  20.     pop {r0, r1}
  21.     str r0, [r1, #0x10] /* GICC_EOIR */
  22.  
  23.     pop {r0}
  24.     msr spsr_cxsf, r0
  25.     pop {r0-r3, r12}
  26.     pop {lr}
  27.  
  28.     subs pc, lr, #4
图16 ARM 寄存器组 - 来自 ARM 编程手册

C 语言部分写的中断处理函数如下所示,并不复杂,就是根据中断号,在之前初始化好的函数指针数组里执行相应的中断处理函数即可。

  1. void CIrqHandler(unsigned int giccIar)
  2. {
  3.     uint32_t intNum = giccIar & 0x3ff;
  4.     if (intNum >= NUMBER_OF_INT_VECTORS)
  5.         return;
  6.    
  7.     irqNesting++;
  8.  
  9.     irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
  10.  
  11.     irqNesting--;
  12. }

中断驱动部分最后剩下的内容就是初始化函数,见如下代码中的 int_init 函数。int_init 函数首先使用 SDK 包里面的 GIC_Init 函数初始化 GIC,这部分已经在 1.3.1 节中介绍过了。接着重映射中断向量表的基地址,这在 1.2.3 节中已经介绍过。最后是初始化一下各个中断处理函数,一开始全都设置为默认的 default_irqhandler 函数。具体中断处理函数等到要使用到的时候,再通过 system_irqtable_register 函数 “注册”。

  1. static sys_irq_info_t irqTable[NUMBER_OF_INT_VECTORS];
  2.  
  3. void default_irqhandler(unsigned int giccIar, void* param)
  4. {
  5.     while (1);
  6. }
  7.  
  8. void system_irqtable_register(IRQn_Type irq, system_irq_handler_t handler, void* userParam)
  9. {
  10.     irqTable[irq].irqHandler = handler;
  11.     irqTable[irq].userParam = userParam;
  12. }
  13.  
  14. void system_irqtable_init()
  15. {
  16.     int i = 0;
  17.     irqNesting = 0;
  18.     for (i = 0; i < NUMBER_OF_INT_VECTORS; i++)
  19.     {
  20.         system_irqtable_register(i, default_irqhandler, NULL);
  21.     }
  22. }
  23.  
  24. void int_init()
  25. {
  26.     GIC_Init();
  27.     __set_VBAR(0x87800000);
  28.  
  29.     system_irqtable_init();
  30. }

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 中断。

图17 按键原理图

  1. void gpio1_io18_irqhandler(unsigned int giccIar, void* param)
  2. {
  3.     static uint32_t led_status = 1;
  4.  
  5.     delay(10);
  6.     if ( ((GPIO1->DR >> 18) & 0x1) == 0)
  7.     {
  8.         led_switch(LED0, led_status);
  9.         led_status = !led_status;
  10.     }
  11.  
  12.     /* clear */
  13.     GPIO1->ISR |= (1 << 18);
  14. }
  15.  
  16. void bnt_init(void)
  17. {
  18.     IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
  19.     IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xf080);
  20.  
  21.     /* disable int */
  22.     GPIO1->IMR &= (~(1 << 18));
  23.  
  24.     /* input */
  25.     GPIO1->GDIR &= (~(1 << 18));
  26.  
  27.     /* interrupt configure */
  28.     GPIO1->EDGE_SEL &= (~(1 << 18)); /* disable */
  29.  
  30.     GPIO1->ICR2 |= (3 << 4); /* falling_edge */
  31.  
  32.     GPIO1->IMR |= (1 << 18); /* enable int */
  33.  
  34.     /* GIC */
  35.     system_irqtable_register(GPIO1_Combined_16_31_IRQn, gpio1_io18_irqhandler, NULL);
  36.     GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
  37. }

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 的中断号。

图18 SPI 中断

在实验过程中,一开始 CPU 主频设置的为 696MHz,运行出错。改为 528MHz 就正常了,不明原因。


3.总结

中断这章内容非常零碎,涉及 GIC、CP15 以及 GPIO 中断设置,并且有非常多的寄存器。中断嵌套相关内容在 ARM 编程手册的 12.1.3 节有介绍,后续应该有机会参阅 linux 这方面的实现。