STM32中中断系统主要分为以下几个部分:
① 中断向量表
② NVIC(内嵌向量中断控制器)
③ 中断使能
④ 中断服务函数
中断向量表是一个表,这个表里面存放的是中断向量
中断服务程序的入口地址或存放中 断服务程序的首地址成为中断向量
STM32的中断向量表放在了启动文件里面
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__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 RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line 0
DCD EXTI1_IRQHandler ; EXTI Line 1
DCD EXTI2_IRQHandler ; EXTI Line 2
DCD EXTI3_IRQHandler ; EXTI Line 3
DCD EXTI4_IRQHandler ; EXTI Line 4
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1
#....................#
中断向量表都是链接到代码的最前面,比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的,那么中断向量表就是 从 0X00000000 开始存放的。代码中__initial_sp就是第一条中断向量,存放的是栈顶指针,Reset_Handler 的入口地址存放的复位函数入口地址,就这样一直列下去存放各种中断入口地址。
因为STM32的处理器从ROM地址 0x00000000 开始运行的,而我们下载程序是到 0x80000000 地址的,就像下图
那么,CPU 是如何执行到 0x80000000 处的中断向量表呢?
解决方式就是 Cortex-M 架构引入了一 个新的概念:中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可,翻出代码,找到偏移配置代码如下:
其中
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
就是设置 FLASH 地址偏移
中断系统必须有个管理机构,对于 Cortex-M 内核的单片机,管理机构叫做 NVIC,对单片机的各种中断进行分级和管理,具体参考这篇文章解释:STM32的NVIC和中断的总结
中断使能就是使能对应的寄存器,在 STM32 的库函数开发中一般都是通过 NVIC 来配置对应的寄存器,就如下面的配置代码
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);
中断一个最重要的点就是中断服务韩式,在触发中断之后,通过中断向量表进入中断服务函数,执行中断代码,在STM32 中中断服务函数如下:
/* 外部中断 2 服务程序 */
void EXTI2_IRQHandler(void)
{
/* 中断处理代码 */
}
回顾了 Cortex-M 的中断原理,Cortex-A7 和 Cortex-M 的中断也差不多,首先它也有中断向量表,中断向量表也是在代码的最前面,CortexA7 内核有 8 个异常中断,这 8 个异常中断的中断向量表如表
向量地址 | 中断类型 | 中断模式 | 功能 |
---|---|---|---|
0X00 | 复位中断(Rest) | 特权模式(SVC) | 复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等 |
0X04 | 未定义指令中断(Undefined Instruction) | 未定义指令中止模式(Undef) | 未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断 |
0X08 | 软中断(Software Interrupt,SWI) | 特权模式(SVC) | 软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI 指令来引起软中断,通过软中断来陷入到内核空间 |
0X0C | 指令预取中止中断(Prefetch Abort) | 中止模式 | 指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断 |
0X10 | 数据访问中止中断(Data Abort) | 中止模式 | 数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断 |
0X14 | 未使用(Not Used) | 未使用 | |
0X18 | IRQ 中断(IRQ Interrupt) | 外部中断模式(IRQ) | IRQ 中断(IRQ Interrupt),外部中断,外设中断都会引起此中断的发生 |
0X1C | FIQ 中断(FIQ Interrupt) | 快速中断模式(FIQ) | FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断 |
注意这里的都是异常中断,有系统内部软件产生,和外部中断(定时器中断、触发中断等等)不一样
Cortex-M 中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。 CotexA 并没有这么做,在表中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IQR 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理,这种处理思想类似于回调函数处理机制!所以 IRQ 中断和所有的外部中断关系如下:
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 位芯片使用的
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 |
那么 GIC 是如何实现上报信号量的过程的呢?先看一下 GIC 的结构图:
图中分为三个部分:左边是中断源,中间是 GIC 控制器,右边则是上报的信号量,同时图中 GIC 控制器将中断源分为三个部分: SPI、PPI、SGI 三组
中断分类 | 功能 |
---|---|
SPI(Shared Peripheral Interrupt) | 共享中断,顾名思义,所有 Core 共享的中断,外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口 中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core |
PPI(Private Peripheral Interrupt) | 私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断 |
SGI(Software-generated Interrupt) | 软件中断,由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信 |
Cortex-A7 的中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是 中断 ID,每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019,这些 ID 号包含了 PPI、SPI和SGI,具体如下:
ID号 | 分配的中断源 |
---|---|
ID0~ID15 | 这 16 个 ID 分配给 SGI。 |
ID16~ID31 | 这 16 个 ID 分配给 PPI |
D32~ID1019 | 这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 |
比如 I.MX6U 的总共 使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160 个,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 CortexA7 interrupts”小节,以下是一部分:
当然,这些 ID 在 NXP 提供的 SDK 包里面有提供,可以在 MCIMX6Y2C.h 文件中的枚举类型 IRQn_Type 里面找到,代码如下:
使用的时候可以轻松调用
从刚刚的 GIC 架构图上我们可以看到 GIC 主要分为两个逻辑块:Distributor 和 CPU Interface
**Distributor(分发器端):**分发器收集所有的中断源,可以控制 每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端,主要工作如下:
① 全局中断使能控制
② 控制每一个中断的使能或者关闭
③ 设置每个中断的优先级
④ 设置每个中断的目标处理器列表
⑤ 设置每个外部中断的触发模式:电平触发或边沿触发
⑥ 设置每个中断属于组 0 还是组 1
**CPU Interface(CPU 接口端):**和 CPU Core 相连接的,每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface,其主要工作如下:
① 使能或者关闭发送到 CPU Core 的中断请求信号
② 应答中断
③ 通知中断处理完成
④ 设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core
⑤ 定义抢占策略
⑥ 当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core
关于 GIC 的寄存器配置,在 SDK 的 core_ca7.h 中有定义一个结构体,包含其配置寄存器,方便用户使用:
该文件正点原子做过修改,我们直接拷贝正点原子的例程进行使用
注意:
分发器端相关寄存器,其相对于 GIC 基地址偏移为 0X1000;
GIC 的 CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000;
Cortex-A 有一个协处理器:CP15;
操作手册在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第 1469 页 “B3.17 Oranization of the CP15 registers in a VMSA implementation“。《Cortex-A7 Technical ReferenceManua.pdf》第 55 页 “Capter 4 System Control”
CP15一般用于存储系统管理,在中断中也会使用到,其一共有 16 个 32 位寄存器。CP15 协处理器的访问通过如下两个指令完成:
MCR 写指令格式如下:
MCR{cond} p15, ,
参数 | 功能 |
---|---|
cond | 指令执行的条件码,如果忽略的话就表示无条件执行 |
opc1 | 协处理器要执行的操作码 |
Rt | ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中 |
CRn | CP15 协处理器的目标寄存器 |
CRm | 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm设置为 C0,否则结果不可预测 |
opc2 | 可选的协处理器特定操作码,当不需要的时候要设置为 0 |
MRC 读指令格式和MCR一致,MRC 的指令格式和 MCR 一样,只不过指令中 Rt 是目标寄存器
CP15 协处理器有 16 个 32 位寄存器,c0~c15,我们关注 c0、c1、c12 和 c15这四个寄存器,和中断有关
c0寄存器
c0 寄存器访问指令中的 CRn、opc1、CRm 和 opc2 通过不同的搭配,访问的结果是不同的,如下表:
当访问指令为
MRC p15, 0, r0, c0, c0, 0
此时读取的 c0 就是 MIDR 寄存器,此寄存器存放系统信息
c1寄存器
c1 和 c0 一样,不同指令访问目标寄存器地址不同
当 MRC/MCR 指令中的 CRn=c1,opc1=0,CRm=c0,opc2=0 的时候就表示 此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,SCTLR 寄存 器主要是完成控制功能的,比如使能或者禁止 MMU、I/D Cache 等
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
c15寄存器
c15 寄存器也可以通过不同的配置得到不同的含义
我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR 中,指令如下:
MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中
获取了基地址,我们通过偏移,就可以访问设置 GIC 相关寄存器,比如下面一段汇编
;获取 GIC 基地址
MRC p15, 4, r1, c15, c0, 0
;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
ADD r1, r1, #0X2000
;读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器GIC_IAR 的值
LDR r0, [r1, #0XC]
综上,这四个寄存器的功能如下:
中断使能包括两部分:
IRQ 或者 FIQ 总中断使能
ID0~ID1019 这 1020 个中断源的使能
IRQ 和 FIQ 分别是外部中断和快速中断的总开关,寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使 能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ;同时因为中断的开关操作频繁,所以有更简单的指令来控制开关:
指令 | 描述 |
---|---|
cpsid i | 禁止 IRQ 中断。 |
cpsie i | 使能 IRQ 中断。 |
cpsid f | 禁止 FIQ 中断。 |
cpsie f | 使能 FIQ 中断。 |
GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止;
对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么 就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个 GICD_ICENABLER 寄存器来完成中断的禁止
Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置;GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高!
在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级,寄存器结构如下:
优先级设置如表:
低8位 | 功能 |
---|---|
11111111 | 256 个优先级 |
11111110 | 128 个优先级 |
11111100 | 64 个优先级 |
11111000 | 32 个优先级 |
11110000 | 16 个优先级 |
I.MX6U 是 Cortex-A7 内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 11111000
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的,GICC_BPR 寄存器结构如图:
寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的中断优先级位的位数也不同,具体配置如下:
一般将所有的中断优先级位都配置为抢占优先级(这样简单),比如 I.MX6U 的优先级位数为 5 (32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级
设置完优先级后,其优先级数字越小优先级越高。具体要使用某 个中断的时候就可以设置其优先级为 0~31;中断优先级设置由寄存器 D_IPRIORITYR 来完成,如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,示例代码如下:
GICD_IPRIORITYR[40] = 5 << 3;
综合上面,优先级设置主要有三部分: