STM32中断系统回顾
STM32 的中断系统主要有以下几个关键点:
- 中断向量表。
- NVIC(内嵌向量中断控制器)。
- 中断使能。
- 中断服务函数。
1、中断向量表
中断向量表是一个表,表里面存放的是中断向量。中断服务程序的入口地址或存放中断服务程序的首地址称为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表的主要功能是描述中断对应的中断服务函数。中断向量表在整个程序的最前面,比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的,那么中断向量表就从 0X00000000 开始存放。如下代码中第 1 行“__initial_sp”就是第一条中断向量,存放的是栈顶指针,接下来是第 2 行复位中断复位函数 Reset_Handler 的入口地址,依次类推,第 27 行是最后一个中断服务函数DMA2_Channel4_5_IRQHandler 的入口地址,这样 STM32F103 的中断向量表就建好了,如下:
1 __Vectors DCD __initial_sp ; Top of Stack //0x08000000
2 DCD Reset_Handler ; Reset Handler //0x08000004
3 DCD NMI_Handler ; NMI Handler //...依次类推
4 DCD HardFault_Handler ; Hard Fault Handler
5 DCD MemManage_Handler ; MPU Fault Handler
6 DCD BusFault_Handler ; Bus Fault Handler
7 DCD UsageFault_Handler ; Usage Fault Handler
8 DCD 0 ; Reserved
9 DCD 0 ; Reserved
10 DCD 0 ; Reserved
11 DCD 0 ; Reserved
12 DCD SVC_Handler ; SVCall Handler
13 DCD DebugMon_Handler ; Debug Monitor Handler
14 DCD 0 ; Reserved
15 DCD PendSV_Handler ; PendSV Handler
16 DCD SysTick_Handler ; SysTick Handler
17
18 ; External Interrupts //从这里开始就是STM32一些外设的中断服务函数
19 DCD WWDG_IRQHandler ; Window Watchdog
20 DCD PVD_IRQHandler ; PVD through EXTI Line detect
21 DCD TAMPER_IRQHandler ; Tamper
22 DCD RTC_IRQHandler ; RTC
23 DCD FLASH_IRQHandler ; Flash
24
25 /* 省略掉其它代码 */
26
27 DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & l5
28 __Vectors_End
我们说 ARM 处理器都是从地址 0X00000000 开始运行的,但学习 STM32 时,代码是下载到 0X08000000 开始的存储区域中。因此中断向量表是要存放到 0X08000000 地址处,而不是 0X00000000。若代码一定要从0x08000000处开始运行,需要告诉一下SOC/MCU内核,以此引入了概念——中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址处,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB的VTOR 寄存器写入新的中断向量表首地址即可,代码如下:
STM32F103 中断向量表偏移
1 void SystemInit (void)
2 {
3 RCC->CR |= (uint32_t)0x00000001;
4
5 /* 省略其它代码 */
6
7 #ifdef VECT_TAB_SRAM
8 SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
9 #else
10 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
11 #endif
12 }
第 8 行和第 10 行就是设置中断向量表偏移:第 8 行是将中断向量表设置到 RAM 中,第10 行是将中断向量表设置到 ROM 中,基本都是将中断向量表设置到 ROM 中,也就是地址0X08000000 处。第 10 行用到了 FALSH_BASE 和 VECT_TAB_OFFSET,这两宏定义如下:
#define FLASH_BASE ((uint32_t)0x08000000)
#define VECT_TAB_OFFSET 0x0
第 10 行代码意思就是: SCB->VTOR=0X080000000,中断向量表偏移设置完成。上面介绍了两个跟 STM32 中断有关概念:中断向量表和中断向量表偏移,I.MX6U 所使用的 Cortex-A7 内核也有中断向量表和中断向量表偏移,含义和 STM32 一样!只是用到的寄存器不同,概念完全相同!
2、NVIC中断控制器
中断系统得有个管理机构,对于 STM32 这种Cortex-M 内核的单片机来说此管理机构叫 NVIC(Nested Vectored Interrupt Controller),作用是使能和关闭指定中断、设置中断优先级。I.MX6U 所使用的 Cortex-A7 内核也有个中断系统管理机构,Cortex-A 内核的中断管理机构不叫NVIC,而是叫做 GIC,全称 general interrupt controller。
3、中断使能
要使用某个外设的中断,肯定要先使能这个外设的中断,以 STM32F103 的 PE2 这个 IO 为例,假如要使用 PE2 的输入中断需要用如下代码来使能对应中断:
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
同理,若要使用 I.MX6U 的某中断的话也需使能其对应的中断。
4、中断服务函数
使用中断的目的就是为了使用中断服务函数,中断发生后中断服务函数就会被调用,我们要处理的工作就可以放到中断服务函数中去完成。同样以 STM32F103 的 PE2 为例,其中断服务函数如下所示:
/* 外部中断 2 服务程序 */
void EXTI2_IRQHandler(void)
{
/* 中断处理代码 */
}
当PE2 引脚的中断触发后就会调用其对应的中断处理函数 EXTI2_IRQHandler,我们可在函数EXTI2_IRQHandler中添加中断处理代码。I.MX6U 也有中断服务函数,当某个外设中断发生以后就会调用其对应的中断服务函数。
Cortex-A7中断系统
1、Cortex-A中断向量表
和 STM32 一样, Cortex-A7 也有中断向量表,也是在代码的最前面。 中断向量表里面都是中断服务函数的入口地址,因此一款芯片有什么中断都是可以从中断向量表看出来的。从下表中可看出, Cortex-A7 一共有 8 个中断,而且还有一个中断向量未使用,实际只有 7 个中断。
在上表中一共有 7 个中断,简单介绍一下这 7 个中断:
- 复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等。
- 未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
- 软中断(Software Interrupt,SWI),由 SWI 指令引起的中断, Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
- 指令预取中止中断(Prefetch Abort),预取指令出错的时候会产生此中断。
- 数据访问中止中断(Data Abort),访问数据出错时会产生此中断。
- IRQ 中断(IRQ Interrupt),外部中断,芯片内部的外设中断都会引起此中断的发生。
- FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
在上面的 7 个中断中,常用的就是复位中断和 IRQ 中断,所以需编写这两个中断的中断服务函数
Cortex-A 和 Cotex-M 在中断向量表,IRQ外部中断这部分的区别:
对于 Cortex-M 内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于 Cortex-A 内核来说并没有这么做,上表中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于IRQ 中断,当任一个外部中断发生时都会触发 IRQ 中断。在 IRQ 中断服务函数里就可读取指定的寄存器来判断发生的具体是什么中断,在IRQ中断服务函数里面会读取当前触发中断的中断号,进而根据具体中断号再去执行具体的中断服务函数。
上图中,左侧的 Software0_IRQn~PMU_IRQ2_IRQ 这些都是 I.MX6U 的中断,都属于 IRQ 中断。当上图左侧这些中断有任意一个发生时,IRQ 中断都会被触发,我们需要在 IRQ 中断服务函数中判断究竟是左侧哪个中断发生了,然后再做出具体的处理。
首先要根据上面表格的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如之前例程的 start.s 文件最前面。中断向量表如下:
第 4 到 11 行是中断向量表,当指定中断发生后就会调用对应中断服务函数,比如复位中断发生后就会执行第 4 行代码,也就是调用函数 Reset_Handler,函数 Reset_Handler就是复位中断的中断服务函数,其它的中断同理。
第 14 到 50 行对应中断服务函数,中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断没有用到,所以都设置为死循环。
示例代码 17.1.1.1 Cortex-A 向量表模板
1 .global _start /* 全局标号 */
2
3 _start: //ldr pc,xxxx 将xxxx写入到pc寄存器,即:执行xxxx
4 ldr pc, =Reset_Handler /* 复位中断 */
5 ldr pc, =Undefined_Handler /* 未定义指令中断 */
6 ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
7 ldr pc, =PrefAbort_Handler /* 预取终止中断 */
8 ldr pc, =DataAbort_Handler /* 数据终止中断 */
9 ldr pc, =NotUsed_Handler /* 未使用中断 */
10 ldr pc, =IRQ_Handler /* IRQ 中断 */
11 ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
12 //上面这些函数名都是自己定义的,后面需要具体实现其具体任务处理
13 /* 复位中断 */
14 Reset_Handler:
15 /* 复位中断具体处理过程 */
16
17 /* 未定义中断 */
18 Undefined_Handler:
19 ldr r0, =Undefined_Handler
20 bx r0
21
22 /* SVC 中断 */
23 SVC_Handler:
24 ldr r0, =SVC_Handler
25 bx r0
26
27 /* 预取终止中断 */
28 PrefAbort_Handler:
29 ldr r0, =PrefAbort_Handler
30 bx r0
31
32 /* 数据终止中断 */
33 DataAbort_Handler:
34 ldr r0, =DataAbort_Handler
35 bx r0
36
37 /* 未使用的中断 */
38 NotUsed_Handler:
39 ldr r0, =NotUsed_Handler
40 bx r0
41
42
43 /* IRQ 中断!重点!!!!! */
44 IRQ_Handler:
45 /* 复位中断具体处理过程 */
46
47 /* FIQ 中断 */
48 FIQ_Handler:
49 ldr r0, =FIQ_Handler
50 bx r0
在编写复位中断服务函数和 IRQ 中断服务函数之前我们还需了解一些其它知识。如下:
2、Cortex-A中断向量偏移
裸机历程都是从0X87800000开始的,因此需要设置中断向量偏移。
void int_init(void)
{
GIC_Init(); //初始化GIC
system_irqtable_init(); //初始化中断表
__set_VBAR((uint32_t)0x87800000) //中断向量偏移,偏移到起始地址
}
3、GIC中断控制器
1、GIC总览
STM32(Cortex-M)的中断控制器叫做 NVIC, I.MX6U(Cortex-A)的中断控制器叫做 GIC。GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。目前 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 内核,这四个信号的含义如下:
- VFIQ:虚拟快速 FIQ
- VIRQ:虚拟外部 IRQ
- FIQ:快速中断 IRQ
- IRQ:外部中断 IRQ
VFIQ 和 VIRQ 是针对虚拟化的,只学 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号。GIC如何完成此工作—>GICV2 的逻辑图如下图:
左侧部分是中断源,中间部分是 GIC 控制器,最右侧是中断控制器向处理器内核发送中断信息。我们重点要看的是中间GIC 部分,GIC 将众多的中断源分为分为三类:
- ①、SPI(Shared Peripheral Interrupt),共享中断,所有核-Core 共享的中断,这是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
- ②、PPI(Private Peripheral Interrupt),私有中断,GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
- ③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间通信。
2、GIC逻辑分块
GIC 架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。
这两个逻辑块的含义如下:
Distributor(分发器端):由上面GICV2总体框图可看出,此逻辑块负责处理各中断事件的分发问题,也就是:中断事件应发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:
①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组 0 还是组 1。
CPU Interface(CPU 接口端): CPU 接口端是和 CPU Core 相连接的,因此在上面GICV2总体框图中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。 CPU 接口端就是分发器和 CPU Core 之间的桥梁, CPU 接口端主要工作如下:
①、使能或者关闭发送到 CPU Core 的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。
文件 core_ca7.h 定义了 GIC 结构体,此结构体里的寄存器分为分发器端和 CPU 接口端,寄存器定义如下:
/*GIC 寄存器描述结构体*/
1 typedef struct
2 {
3 /* 分发器端寄存器 */
4 uint32_t RESERVED0[1024];
5 __IOM uint32_t D_CTLR; /* Offset: 0x1000 (R/W) */
6 __IM uint32_t D_TYPER; /* Offset: 0x1004 (R/ ) */
7 __IM uint32_t D_IIDR; /* Offset: 0x1008 (R/ ) */
8 uint32_t RESERVED1[29];
9 __IOM uint32_t D_IGROUPR[16]; /* Offset: 0x1080 - 0x0BC (R/W) */
10 uint32_t RESERVED2[16];
11 __IOM uint32_t D_ISENABLER[16];/* Offset: 0x1100 - 0x13C (R/W) */
12 uint32_t RESERVED3[16];
13 __IOM uint32_t D_ICENABLER[16];/* Offset: 0x1180 - 0x1BC (R/W) */
14 uint32_t RESERVED4[16];
15 __IOM uint32_t D_ISPENDR[16]; /* Offset: 0x1200 - 0x23C (R/W) */
16 uint32_t RESERVED5[16];
17 __IOM uint32_t D_ICPENDR[16]; /* Offset: 0x1280 - 0x2BC (R/W) */
18 uint32_t RESERVED6[16];
19 __IOM uint32_t D_ISACTIVER[16];/* Offset: 0x1300 - 0x33C (R/W) */
20 uint32_t RESERVED7[16];
21 __IOM uint32_t D_ICACTIVER[16];/* Offset: 0x1380 - 0x3BC (R/W) */
22 uint32_t RESERVED8[16];
23 __IOM uint8_t D_IPRIORITYR[512];/* Offset: 0x1400 - 0x5FC (R/W) */
24 uint32_t RESERVED9[128];
25 __IOM uint8_t D_ITARGETSR[512];/* Offset: 0x1800 - 0x9FC (R/W) */
26 uint32_t RESERVED10[128];
27 __IOM uint32_t D_ICFGR[32]; /* Offset: 0x1C00 - 0xC7C (R/W) */
28 uint32_t RESERVED11[32];
29 __IM uint32_t D_PPISR; /* Offset: 0x1D00 (R/ ) */
30 __IM uint32_t D_SPISR[15]; /* Offset: 0x1D04 - 0xD3C (R/ ) */
31 uint32_t RESERVED12[112];
32 __OM uint32_t D_SGIR; /* Offset: 0x1F00 ( /W) */
33 uint32_t RESERVED13[3];
34 __IOM uint8_t D_CPENDSGIR[16];/* Offset: 0x1F10 - 0xF1C (R/W) */
35 __IOM uint8_t D_SPENDSGIR[16];/* Offset: 0x1F20 - 0xF2C (R/W) */
36 uint32_t RESERVED14[40];
37 __IM uint32_t D_PIDR4; /* Offset: 0x1FD0 (R/ ) */
38 __IM uint32_t D_PIDR5; /* Offset: 0x1FD4 (R/ ) */
39 __IM uint32_t D_PIDR6; /* Offset: 0x1FD8 (R/ ) */
40 __IM uint32_t D_PIDR7; /* Offset: 0x1FDC (R/ ) */
41 __IM uint32_t D_PIDR0; /* Offset: 0x1FE0 (R/ ) */
42 __IM uint32_t D_PIDR1; /* Offset: 0x1FE4 (R/ ) */
43 __IM uint32_t D_PIDR2; /* Offset: 0x1FE8 (R/ ) */
44 __IM uint32_t D_PIDR3; /* Offset: 0x1FEC (R/ ) */
45 __IM uint32_t D_CIDR0; /* Offset: 0x1FF0 (R/ ) */
46 __IM uint32_t D_CIDR1; /* Offset: 0x1FF4 (R/ ) */
47 __IM uint32_t D_CIDR2; /* Offset: 0x1FF8 (R/ ) */
48 __IM uint32_t D_CIDR3; /* Offset: 0x1FFC (R/ ) */
49
50 /* CPU 接口端寄存器 */
51 __IOM uint32_t C_CTLR; /* Offset: 0x2000 (R/W) */
52 _IOM uint32_t C_PMR; /* Offset: 0x2004 (R/W) */
53 __IOM uint32_t C_BPR; /* Offset: 0x2008 (R/W) */
54 __IM uint32_t C_IAR; /* Offset: 0x200C (R/ ) */
55 __OM uint32_t C_EOIR; /* Offset: 0x2010 ( /W) */
56 __IM uint32_t C_RPR; /* Offset: 0x2014 (R/ ) */
57 __IM uint32_t C_HPPIR; /* Offset: 0x2018 (R/ ) */
58 __IOM uint32_t C_ABPR; /* Offset: 0x201C (R/W) */
59 __IM uint32_t C_AIAR; /* Offset: 0x2020 (R/ ) */
60 __OM uint32_t C_AEOIR; /* Offset: 0x2024 ( /W) */
61 __IM uint32_t C_AHPPIR; /* Offset: 0x2028 (R/ ) */
62 uint32_t RESERVED15[41];
63 __IOM uint32_t C_APR0; /* Offset: 0x20D0 (R/W) */
64 uint32_t RESERVED16[3];
65 __IOM uint32_t C_NSAPR0; /* Offset: 0x20E0 (R/W) */
66 uint32_t RESERVED17[6];
67 __IM uint32_t C_IIDR; /* Offset: 0x20FC (R/ ) */
68 uint32_t RESERVED18[960];
69 __OM uint32_t C_DIR; /* Offset: 0x3000 ( /W) */
70 } GIC_Type;
结构体 GIC_Type 就是 GIC 控制器,列举了 GIC 控制器的所有寄存器,可通过此结构体来访问 GIC 的所有寄存器。第 5 行是 GIC 分发器端相关寄存器,其相对于 GIC 基地址偏移为 0X1000,获取到 GIC 基地址以后只需加上 0X1000 即可访问 GIC 分发器端寄存器。第 51 行是 GIC 的 CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000,获取 GIC 基地址后只需加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。
CP15协处理器的c15寄存器设置为CBAR寄存器,读取CBAR寄存器即可得到GIC的基地址。CBAR寄存器中保存了GIC的寄存器组的基地址,如下表格记录了GIC的寄存器相对于基地址偏移的定义。CP15协处理器细节在后面部分,可以跳过去看一下。
4、I.MX6U中断号(中断ID)
中断源有很多,为区分这些不同中断源要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,那么这三类中断是具体如何分配这 1020 个中断 ID 如下:
- ID0~ID15:这 16 个 ID 分配给 SGI。
- ID16~ID31:这 16 个 ID 分配给 PPI。
- ID32~ID1019:这 988 个 ID 分配给 SPI
像 GPIO 中断、串口中断等这些外部中断都属于SPI,具体到某个 ID 对应哪个中断由半导体厂商定义。比如 I.MX6U 总共使用了 128个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160个(私有中断和软件中断不包含在下表中,下 表中仅有128个共享的中断,如下表中IRQ0对应ID32,ID0~31为PPI和SGI),中断源如下表,表格并没有给出 I.MX6U 完整的中断源,完整中断源查阅《I.MX6ULL 参考手册》的 3.2 小节:
打开裸机例程“9_int”,前面移植了 NXP 官方 SDK中的文件 MCIMX6Y2C.h文件中定义了一个枚举类型 IRQn_Type,此枚举类型就枚举出了 I.MX6U 的所有中断,代码如下所示:
中断向量代码
1 #define NUMBER_OF_INT_VECTORS 160 /* 中断源 160 个, SGI+PPI+SPI*/
2
3 typedef enum IRQn {
4 /* Auxiliary constants */
5 NotAvail_IRQn = -128,
6
7 /* Core interrupts */ 32个
8 Software0_IRQn = 0,
9 Software1_IRQn = 1,
10 Software2_IRQn = 2,
11 Software3_IRQn = 3,
12 Software4_IRQn = 4,
13 Software5_IRQn = 5,
14 Software6_IRQn = 6,
15 Software7_IRQn = 7,
16 Software8_IRQn = 8,
17 Software9_IRQn = 9,
18 Software10_IRQn = 10,
19 Software11_IRQn = 11,
20 Software12_IRQn = 12,
21 Software13_IRQn = 13,
22 Software14_IRQn = 14,
23 Software15_IRQn = 15,
24 VirtualMaintenance_IRQn = 25,
25 HypervisorTimer_IRQn = 26,
26 VirtualTimer_IRQn = 27,
27 LegacyFastInt_IRQn = 28,
28 SecurePhyTimer_IRQn = 29,
29 NonSecurePhyTimer_IRQn = 30,
30 LegacyIRQ_IRQn = 31,
31
32 /* Device specific interrupts */ 128个
33 IOMUXC_IRQn = 32,
34 DAP_IRQn = 33,
35 SDMA_IRQn = 34,
36 TSC_IRQn = 35,
37 SNVS_IRQn = 36,
…… ...... ......
151 ENET2_1588_IRQn = 153,
152 Reserved154_IRQn = 154,
153 Reserved155_IRQn = 155,
154 Reserved156_IRQn = 156,
155 Reserved157_IRQn = 157,
156 Reserved158_IRQn = 158,
157 PMU_IRQ2_IRQn = 159
158} IRQn_Type;
对于所有的外设中断,触发的只有IRQ中断,IRQ中断服务函数只有一个,我们需要在IRQ中断服务函数里面获取触发当前中断的中断号,然后根据此中断号再去找到其对应的中断服务函数去执行。通过一个函数数组,存储了各个服务函数,待中断触发之后,在此数组中寻找其对应的服务函数去执行。
5、CP15协处理器
CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到, CP15 协处理器一共有16 个 32 位寄存器。c0~c15,本次只用到了c0、 c1、 c12 和 c15 这四个寄存器。
其他的寄存器参考:《 ARMArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》 第 1469 页“B3.17 Oranizationof the CP15 registers in a VMSA implementation”。《Cortex-A7 Technical ReferenceManua.pdf》 第55 页“Capter 4 System Control”。
访问CP15 协处理器通过如下另个指令完成:
- MRC: 将 CP15 协处理器中的寄存器(c0~c15)数据读到 ARM 寄存器(r0~r15)中。
- MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
MRC 就是读 CP15 寄存器, MCR 就是写 CP15 寄存器。
MCR 指令格式:
MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
- cond:指令执行的条件码,如果忽略的话就表示无条件执行。
- opc1:协处理器要执行的操作码。
- Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
- CRn:CP15 协处理器的目标寄存器。
- CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测。
- opc2: 可选的协处理器特定操作码,当不需要的时候要设置为 0。
MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 是目标寄存器,也就是从CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 是源寄存器,也就是要读取的写处理器寄存器。
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:
MRC p15, 0, r0, c0, c0, 0
1、c0 寄存器
CP15 协处理器有 16 个 32 位寄存器, c0~c15,在使用 MRC 或者 MCR 指令访问这 16 个
寄存器的时候,指令中的 CRn、 opc1、 CRm 和 opc2 通过不同的搭配,其得到的寄存器含义是
不同的。比如 c0 在不同的搭配情况下含义如下图:
上图中当 MRC/MCR 指令中的 CRn=c0, opc1=0, CRm=c0, opc2=0 的时,表示此时的 c0 是 MIDR 寄存器,也就是主 ID 寄存器,这也是 c0 的基本作用。对于 Cortex-A7内核来说, c0 作为 MDIR 寄存器的时候其含义如下图:
- bit31:24:厂商编号, 0X41- ARM。
- bit23:20:内核架构的主版本号, ARM 内核版本一般使用 rnpn 来表示,比如 r0p1,其中 r0后面的 0 就是内核架构主版本号。
- bit19:16:架构代码,0XF,ARMv7 架构。
- bit15:4:内核版本号,0XC07,Cortex-A7 MPCore 内核。
- bit3:0:内核架构的次版本号, rnpn 中的 pn,比如 r0p1 中 p1 后面的 1 就是次版本号。
2、c1 寄存器
c1 寄存器同样通过不同的配置,其代表的含义也不同,如下图:
在上图中:当 MRC/MCR 指令中的 CRn=c1, opc1=0, CRm=c0, opc2=0 的时候就表示此时的 c1 就是 SCTLR 寄存器(系统控制寄存器),这是 c1 的基本作用。SCTLR 寄存器主要是完成控制功能的,比如使能或者禁止 MMU、 I/D Cache 等, c1 作为 SCTLR 寄存器的时其含义下图:
SCTLR 的位比较多,我们只看本章会用到的几位:
- bit13: V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可以使用 VBAR 来重映射此基地址,也就是中断向量表重定位。为 1 的话中断向量表基地址为0XFFFF0000,此基地址不能被重映射。
- bit12: I, I Cache 使能位,为 0 的话关闭 I Cache,为 1 的话使能 I Cache。
- bit11: Z,分支预测使能位,如果开启 MMU 的话,此位也会使能。
- bit10: SW, SWP 和 SWPB 使能位,当为 0 的话关闭 SWP 和 SWPB 指令,当为 1 时使能 SWP 和 SWPB 指令。
- bit9:3:未使用,保留。
- bit2: C,D Cache 和缓存一致性使能位,为 0 时禁止D Cache 和缓存一致性,为1时使能。(也就是D Cache的打开和关闭)
- bit1: A,内存对齐检查使能位,为 0 的时候关闭内存对齐检查,为 1 时使能内存对齐检查。
- bit0: M, MMU 使能位,为 0 的时候禁止 MMU,为 1 的时候使能 MMU。
如果要读写 SCTLR 的话,可使用如下命令:
MRC p15, 0, <Rt>, c1, c0, 0 ; 读取 SCTLR 寄存器,数据保存到 Rt 中。
MCR p15, 0, <Rt>, c1, c0, 0 ; 将 Rt 中的数据写到 SCTLR(c1)寄存器中。
3、c12 寄存器
c12 寄存器通过不同的配置,其代表的含义也不同,如下图:
在上图中当 MRC/MCR 指令中的 CRn=c12, opc1=0, CRm=c0, opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中,比如之前例程中,代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下:
ldr r0, =0X87800000 ; r0=0X87800000
MCR p15, 0, r0, c12, c0, 0 ; 将 r0 里面的数据写入到 c12 中,即 c12=0X87800000
4、c15 寄存器
c15 寄存器也可以通过不同的配置得到不同的含义,参考文档《Cortex-A7 Technical
ReferenceManua.pdf》 第 68 页“4.2.16 c15 registers”,其配置如下图:
上图中,我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR中,我们可以通过如下命令获取到 GIC 基地址:
MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中。
获取到 GIC 基地址后就可以设置 GIC 相关寄存器了,比如我们可以读取当前中断 ID,当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址相对于 CPU 接口端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下:
MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器GIC_IAR 的值
简单总结一下,通过 c0 寄存器可获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、 I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。CP15协处理器其他寄存器用到了再去查文档。
6、中断使能
中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020
个中断源的使能。
1、 IRQ 和 FIQ 总中断使能
IRQ 和 FIQ 分别是外部中断和快速中断的总开关,就类似电源总闸,然后ID0~ID1019 这 1020 个中断源就类似各个电器开关。若要想开电视,那肯定要保证进户总电闸是打开的,因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断(我们未用到FIQ)。之前学到的寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ; F=1 禁止 FIQ, F=0 使能 FIQ。
还有如下更简单的指令来完成 IRQ 或者 FIQ 的使能和禁止,如下图表:
2、 ID0~ID1019 中断使能和禁止
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 中断,bit[31:16]对应 ID31~16 的 PPI 中断。剩下的GICD_ISENABLER1 ~ GICD_ISENABLER15 就是控制 SPI 中断的。GICD_ICENABLER同理。
7、中断优先级设置
Cortex-M 的中断优先级分为抢占优先级和子优先级,两者是可以配置的。Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可配置的。GIC 控制器最多可支持 256 个优先级,数字越小,优先级越高! Cortex-A7 选择了 32 个优先级。使用中断的时候需初始化 GICC_PMR 寄存器,此寄存器用来配置优先级数,寄存器结构如下图:
由图可看出:GICC_PMR 寄存器只低 8 位有效,这 8 位对应的优先级数设置如下表:
I.MX6U 是 Cortex-A7内核,支持 32 个优先级, 因此 GICC_PMR 要设置为 11111000。
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 决定,GICC_BPR 寄存器结构如下图:
寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级各占用的位数也不同,配置如下表:
一般情况,将所有的中断优先级位都配置为抢占优先级,比如:I.MX6U 的优先级位数为 5(1111 1000,共32 个优先级),所以可以设置 Binary point 为 2,这样 bit7~3 这 5 个优先级位全部为抢占优先级。
优先级设置
设置好 I.MX6U 一共有 32 个抢占优先级。具体使用某中断时就可以设置其优先级为 0~31。某个具体中断 ID 其中断优先级由寄存器 D_IPRIORITYR 设置,Cortex-A7 使用了 512 个中断 ID,每个中断 ID 都配有一个优先级寄存器,一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:3 设置优先级,也就是说实际的优先级要左移 3 位。
比如:设置 ID40 中断的优先级为 5,则:GICD_IPRIORITYR[40] = 5 << 3
优先级设置主要有三部分,通过三个寄存器来设置:
①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。GICC_PMR
②、设置抢占和子优先级位数,一般可将所有的位数都设置为抢占优先级。GICC_BPR
③、设置指定中断 ID 的优先级,也就是设置外设优先级。D_IPRIORITYR
8、中断服务函数编写
需要编写的有两部分中断服务函数:一个是IRQ中断服务函数的编写,另一个是在IRQ中断服务函数里面去查找并运行的具体的外设中断服务函数。编写按键中断程序:KEY0使用UART1_CTS这个IO。编写UART1_CTS中断代码。
1、添加中断向量表(8个)
.global _start /* 全局标号 */
/*_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(快速中断) */
2、编写复位中断服务函数
1、步骤
①、关闭 I.Cache-缓存指令 、 D.Cache-缓存数据 和 MMU-内存管理单元,操作SCTLR寄存器。
②、设置中断向量偏移:将新的中断向量表首地址写入到CP15协处理器的VBAR寄存器。
③、设置处理器9种工作模式下对应的sp指针,要使用中断必须设置IRQ模式下的sp指针。这里直接设置好所有模式下的sp指针
④、清除bss段
⑤、跳到C函数,也就是main函数
2、代码
/* 复位中断 */
Reset_Handler:
cpsid i /* 关闭全局中断 */
/* 关闭 I,DCache 和 MMU。采取读出-改完-写入的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取 CP15 的 C1 寄存器(SCTLR寄存器)到 R0 中 */
bic r0, r0, #(0x1 << 12) /* 清除 C1 的 I 位,关闭 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除 C1 的 C 位,关闭 D Cache */
bic r0, r0, #0x2 /* 清除 C1 的 A 位,关闭对齐检查 */
bic r0, r0, #(0x1 << 11) /* 清除 C1 的 Z 位,关闭分支预测 */
bic r0, r0, #0x1 /* 清除 C1 的 M 位,关闭 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将 r0 的值写入到 CP15 的 C1 (SCTLR寄存器)中 */
/*设置中断向量偏移*/
#if 0
/* 汇编版本 */
ldr r0, =0X87800000
dsb /*数据同步隔离:保证它之前的存储器访问操作都执行完毕之后,才执行它后面的指令*/
isb /*指令同步隔离:保证它之前的指令都执行完毕才执行后面的指令*/
mcr p15, 0, r0, c12, c0, 0 /*将r0寄存器里面保存的0x87800000写入到VBAR寄存器(c12)*/
dsb
isb
#endif
/*清除BSS段*/
ldr r0,_bss_start
ldr r1,_bss_end
mov r2,#0
bss_loop:
stmia r0!,{r2}
cmp r0,r1 /*比较r0和r1里面的值*/
ble bss_loop /*如果r0地址小于等于r1,继续清除bss段*/
/* 设置各个模式下的栈指针。注意:IMX6UL的堆栈向下增长!
* 堆栈指针地址一定要是 4 字节地址对齐!!!
* DDR 范围:0X80000000~0X9FFFFFFF 或者 0X8FFFFFFF
*/
/* 进入 IRQ 模式 */
mrs r0, cpsr /* 读取cpsr到r0 */
bic r0, r0, #0x1f /* 将 r0 的bit4~0 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x12 /* r0 或上 0x12,表示使用 IRQ 模式 */
msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
ldr sp, =0x80600000 /* IRQ 模式栈首地址为 0X80600000,大小为 2MB */
/* 进入 SYS 模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x1f /* r0 或上 0x1f,表示使用 SYS 模式 */
msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
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 中 */
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 函数 */
3、编写IRQ中断服务函数。
1、步骤
push {lr} /* 保存 lr 地址,push入栈来保存现场,便于中断完成之后返回此节点*/
push {r0-r3, r12} /* 保存 r0-r3, r12 寄存器 */
首先push压栈,保存当前lr,r0~r3,r12这些寄存器地址
mrs r0, spsr /* 读取 spsr 寄存器 */
push {r0} /* 保存 spsr 寄存器,push压入栈中*/
然后读取spsr:当前程序状态寄存器,压栈,以便于返回中断发生时的状态
MRC{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
mrc p15, 4, r1, c15, c0, 0
此行指令读取CP15的CBAR寄存器。(CBAR寄存器中保存了GIC这个控制器的那些寄存器组首地址)。此句将CBAR寄存器中的值保存到r1寄存器中
add r1, r1, #0X2000
此句代码进行基地址偏移,偏移0x2000,也就是CPU接口端的起始地址
r0, [r1, #0XC]
此句代码将CPU接口端起始地址加上0x0C偏移的地址下的值保存到r0寄存器。意思为:读取CPU接口端的GICC_IAR寄存器bit9~0(bit9~0即为:当前发生中断的中断ID)保存到r0寄存器里。读取中断ID的目的:根据中断ID来绝对调用对应中断服务函数
GICC_IAR寄存器位定义如下图:不仅保存着中断ID,还有CPU ID
cps #0x13
ldr r2, =system_irqhandler
blx r2 /* 运行 C 语言中断处理函数,带有一个参数 */
由IRQ模式切换到SVC模式,SVC模式允许其他中断发生。然后,加载 C 语言中断处理函数到 r2 寄存器中,然后blx跳转到r2里面执行中断服务函数system_irqhandler,此函数有一个参数:GICC_IAR寄存器的值,此参数保存在r0中,传参给C语言函数system_irqhandler,此函数需要自行C语言编写。形参可以由r0~r3这四个寄存器来传递,若形参数大于4个,大于4个的形参使用堆栈进行传递。所以这里给r0寄存器写入中断号就可完成此参数的参数传递。
pop {lr} /* 执行完 C 语言中断服务函数, lr 出栈 */
cps #0x12 /* 进入 IRQ 模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写 EOIR */
执行完system_irqhandler之后,pop出栈,退回到lr寄存器中所保存的system_irqhandler中断发生前的地址位置,恢复现场。之后再重新由SVC模式进入到IRQ模式,r0,r1出栈,str将r0写入到r1加0x10偏移之后的地址下面去:将GICC_IAR寄存器的值写入到GICC_EOIR寄存器。(规定:中断处理函数处理完之后,必须将对应发生的中断ID写入到GICC_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 */
出栈恢复现场操作,最后一句将 lr-4 赋值给pc
为什么不是直接将lr赋值给pc?
ARM 的指令是三级流水线:取指、译指、执行。(类似于一种预处理的设置,要预取指)pc 指向的是正在取值的地址,如下举例:左侧一列是地址,中间是指令,最右边是流水线。
0X2000 MOV R1, R0 ;执行 0X2004 MOV R2, R3 ;译指 0X2008 MOV R4, R5 ;取值 //PC里保存着这句指令
当前正执行 0X2000地址处的指令“MOV R1, R0”,但 PC 里保存着 0X2008 地址处的指令“MOV R4, R5”。若此时发生中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008。当中断处理完后肯定要回被中断点接着执行,若直接跳转到 lr 里保存的地址处(0X2008)运行,则地址 0X2004 处的指令“MOV R2, R3” 指令得不到执行,即会产生错误!所以要将 lr- 4 赋值给 pc,也就是 pc=0X2004,从指令“MOV R2,R3”开始执行。
2、代码
/* IRQ 中断服务函数 !!*/
IRQ_Handler:
push {lr}
push {r0-r3, r12}
mrs r0, spsr /* 读取 spsr 寄存器 */
push {r0} /* 保存 spsr 寄存器,push压入栈中*/
mrc p15, 4, r1, c15, c0, 0
add r1, r1, #0X2000
r0, [r1, #0XC]
push {r0, r1} /* 保存 r0,r1 */
cps #0x13 /* 进入 SVC 模式,允许其他中断再次进去 */
push {lr} /* 保存 SVC 模式的 lr 寄存器 */
ldr r2, =system_irqhandler /* 加载 C 语言中断处理函数到 r2 寄存器中*/
blx r2 /* 跳转运行 C 语言中断处理函数 */
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 */
本篇为SOC-I.MX6ULL的中断系统学习的上半部分笔记,中断系统知识较多,下一篇还有下半部分,内容太多分两部分。如果文章帮上你请帮忙点个赞哦!