中断向量表是一个表,这个表里面存放的是中断向量。
中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。
这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。
中断向量表在整个程序的最前面,比如 STM32F103 的中断向量表如下所示:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
/* 省略掉其它代码 */
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & l5
与其说是中断向量表,不如说是中断服务函数的表。向量的含义其实是表示一种对应关系,其中的每个中断服务函数都对应一个中断ID,当相应的中断产生时,就会根据这个中断的ID来选择对应的中断服务函数进行处理。
下面我们来看 I.MX6ULL所属的Cortex-A7架构的中断向量表
但是这里只有8个中断啊~其中一个还是未使用的。
那类似 STM32 中的EXTI9_5_IRQHandler、 TIM2_IRQHandler 这样的中断向量在哪里? I2C、 SPI、定时器等等的中断怎么处理呢?这个就是 Cortex-A 和 Cotex-M 在中断向量表这一块的区别,对于 Cortex-M 内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于 CotexA 内核来说并没有这么做,上表有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IQR 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器(获取到中断具体中断的ID)来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理(也就是说在IRQ中断服务函数中一定还有其他的对于不同ID的处理函数)。
我们仔细看一下上面的7个中断服务函数:
1) 复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做
一些初始化工作,比如初始化 SP 指针、 DDR 等等。
2) 未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
3) 软中断(Software Interrupt,SWI),由 SWI 指令引起的中断, Linux 的系统调用会用 SWI 指
令来引起软中断,通过软中断来陷入到内核空间。
4) 指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
5) 数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
6) IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中断的发生。
7) FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中。
在上面的 7 个中断中,我们常用的就是复位中断和 IRQ 中断,所以我们需要编写这两个中断的中断服务函数,稍后会具体编写对应的中断服务函数。首先我们要根据这7个中断服务函数的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的 start.S
文件最前面,中断向量表如下:
.global _start /* 全局标号 */
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义指令中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ 中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
/* 复位中断 */
Reset_Handler:
/* 复位中断具体处理过程 */
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中断 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0
/* IRQ 中断!重点!!!!! */
IRQ_Handler:
/* 复位中断具体处理过程 */
/* FIQ 中断 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
代码前边是中断向量表,当指定的中断发生以后就会调用相应的中断服务函数,比如发生了复位中断,就会调用Reset_Handler复位中断服务函数。其他中断同理。
代码后面一部分就是对应的各个中断服务函数的具体内容,都是用汇编写的。我们在本次实验中只会用到其中的1. 复位中断服务函数:Reset_Handler、2.IRQ中断服务函数:IRQ_Handler。其他都没用到所以是死循环。
上面的代码中我们需要完成的1、和2、并没有进行编写,我们的目的是在1、中完成reset的功能,在2、中完成保存现场、获取中断id号、调用中断服务函数、还原现场的任务。 那么为了完成这些,我们需要进一步了解中断~
GIC:general interrupt controller
目前 GIC 有 4 个版本:V1~V4, V1 是最老的版本,已经被废弃了。 V2~V4 目前正在大量的使用。 GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、 Cortex-A9、 Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。
I.MX6U 是 Cortex-A 内核的,因此我们主要讲解 GIC V2。 GIC V2 最多支持 8 个核。 ARM 会根据 GIC 版本的不同研发出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可,比如 ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ
听起来很乱,我们看图:
从图中我们看出,各种各样的中断触发,首先由GIC控制器来接管这些中断,在经过某些作用后,按照中断的不同向ARM内核发送4种类型的信号:
VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了。本节我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号。那么 GIC 是如何完成这个工作的呢?也就是说,刚刚提到的“经过某些作用”,是什么呢?
首先我们看个图
咋一看乱七八糟,仔细一看,还挺有层次的。
我们先挑重点:
中间黑色框框中的就是GIC,以及它的作用;左侧红色标记的①②③代表不同的中断源;右侧是GIC向ARM内核发送的刚刚说的那4种信号与内核之间的交流。
我们从左向右说:
①、 SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、 PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,那么这三类中断是如何分配这 1020 个中断 ID 的呢?这 1020 个 ID 分配如下:
ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 。但是这部分ID太多了,具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。
比如 I.MX6U 的总共使用了 128 个SPI中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID, I.MX6U 的中断源共有 128+32=160个。
也就是说,在i.imx6ull中IRQ中断类型一共就由128个,占用了ID 32 - 159
也就是说,官方已经定义了各个中断所使用的ID,那么这部分信息我们在代码中怎么使用呢,其实在NXP的官方SDK中MCIMX6Y2C.h
中就有这部分的定义。
#define NUMBER_OF_INT_VECTORS 160 /* 中断源 160 个, SGI+PPI+SPI*/
typedef enum IRQn {
/* Auxiliary constants */
NotAvail_IRQn = -128,
/* Core interrupts */
Software0_IRQn = 0,
Software1_IRQn = 1,
Software2_IRQn = 2,
Software3_IRQn = 3,
Software4_IRQn = 4,
Software5_IRQn = 5,
Software6_IRQn = 6,
Software7_IRQn = 7,
Software8_IRQn = 8,
Software9_IRQn = 9,
Software10_IRQn = 10,
Software11_IRQn = 11,
Software12_IRQn = 12 ,
Software13_IRQn = 13,
Software14_IRQn = 14,
Software15_IRQn = 15,
VirtualMaintenance_IRQn = 25,
HypervisorTimer_IRQn = 26,
VirtualTimer_IRQn = 27,
LegacyFastInt_IRQn = 28,
SecurePhyTimer_IRQn = 29,
NonSecurePhyTimer_IRQn = 30,
LegacyIRQ_IRQn = 31,
/* Device specific interrupts */
IOMUXC_IRQn = 32,
DAP_IRQn = 33,
SDMA_IRQn = 34,
TSC_IRQn = 35,
SNVS_IRQn = 36,
...... ......
ENET2_1588_IRQn = 153,
Reserved154_IRQn = 154,
Reserved155_IRQn = 155,
Reserved156_IRQn = 156,
Reserved157_IRQn = 157,
Reserved158_IRQn = 158,
PMU_IRQ2_IRQn = 159
} IRQn_Type;
使用的时候我们只要引入这个头文件就ok了~
细心的盆友可能发现,在GIC部分(也就是中间的部分)还分为两块。
就是图中的DIstributor和CPU Interface
GIC 架构分为了两个逻辑块: Distributor 和 CPU Interface,也就是分发器端和CPU 接口端。
这两个逻辑块的含义如下:
Distributor(分发器端):
从图可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:
①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组 0 还是组 1。
CPU Interface(CPU 接口端):
CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在图中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。 CPU 接口端就是分发器和 CPU Core 之间的桥梁, CPU 接口端主要工作如下:
①、使能或者关闭发送到 CPU Core 的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core
说了这么多功能,那么我们怎么使用它来完成我们的目的呢?
这就需要操作寄存器了。
上面提到了外部中断源产生的中断信号进入到GIC中,Distributor 和 CPU Interface同心协力作用。
那么这两部分功能的实现必然需要我们通过设置某些寄存器,以及我们读取某些寄存器获得我们想要的参数,比如:我们如何获得外部中断源的ID呢?
在core_ca7.h
中,定义了GIC结构体,其描述了GIC的所有寄存器,分为分发器端和CPU接口端。
/*
* GIC 寄存器描述结构体,
* GIC 分为分发器端和 CPU 接口端
*/
typedef struct
{
/* 分发器端寄存器 */
uint32_t RESERVED0[1024];
__IOM uint32_t D_CTLR; /* Offset: 0x1000 (R/W) */
__IM uint32_t D_TYPER; /* Offset: 0x1004 (R/ ) */
__IM uint32_t D_IIDR; /* Offset: 0x1008 (R/ ) */
uint32_t RESERVED1[29];
__IOM uint32_t D_IGROUPR[16]; /* Offset: 0x1080 - 0x0BC (R/W) */
uint32_t RESERVED2[16];
__IOM uint32_t D_ISENABLER[16];/* Offset: 0x1100 - 0x13C (R/W) */
uint32_t RESERVED3[16];
__IOM uint32_t D_ICENABLER[16];/* Offset: 0x1180 - 0x1BC (R/W) */
uint32_t RESERVED4[16];
__IOM uint32_t D_ISPENDR[16]; /* Offset: 0x1200 - 0x23C (R/W) */
uint32_t RESERVED5[16];
__IOM uint32_t D_ICPENDR[16]; /* Offset: 0x1280 - 0x2BC (R/W) */
uint32_t RESERVED6[16];
__IOM uint32_t D_ISACTIVER[16];/* Offset: 0x1300 - 0x33C (R/W) */
uint32_t RESERVED7[16];
__IOM uint32_t D_ICACTIVER[16];/* Offset: 0x1380 - 0x3BC (R/W) */
uint32_t RESERVED8[16];
__IOM uint8_t D_IPRIORITYR[512];/* Offset: 0x1400 - 0x5FC (R/W) */
uint32_t RESERVED9[128];
__IOM uint8_t D_ITARGETSR[512];/* Offset: 0x1800 - 0x9FC (R/W) */
uint32_t RESERVED10[128];
__IOM uint32_t D_ICFGR[32]; /* Offset: 0x1C00 - 0xC7C (R/W) */
uint32_t RESERVED11[32];
__IM uint32_t D_PPISR; /* Offset: 0x1D00 (R/ ) */
__IM uint32_t D_SPISR[15]; /* Offset: 0x1D04 - 0xD3C (R/ ) */
uint32_t RESERVED12[112];
__OM uint32_t D_SGIR; /* Offset: 0x1F00 ( /W) */
uint32_t RESERVED13[3];
__IOM uint8_t D_CPENDSGIR[16];/* Offset: 0x1F10 - 0xF1C (R/W) */
__IOM uint8_t D_SPENDSGIR[16];/* Offset: 0x1F20 - 0xF2C (R/W) */
uint32_t RESERVED14[40];
__IM uint32_t D_PIDR4; /* Offset: 0x1FD0 (R/ ) */
__IM uint32_t D_PIDR5; /* Offset: 0x1FD4 (R/ ) */
__IM uint32_t D_PIDR6; /* Offset: 0x1FD8 (R/ ) */
__IM uint32_t D_PIDR7; /* Offset: 0x1FDC (R/ ) */
__IM uint32_t D_PIDR0; /* Offset: 0x1FE0 (R/ ) */
__IM uint32_t D_PIDR1; /* Offset: 0x1FE4 (R/ ) */
__IM uint32_t D_PIDR2; /* Offset: 0x1FE8 (R/ ) */
__IM uint32_t D_PIDR3; /* Offset: 0x1FEC (R/ ) */
__IM uint32_t D_CIDR0; /* Offset: 0x1FF0 (R/ ) */
__IM uint32_t D_CIDR1; /* Offset: 0x1FF4 (R/ ) */
__IM uint32_t D_CIDR2; /* Offset: 0x1FF8 (R/ ) */
__IM uint32_t D_CIDR3; /* Offset: 0x1FFC (R/ ) */
/* CPU 接口端寄存器 */
__IOM uint32_t C_CTLR; /* Offset: 0x2000 (R/W) */
__IOM uint32_t C_PMR; /* Offset: 0x2004 (R/W) */
__IOM uint32_t C_BPR; /* Offset: 0x2008 (R/W) */
__IM uint32_t C_IAR; /* Offset: 0x200C (R/ ) */
__OM uint32_t C_EOIR; /* Offset: 0x2010 ( /W) */
__IM uint32_t C_RPR; /* Offset: 0x2014 (R/ ) */
__IM uint32_t C_HPPIR; /* Offset: 0x2018 (R/ ) */
__IOM uint32_t C_ABPR; /* Offset: 0x201C (R/W) */
__IM uint32_t C_AIAR; /* Offset: 0x2020 (R/ ) */
__OM uint32_t C_AEOIR; /* Offset: 0x2024 ( /W) */
__IM uint32_t C_AHPPIR; /* Offset: 0x2028 (R/ ) */
uint32_t RESERVED15[41];
__IOM uint32_t C_APR0; /* Offset: 0x20D0 (R/W) */
uint32_t RESERVED16[3];
__IOM uint32_t C_NSAPR0; /* Offset: 0x20E0 (R/W) */
uint32_t RESERVED17[6];
__IM uint32_t C_IIDR; /* Offset: 0x20FC (R/ ) */
uint32_t RESERVED18[960];
__OM uint32_t C_DIR; /* Offset: 0x3000 ( /W) */
} GIC_Type;
根据结构体中元素的地址连续性,我们可以直接通过GIC_Type这个结构体来访问GIC的所有寄存器。不必担心首地址的问题,这是官方的sdk包中的文件,后面所标注的偏移量就是相对于GIC及地址的偏移。
因此,我们只需要在获取到GIC及地址后加上0x1000就能访问到GIC分发器端寄存器了、加上0x2000就能访问到GIC的CPU接口端寄存器了
那GIC的及地址在哪里得到呢?——>Cortex-A的CP15协处理器。
CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到, CP15 协处理器一共有16 个 32 位寄存器(c0-c15)。 CP15 协处理器的访问通过其独特的语法指令完成,如下所示:
(move to register from controller;move to controller to register)
MRC: 将** CP15 协处理器中的寄存器数据读到 ARM 寄存器中**。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
MRC 就是读 CP15 寄存器, MCR 就是写 CP15 寄存器, MCR 指令格式如下:
MCR{cond} p15, ,
MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 就是目标寄存器,也就是从CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器,也就是要读取的写处理器寄存器。
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:
MRC p15, 0, r0, c0, c0, 0
CP15 协处理器有 16 个 32 位寄存器, c0~c15,我们来看一下 c0、 c1、 c12 和 c15 这四个寄存器,因为我们本章实验要用到这四个寄存器。
(内容比较多,具体内容见上一篇:Cortex-A 系列CP15协处理器简单解析)
关于 CP15 协处理器这里简单总结一下,
我们通过CP15寄存器 和 state register 完成上面4个步骤以及其他初始化任务。
当然,其中最为关键的获取中断ID是什么?当我们过得了GIC基地址之后,我们能够通过上面的GIC_Type结构体
中找到CPU接口端寄存器的地址偏移量。
再铜鼓怄气中的C_IAR寄存器其中保存着当前中断的ID
据此我们了解到,
start.S
中的中断服务函数,当然,Reset函数可以直接在汇编文件start.S
中编写,而IRQ中断服务函数比较复杂,汇编文件中可能并不是很容易实现,我们需要在IRQ中断服务函数中添加一个 C函数,来完成针对不同中断ID的具有 特异性的服务函数。这样我们对于这部分中断内容有了一个初步的流程认识,我们继续看下面的知识点,然后我们会对总体的流程进行一个总结。
中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020
个中断源的使能。
IRQ 是外部中断的总开关,就类似家里买的进户总电闸,然后ID0~ID1019 这 1020个中断源就类似家里面的各个电器开关。要想开电视,那肯定要保证进户总电闸是打开的,因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断。在“6.3.2 程序状态寄存器”中说过,**寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;**我们还有更简单的指令来完成 IRQ 或者 FIQ 的使能和禁止,如下表:
GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个GICD_ICENABLER 寄存器来完成中断的禁止。其中 GICD_ISENABLER0 的 bit[15:0]对应ID15~0 的 SGI 中断, GICD_ISENABLER0 的 bit[31:16]对应 ID31~16 的 PPI 中断。
剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
3.1、优先级数配置
Cortex-M 的中断优先级分为抢占优先级和子优先级,两者是可以配置的。同样的Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置
的。 Cortex-A7 最多可以支持 256 个优先级,数字越小,优先级越高!半导体厂商自行决定选择多少个优先级。 I.MX6U 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR
寄存器,此寄存器用来决定使用几级优先级,寄存器结构如图所示:
GICC_PMR 寄存器
只有低 8 位有效,这 8 位最多可以设置 256 个优先级,其他优先级数设置如表所示:
I.MX6U 支持 32 个优先级,所以 GICC_PMR
要设置为 0b 11111000
3.2、抢占优先级和子优先级位数设置
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的, GICC_BPR 寄存器结构如图所示:
寄存器 GICC_BPR
只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如表 所示:
为了简单起见,一般将所有的中断优先级位都配置为抢占优先级,比如 I.MX6U 的优先级位数为 5(32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级。
3.3、优先级设置
前面已经设置好了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。具体要使用某个中断的时候就可以设置其优先级为 0~31。某个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR
来完成,前面说了 Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5,示例代码如下:GICD_IPRIORITYR[40] = 5 << 3;
有关优先级设置的内容就讲解到这里,优先级设置主要有三部分:
①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。
②、设置抢占优先级和子优先级位数,一般为了简单起见,会将所有的位数都设置为抢占优先级。
③、设置指定中断 ID 的优先级,也就是设置外设优先级。
在写程序之前,我们需要明确几个问题:
将 SDK 包中的文件 core_ca7.h
拷贝到本章试验工程中的“imx6ull”文件夹中,对core_ca7.h
进行修改。主要留下和 GIC 相关的内容,我们重点是需要 core_ca7.h 中的 10 个 API 函数,这 10 个函数如表所示:
功能:
这里重点和难点是这两个中断服务函数的编写,我们对其中的内容进行整理(具体内容可见代码中的注释信息)
system_irqhandler
C语言函数到r2,并跳转执行r2(blx)
.global _start /* 全局标号 */
/*
* 描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
/* 复位中断 */
Reset_Handler:
cpsid i /* 关闭全局中断 */
/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */
#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000
dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif
/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endif
b main /* 跳转到main函数 */
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中断 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0
/* IRQ中断!重点!!!!! */
IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */
mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */
cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */
pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */
pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */
pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */
/* FIQ中断 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
至此,start.S
文件编写完成,完成了系统初始化的过程。
在 start.S
文件中我们在中断服务函数 IRQ_Handler
中调用了 C 函数system_irqhandler
来处理具体的中断。此函数有一个参数,参数是中断号(这个参数在汇编文件中,默认传入的是r0),但是函数 system_irqhandler
的具体内容还没有实现,所以需要实现函数 system_irqhandler 的具体内容。不同的中断源对应不同的中断处理函数, I.MX6U 有 160 个中断源,所以需要 160 个中断处理函数,我们可以将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。
当中断发生以后函数 system_irqhandler
根据中断号从中断处理函数数组中找到对应的中断处理函数并执行即可。
在 bsp 目录下新建名为“int”的文件夹,在 bsp/int 文件夹里面创建 bsp_int.c
和 bsp_int.h
这两个文件。
文件内容:
int_init()函数
int_init()
函数主要用来:①初始化GIC ②初始化中断表 ③设置中断向量偏移GIC_Init()
函数是core_ca7.h
SDK包中的GIC初始化函数system_irqtable_init(省略)
函数时初始化中断表函数(将所有160个ID对应的中断服务函数都注册为默认中断服务函数死循环);其中调用system_register_irqhandler(省略)
函数(用来给指定的中断号注册中断服务函数);__set_VBAR((uint32_t)0x87800000)
完成中断向量偏移设置,该函数也在sdk包中。system_irqhandler(unsigned int giccIar)
函数。本函数是C 语言中断服务函数, irq 汇编中断服务函数会调用此函数,此函数通过在中断服务列表中查找指定中断号所对应的中断处理函数并执行(所谓的对应,就是在之前注册的结构体数组中,将对应的处理函数和参数进行初始化)#ifndef _BSP_INT_H
#define _BSP_INT_H
#include "imx6ul.h"
/* 中断服务函数形式 */
typedef void (*system_irq_handler_t) (unsigned int giccIar, void *param);
/* 中断服务函数结构体*/
typedef struct _sys_irq_handle
{
system_irq_handler_t irqHandler; /* 中断服务函数 */
void *userParam; /* 中断服务函数参数 */
} sys_irq_handle_t;
/* 函数声明 */
void int_init(void);
void system_irqtable_init(void);
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam);
void system_irqhandler(unsigned int giccIar);
void default_irqhandler(unsigned int giccIar, void *userParam);
#endif
bsp_int.c:
#include "bsp_int.h"
/* 中断嵌套计数器 */
static unsigned int irqNesting;
/* 中断服务函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
/*
* @description : 中断初始化函数
* @param : 无
* @return : 无
*/
void int_init(void)
{
GIC_Init(); /* 初始化GIC */
system_irqtable_init(); /* 初始化中断表 */
__set_VBAR((uint32_t)0x87800000); /* 中断向量表偏移,偏移到起始地址*/
}
/*
* @description : 初始化中断服务函数表
* @param : 无
* @return : 无
*/
void system_irqtable_init(void)
{
unsigned int i = 0;
irqNesting = 0;
/* 先将所有的中断服务函数设置为默认值 */
for(i = 0; i < NUMBER_OF_INT_VECTORS; i++)
{
system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);
}
}
/*
* @description : 给指定的中断号注册中断服务函数
* @param - irq : 要注册的中断号
* @param - handler : 要注册的中断处理函数
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}
/*
* @description : C语言中断服务函数,irq汇编中断服务函数会
调用此函数,此函数通过在中断服务列表中查
找指定中断号所对应的中断处理函数并执行。
* @param - giccIar : 中断号
* @return : 无
*/
void system_irqhandler(unsigned int giccIar)
{
uint32_t intNum = giccIar & 0x3FFUL;
/* 检查中断号是否符合要求 */
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{
return;
}
irqNesting++; /* 中断嵌套计数器加一 */
/* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/
irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
irqNesting--; /* 中断执行完成,中断嵌套寄存器减一 */
}
/*
* @description : 默认中断服务函数
* @param - giccIar : 中断号
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void default_irqhandler(unsigned int giccIar, void *userParam)
{
while(1);
}
在之前的试验中我们使用到了 GPIO 最基本的输入输出功能,这里我们需要使用
GPIO 的中断功能。所以需要修改文件 GPIO 的驱动文件 bsp_gpio.c 和 bsp_gpio.h,加上中断相关函数。关于 GPIO 的配置工作,这里就不赘述了。
文件内容:(这里只针对终端需要的驱动来分析,基本配置不讲解,可以看我之前的文章)
bsp_gpio.h
中新增加了一个枚举类型gpio_interrupt_mode_t
用来描述GPIO引脚的中断触发方式(高低电平等)gpio_pin_config_t
GPIO配置结构体中新增 1 中的中断触发方式变量。bsp_gpio.h:
#ifndef _BSP_GPIO_H
#define _BSP_GPIO_H
#define _BSP_KEY_H
#include "imx6ul.h"
/*
* 枚举类型和结构体定义
*/
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;
/*
* GPIO中断触发类型枚举
*/
typedef enum _gpio_interrupt_mode
{
kGPIO_NoIntmode = 0U, /* 无中断功能 */
kGPIO_IntLowLevel = 1U, /* 低电平触发 */
kGPIO_IntHighLevel = 2U, /* 高电平触发 */
kGPIO_IntRisingEdge = 3U, /* 上升沿触发 */
kGPIO_IntFallingEdge = 4U, /* 下降沿触发 */
kGPIO_IntRisingOrFallingEdge = 5U, /* 上升沿和下降沿都触发 */
} gpio_interrupt_mode_t;
/*
* GPIO配置结构体
*/
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
gpio_interrupt_mode_t interruptMode; /* 中断方式 */
} gpio_pin_config_t;
/* 函数声明 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
int gpio_pinread(GPIO_Type *base, int pin);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pinInterruptMode);
void gpio_enableint(GPIO_Type* base, unsigned int pin);
void gpio_disableint(GPIO_Type* base, unsigned int pin);
void gpio_clearintflags(GPIO_Type* base, unsigned int pin);
#endif
bsp_gpio.c:
#include "bsp_gpio.h"
/*
* @description : GPIO初始化。
* @param - base : 要初始化的GPIO组。
* @param - pin : 要初始化GPIO在组内的编号。
* @param - config : GPIO配置结构体。
* @return : 无
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
base->IMR &= ~(1U << pin);
if(config->direction == kGPIO_DigitalInput) /* GPIO作为输入 */
{
base->GDIR &= ~( 1 << pin);
}
else /* 输出 */
{
base->GDIR |= 1 << pin;
gpio_pinwrite(base,pin, config->outputLogic); /* 设置默认输出电平 */
}
gpio_intconfig(base, pin, config->interruptMode); /* 中断功能配置 */
}
/*
* @description : 读取指定GPIO的电平值 。
* @param - base : 要读取的GPIO组。
* @param - pin : 要读取的GPIO脚号。
* @return : 无
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}
/*
* @description : 指定GPIO输出高或者低电平 。
* @param - base : 要输出的的GPIO组。
* @param - pin : 要输出的GPIO脚号。
* @param - value : 要输出的电平,1 输出高电平, 0 输出低低电平
* @return : 无
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if (value == 0U)
{
base->DR &= ~(1U << pin); /* 输出低电平 */
}
else
{
base->DR |= (1U << pin); /* 输出高电平 */
}
}
/*
* @description : 设置GPIO的中断配置功能
* @param - base : 要配置的IO所在的GPIO组。
* @param - pin : 要配置的GPIO脚号。
* @param - pinInterruptMode: 中断模式,参考枚举类型gpio_interrupt_mode_t
* @return : 无
*/
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pin_int_mode)
{
volatile uint32_t *icr;
uint32_t icrShift;
icrShift = pin;
base->EDGE_SEL &= ~(1U << pin);
if(pin < 16) /* 低16位 */
{
icr = &(base->ICR1);
}
else /* 高16位 */
{
icr = &(base->ICR2);
icrShift -= 16;
}
switch(pin_int_mode)
{
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
case(kGPIO_IntHighLevel):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (1U << (2 * icrShift));
break;
case(kGPIO_IntRisingEdge):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));
break;
case(kGPIO_IntFallingEdge):
*icr |= (3U << (2 * icrShift));
break;
case(kGPIO_IntRisingOrFallingEdge):
base->EDGE_SEL |= (1U << pin);
break;
default:
break;
}
}
/*
* @description : 使能GPIO的中断功能
* @param - base : 要使能的IO所在的GPIO组。
* @param - pin : 要使能的GPIO在组内的编号。
* @return : 无
*/
void gpio_enableint(GPIO_Type* base, unsigned int pin)
{
base->IMR |= (1 << pin);
}
/*
* @description : 禁止GPIO的中断功能
* @param - base : 要禁止的IO所在的GPIO组。
* @param - pin : 要禁止的GPIO在组内的编号。
* @return : 无
*/
void gpio_disableint(GPIO_Type* base, unsigned int pin)
{
base->IMR &= ~(1 << pin);
}
/*
* @description : 清除中断标志位(写1清除)
* @param - base : 要清除的IO所在的GPIO组。
* @param - pin : 要清除的GPIO掩码。
* @return : 无
*/
void gpio_clearintflags(GPIO_Type* base, unsigned int pin)
{
base->ISR |= (1 << pin);
}
本节目的是以中断的方式编写 KEY 按键驱动,当按下 KEY 以后触发 GPIO 中断,然后在中断服务函数里面控制蜂鸣器的开关。所以接下来就是要编写按键 KEY 对应的UART1_CTS 这个 IO 的中断驱动,在 bsp 文件夹里面新建名为“exit”的文件夹,然后在 bsp/exit里面新建 bsp_exit.c 和 bsp_exit.h
两个文件。
简单描述一下文件的内容:
exit_inti()
函数:其中①设置GPIO复用功能、电器属性②初始化GPIO的输入输出、默认电平、中断触发方式;③调用SDK中的GIC_EnableIRQ
函数使能GIC对应的中断(GIC中使能GPIO1_IO18对应的中断);④注册中断服务函数(注册证明已经编写好了中断处理函数,同样在本文件中。);⑤使能GPIO中断(从GPIO本身使能,这部分在②中默认关闭)bsp_exit.h :
#ifndef _BSP_EXIT_H
#define _BSP_EXIT_H
#include "imx6ul.h"
/* 函数声明 */
void exit_init(void); /* 中断初始化 */
void gpio1_io18_irqhandler(void); /* 中断处理函数 */
#endif
bsp_exit.c:
#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"
/*
* @description : 初始化外部中断
* @param : 无
* @return : 无
*/
void exit_init(void)
{
gpio_pin_config_t key_config;
/* 1、设置IO复用 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0); /* 复用为GPIO1_IO18 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
/* 2、初始化GPIO为中断模式 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); /* 使能GIC中对应的中断 */
system_register_irqhandler(GPIO1_Combined_16_31_IRQn, (system_irq_handler_t)gpio1_io18_irqhandler, NULL); /* 注册中断服务函数 */
gpio_enableint(GPIO1, 18); /* 使能GPIO1_IO18的中断功能 */
}
/*
* @description : GPIO1_IO18最终的中断处理函数
* @param : 无
* @return : 无
*/
void gpio1_io18_irqhandler(void)
{
static unsigned char state = 0;
/*
*采用延时消抖,中断服务函数中禁止使用延时函数!因为中断服务需要
*快进快出!!这里为了演示所以采用了延时函数进行消抖,后面我们会讲解
*定时器中断消抖法!!!
*/
delay(10);
if(gpio_pinread(GPIO1, 18) == 0) /* 按键按下了 */
{
state = !state;
beep_switch(state);
}
gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}
#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_exit.h"
/*
* @description : main函数
* @param : 无
* @return : 无
*/
int main(void)
{
unsigned char state = OFF;
int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
beep_init(); /* 初始化beep */
key_init(); /* 初始化key */
exit_init(); /* 初始化按键中断 */
while(1)
{
state = !state;
led_switch(LED0, state);
delay(500);
}
return 0;
}