Linux(十二)中断系统
STM32 的中断系统主要有以下几个关键点:
中断向量表
中段向量表就是说明都有什么中断,这些中断服务函数在哪个位置。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。
中断向量表放在整个程序的最前面。ARM 处理器都是从地址 0X00000000 开始运行的,但是我们学习 STM32 的时候代码是下载到 0X8000000 开始的存储区域中。那是因为STM32默认会把中断向量表设置偏移量为 0X8000000。
在做STM32的boot时,就需要修改APP程序的运行地址以及修改中断向量表偏移量,比如说boot是从0X8000000,那么boot的中断向量表就在0X8000000;而boot大小为0x4000,那么我设置APP的中断向量表就在0X8004000.
NVIC(内嵌向量中断控制器)
中断系统得有个管理机构,对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫做 NVIC,全称叫做 Nested Vectored Interrupt Controller。 Cortex-A 内核的中断管理机构不叫做NVIC,而是叫做 GIC,全称是 general interrupt controller.
中断使能
中断服务函数
STM32的中断服务函数一般放在stm32f1xx_it.c中,比如void SysTick_Handler(void);void EXTI2_IRQHandler(void);void USART1_IRQHandler(void)......
我们在这些函数中去添加用户处理的操作,比如回调函数、释放信号量等。
====================================================================================================
I.MX6U的中断系统主要有以下几个关键点:
I.MX6U也是需要以上这些,不同的是STM32在中断向量表把所有的中断列出来,而I.MX6U的中断向量表只有八个大类中断。其中有一个中断未使用,所以只有七类中断。
当然不会只有这么少的中断,像UART、定时器、触发中断等都属于外部中断,它们被放入了IRQ中断中。
复位中断就是复位时产生的中断,可以用来进行初始化,恢复默认设置等。
- 复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、 DDR 等等。
- 未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
- 软中断(Software Interrupt,SWI), 由 SWI 指令引起的中断, Linux 的系统调用会用 SWI 指令来引起软中断,通过软中断来陷入到内核空间。
- 指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
- 数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
- IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中断的发生。
- FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中
以下是在start.S中的中断向量表,中断向量表处于程序最开始的地方。
第9 到 16 行是中断向量表,当指定的中断发生以后就会调用对应的中断复位函数,比如复位中断发生以后就会执行第 9 行代码,也就是调用函数 Reset_Handler,函数 Reset_Handler就是复位中断的中断复位函数,其它的中断同理。
第 19 到 55 行就是对应的中断服务函数,中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断本教程没有用到,所以都是死循环。
中断服务函数就是当某种中断发生时进行相应的处理,类似中断回调函数的作用。
====================================================================================================
GIC
GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ,他们之间的关系如图 所示:
VFIQ:虚拟快速 FIQ。VIRQ:虚拟快速 IRQ。FIQ:快速中断 IRQ。IRQ:外部中断 IRQ。
我的理解是:和NVIC一样,GIC也是一个中断控制器,负责管理中断的,他把进入的中断0-中断N分为四类,并且筛选优先级最高的中断告知ARM内核进行具体的处理工作。
1.GIC架构总览
目前 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 个核。所以图中描述的processor0-7就是这八个核。
图 17.1.3.1 中左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分。
2. 中断源
①、 SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、 PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
3. 中断ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,那么这三类中断是如何分配这 1020 个中断 ID 的呢?这 1020 个 ID 分配如下:
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 ,至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。
SGI、PPI是必须拥有的,各16个。但是SPI部分,I.MX6U只支持了128个,也就是虽然GIC分配了988个ID给SPI,但I.MX6U用不了那么多,也是OK的。因此I.MX6U共有16+16+128=160个中断ID,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 Cortex A7 interrupts”小节。
4. GIC逻辑分块
GIC 架构分为了两个逻辑块:Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。
Distributor(分发器端): 从图 17.1.3.2 可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:
Distributor就是负责管理中断(优先级、触发方式等)
CPU Interface(CPU 接口端): CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在图 17.1.3.2 中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。 CPU 接口端就是分发器和 CPU Core 之间的桥梁, CPU 接口端主要工作如下:
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。
CPU Interface负责GIC与CPU相连的接口,前面distributor分发器把中断管理了,但是GIC并不知道如何向CPU通知中断,CPU interface就是做一个桥梁的,包括多个中断到来时把优先级最高的通知给CPU core.
文件 core_ca7.h 定义了 GIC 结构体,此结构体里面的寄存器分为了分发器端和 CPU 接口端.
GIC 的分发器端相关寄存器相对于 GIC 基地址偏移为 0X1000,因此我们获取到GIC 基地址以后只需要加上 0X1000 即可访问 GIC 分发器端寄存器。
GIC 的 CPU 接口端相关寄存器相对于 GIC 基地址的偏移为 0X2000,同样的,获取到 GIC 基地址以后只需要加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。
总结下GIC中断管理器:GIC中断管理器顾名思义是对中断进行管理的,给各中断分配了中断ID;当有中断产生时,GIC分发器就能通过中断ID进行管理,转向CPU interface;CPU interface从多个中断筛选优先级最高的中断发送至CPU core;CPU core进入中断服务函数进行处理。
====================================================================================================
CP15
那么GIC的基地址如何得知呢?
这就需要引入CP15 , CP15协处理器一般用于存储系统管理,但是在中断中也会使用到, CP15 协处理器一共有16 个 32 位寄存器。
我对CP15的理解是协助ARM内核管理的寄存器组,内核有太多太多寄存器了,如何方便去设置去获取某个寄存器呢?CP15可以帮助我们使用所有的寄存器。
MRC 就是读 CP15 寄存器, MCR 就是写 CP15 寄存器, MCR 指令格式如下:
MCR/MRC {cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。 CP15 至 ARM <CRn> 至 <Rt>
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。 ARM 至 CP15 <Rt> 至 <CRn>
Rt: ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将
opc2: 可选的协处理器特定操作码,当不需要的时候要设置为 0。
假如:我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:
CP15 协处理器有 16 个 32 位寄存器, c0~c15,在使用 MRC 或者 MCR 指令访问这 16 个寄存器的时候,指令中的 CRn、 opc1、 CRm 和 opc2 通过不同的搭配,其得到的寄存器含义是不同的。
如下图所示:当 MRC/MCR 指令中的 CRn=c0, opc1=0, CRm=c0, opc2=0 的时候就表示此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器。
其实就是一种排列组合,这就是CP15的作用,用16个寄存器就能代表指向的好多个寄存器,并替代其进行操作,操作C0就是操作MIDR。
比如,我们修改中断向量表基地址为0X87800000,如何设置呢?
我们看到CRn=c12, opc1=0, CRm=c0, opc2=0 的时候就表示此时的 c12 就是 VBAR 寄存器,也就是设置VBAR为0X87800000。
那么GIC的基地址如何得知呢?
GIC的基地址是在CBAR中,那么CRn=c15, opc1=4, CRm=c0, opc2=0.
我们通过读取C15的命令,从CBAR中读取GIC的基地址。得到GIC基地址后,加上0x1000就是distributor分发器端的寄存器起始地址,加上0x2000就是CPU interface的寄存器起始地址。
====================================================================================================
中断使能
1. IRQ 和 FIQ 总中断使能
这个就类似于STM32的关闭/打开总中断了,在执行一些不能被打断的操作时关闭总中断,操作结束后再打开总中断。
2.ID0~ID1019 中断使能和禁止
GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。GICD_ISENABLER0 的 bit[15:0]对应ID15~0 的 SGI 中断,GICD_ISENABLER0 的 bit[31:16]对应 ID31~16 的 PPI 中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
====================================================================================================
中断优先级的配置
优先级设置主要有三步:
①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。
②、设置抢占优先级和子优先级位数,一般为了简单起见,会将所有的位数都设置为抢占优先级。
STM32是如何配置中断的呢?
在STM32需要对各种中断进行配置的,中断分为抢占优先级、子优先级。优先级数越小越高,0优先级是最高优先级。
抢占优先级的作用是,中断可以嵌套,抢占优先级高的可以打断抢占优先级低的中断。
子优先级的作用是,抢占优先级相同时,可以判断子优先级比较优先级。
若内核正在执行C 的中断服务函数,则它能被抢占优先级更高的中断A 打断,由于B和C 的抢占优先级相同,所以C 不能被B 打断。但如果B 和C 中断是同时到达的,内核就会首先响应响应优先级别更高的B 中断。
1.抢占相同,子优先级不同:此时没有抢占剥夺,此时按照子优先级顺序排序
2.抢占和子优先级都相同但有先后:此时无抢占剥夺,依照FIFO,前一个执行完了才能执行后者;
3.抢占和子优先级都相同且同时到达:此时按照中断向量表顺序排先后
在操作cubemx的时候就能看到,会对中断优先级的方式进行配置。
在配置时可以看到,管理抢占优先级与子优先级共4个bits,可以*分配,但是相加固定为4个bits。
抢占优先级 |
子优先级 |
0 |
4 |
1 |
3 |
2 |
2 |
3 |
1 |
4 |
0 |
当然我用的是STM32F103,cortex-m1内核的,支持中断数量不多,但是对于Cortex-A7中断数量可不少。
I.MX6U如何配置优先级分配方式呢?
Cortex-A7 最多可以支持 256 个优先级,即共8个bits,2^8=256;
半导体厂商可以自行决定选择多少个优先级。 I.MX6U 选择了 32 个优先级,也就是选择用5个Bits去管理优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,设置该寄存器为0b11111000。
与STM32一样,也需要设置抢占优先级几个bits, 子优先级几个bits。为了使用方便,我们设置抢占优先级为5级,也就是抢占优先级共32种,那么就够用了。
I.MX6U如何设置某中断的优先级呢?
前面已经设置好了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。前面说了 Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5,示例代码如下:
====================================================================================================
驱动编写
GIC API
SDK 包中的文件 core_ca7.h 写好了有关GIC的API,我们需要学会使用这些API。
学习寄存器的操作只是为了了解它的机制,但是使用时还是要使用封装函数的,不然不仅容易出错,阅读、调试、移植也十分困难。
初始化中断、完成IRQ中断服务函数函数system_irqhandler
在我们了解了GIC的作用之后,我们知道中断是有GIC进行管理的,但是中断服务函数还是需要在CPU端完成的,所以接下来我们来编写IRQ中断服务函数,我们取名为 system_irqhandler
我们专门新建一个.c、.h文件来完成中断部分的驱动bsp_int.c bsp_int.h
system_irqhandler函数编写好了,我们就能在启动文件中的IRQ中断服务函数中去调用该函数了。
====================================================================================================
启动文件
重新编写start.S:
重点
不过从结构上看呢,一共7种中断,从中断向量表开始,根据不同的中断类型分明配置7种中断服务函数。
复位中断服务函数主要执行了一些寄存器复位,以及设置各模式下在栈中的的起始地址。
IRQ中断服务函数主要是判别是何种中断ID,并调用system_irqhandler函数去对应执行各种外部中断的服务函数。
1.进入中断服务函数前对此刻的现场进行保存(保存寄存器的数值、状态寄存器)。
如何配置KEY(GPIO1_IO18),使KEY摁下时蜂鸣器响
前面我们介绍的是GIC中断管理、中断是如何进入中断服务函数的,但是实际应用该如何下手呢?
我们想要摁下KEY能够进入中断,由原理图可知,KEY未按下时被拉高,摁下被拉低,那么一定会有一个下降沿的过程,我们要将其IO配置为GPIO,并且配置其中断为下降沿触发。
然后编写一个独立的中断服务函数由system_irqhandler 调用,在这个中断服务函数中令蜂鸣器响。同STM32一样,最后清除中断标志位,使可以再次进入中断。
步骤:
一、初始化中断环境
1. GIC_Init是GIC的API函数,就是使用中断前一定得先打开GIC吧,就好像STM32得先打开NVIC一样。
2. system_irqtable_init执行了什么呢?为了管理中断方便,我们自定义了中断管理块,把160个中断与其对应的中断服务函数对应起来。system_irqtable_init就是初始化这些中断管理块的。
3. __set_VBAR((uint32_t)0x87800000);
之前0x87800000在前面用到的时候是在Makefile,因为我们要把.bin放入DDR的0x87800000位置上,就在Makefile中将执行文件.o通过链接0x87800000生成.elf。然后再用.elf转换生成的.bin。
那么这次用0x87800000的意义是修改中断向量表偏移,因为我们本次需要用中断,而中断向量表默认是在0x00000000的位置上开始,而实际上中断向量表应始终在代码的最前端,也就是0x87800000的位置。所以需要中断向量表偏移。
二、初始化时钟、一些外设
三、初始化KEY
四、初始化KEY的中断
首先对KEY的GPIO1_IO18配置为GPIO的复用功能,然后再配置该管脚的中断模式为下降沿触发。
使能GIC中对应的中断、注册中断服务函数,这样的话触发中断后会进入gpio1_io18_irqhandler中断服务函数。
五、中断服务函数编写
即进入gpio1_io18_irqhandler后,我们该做些什么
再执行结束后,要清除中断标志位,这样下次才可以继续触发该中断。
总结
中断的内容含量特别大,但是与STM32有异曲同工之处,对比寻找异同可以有效提高理解。