教程I.MX6U的中断系统讲解是从STM32引入的,这就对我这种没接触过STM32的小白不太友好!并且中断可以说是到目前为止最最重要的知识点。还好,STM32只是大致过了几个知识点
STM32的中断系统回顾
参考教程给出的STM32的中断系统,主要有下面几个知识点
- 中断向量表
- 向量中断控制器NVIC
- 中断使能
- 中断服务函数
我们一个个来看看!
中断向量表
中断向量表说白了也是一个表,表里放的是中断向量,中断服务程序的入口地址或者存放中断程序服务的首地址称为中断向量,所以说中断向量表也就是一系列中断服务程序入口地址组成的表。这些中断服务程序或函数在中断向量表里的位置是由半导体厂商定好的,当某个中断倍触发后会自动跳转到向量表中对应中断服务对应的入口地址。中断向量表在整个程序的最前面。
1 __Vectors DCD __initial_sp ; Top of Stack 2 DCD Reset_Handler ; Reset Handler 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
上面的这一段代码就节选自STM32F103的中断向量表,中断向量表都是链接在代码的最前面,比如ARM的处理器是从0x00000000开始执行代码,那么这个向量表就是从0x00000000开始存放的,代码第一行的__initial_sp就是第一条中断向量,存放的是栈盯指针,下面依次是复位等函数的入口地址,一直到最后这个向量表就完成了。
中断向量偏移
在第一个通过汇编点亮LED等试验中,我们是把程序链接到0x87800000,那么就是从这个地址开始执行指令的,那么不就出错了么?Cortex-M架构引入了一个新的概念:中断向量偏移,通过这个偏移就可以把向量表存储到任何地址。中断向量偏移要在函数SystemInit中完成,通过向SCB_VTOR寄存器写入新的中断向量表的首地址就行了。
内嵌向量中断控制器
内嵌向量中断控制器NVIC(Nested Vectored Interrupt Controller)是对STM32进行中断管理的机构,这个控制器负责协调各个中断的管理。具体原理这里不深究了,但是NVIC是针对Cortex-M内核的,我们这次使用的Cortex-A内核的管理机构不叫NVIC,而是GIC(General Interrupt Controller)。具体作用后面会降到。
中断使能
要想要使用某个外设引发的中断,必须要使能这个外设的中断,这个没什么可讲的,直接设置相关寄存器就可以了。
中断服务函数
我们使用中断主要目的就是使用中断服务函数,当中断发生后当前程序被挂起,中断服务函数被调用,中断服务函数执行完毕后回到原流程。
上面就是STM32整个中断系统的大致说明,Cortex-M内核的中断处理过程和Cortex-A内核的处理流程大同小异,我们看看Cortex-A的中断处理是什么样的。
Cortex-A7中断系统简介
中断向量表
和STM32一样,Cortex-A也是有个中断向量表的,这个表也是在代码的最前面,Cortex-A7内核里只有8个异常中断,这8个异常中断等中断模式如下表(ARM Cortex-A(armV7)编程手册V4.0,第11.1.1章节)
Normal Vector offset | High vector address | Non-secure | Secure | Hypervisor | Monitor |
0x0 | 0xFFFF0000 | Not used | Reset | Reset | Not used |
0x4 | 0xFFFF0004 | Undefined instruction | Undefined instruction | Undefined instruction from Hyp mode | Not used |
0x8 | 0xFFFF0008 | Supervisor Call | Supervisior Call | Secure Monitor Call | Secure Monitor Call |
0xC | 0xFFFF000C | Prefetch Abort | Prefetch Abort | Prefetch Abort from Hyp mode | Prefetch Abort |
0x10 | 0xFFFF0010 | Data Abort | Data Abort | Data Abort from Hyp mode | Data Abort |
0x14 | 0xFFFF0014 | Not used | Not used | Hyp mode entry | Not used |
0x18 | 0xFFFF0018 | IRQ Interrupt | IRQ Interrupt | IRQ Interrupt | IRQ Interrupt |
0x1C | 0xFFFF001C | FIQ Interrupt | FIQ Interrupt | FIQ Interrupt | FIQ Interrupt |
我在编程手册上只找到了上面这个中断模式的表,没有找到对应的中断类型,教程上给出了相对应的中断类型,这里截个图
可以看出来,这个中断向量表只有8个中断,而且还有个未使用,也就是说Cortex-A7的要处理的中断要比Cortex-M少的多,明显不可能。区别就是,Cortex-M在中断向量表里列出了包括所有外设的所有的中断向量,而对于Cortex-A7来说,把所有外部中断都给了IRQ中断。任何一个外部中断触发中断时都会触发IRQ中断,在IRQ中断服务中读取指定的寄存器来判定是什么中断,然后做出相应的中断处理。
异常中断介绍
从上面那个表可以看出来Cortex-A7一共用了7种异常中断,下面简单介绍一下这7种中断的作用
- Reset,复位中断,CPU复位后就会进入复位中断,在中断服务函数中我们需要做一些初始化服务,例如初始化SP指针,DDR等工作。
- Undef未定义指令中断,当指令不能识别的时候就会产生此中断。
- SWI软中断,由软件指令引发的中断,Linux系统调用会用SWI指令引发中断。
- PrefetchAbort指令预取中止中断,预取指令出错时会触发此中断。
- Data Abort 数据访问中止中断,当访问数据出错时产生此中断。
- IRQ外部中断,芯片内部的外设中断都会引发此中断。
- FIQ快速中断,需要快速处理中断时可以使用此中断。
代码构成
由于整个代码比较长,我们把将其按照这几块分割下。下面就是中断向量表的创建
.global _start _start: @添加中断向量表 ldr pc,=Reset_Handler //复位中断函数 ldr pc,=Undefined_Handler //未定义指令中断函数 ldr pc,=SVC_Handler //SVC ldr pc,=PreAbort_Handler //预取终止 ldr pc,=DataAbort_Handler //数据终止 ldr pc,=NotUsed_Handler //未使用 ldr pc,=IRQ_Handler //IRQ中断 ldr pc,=FIQ_Handler //FIQ中断 /*复位中断函数 */ Reset_Handler: ldr r0,=Reset_Handler bx r0 /*未定义指令中断服务函数 */ Undefined_Handler: ldr r0,=Undefined_Handler bx r0 /*未定义指令中断服务函数 */ Undefined_Handler: ldr r0,=Undefined_Handler bx r0 /*SVC中断服务函数 */ SVC_Handler: ldr r0,=SVC_Handler bx r0 /*预取终值中断服务函数 */ PreAbort_Handler: ldr r0,=PreAbort_Handler bx r0 /*数据终值中断服务函数 */ DataAbort_Handler: ldr r0,=DataAbort_Handler bx r0 /*未使用中断服务函数 */ NotUsed_Handler: ldr r0,=NotUsed_Handler bx r0 /*IRQ中断服务函数 */ IRQ_Handler: ldr r0,=IRQ_Handler bx r0 /*FIQ中断服务函数 */ FIQ_Handler: ldr r0,=FIQ_Handler bx r0
可以看出来,中断向量表是从_start开始的,也就是程序的入口,向量表后面紧跟着各个中断服务函数。当CPU响应中断,就会从向量表里根据定义的函数执行相应的服务。上面的每个服务里都是没有写具体的程序,只是做了个一个死循环。
GIC控制器
前面已经提到过,STM32是通过NVIC来控制中断的,而I.MX6U是通过一个叫做GIC的中断控制器来控制中断的。有个手册是专门介绍这个控制器的(ARM Generic Interrupt Controller V2.0)。
GIC是ARM公司给Cortex-A/R内核提供的一个中断控制器,有4个版本(V1~V4)。各个版本支持的Soc如下表
I.MXU6属于CortexA7架构,使用的是V2版,最多支持8个核。当GIC接受到外部中断后就会报给ARM内核,如下图所示
但是从上图中的CPU interface到Processor之间,内核只提供了4个通道用来提供信号通讯:VFIQ,VIRQ,FIQ和IRQ
上面图下标也说明了:VFIQ和VIRQ就是虚拟的FIQ和IRQ。所以我们现在只用处理FIQ和IRQ了。在这个教程里,我们只使用IRQ,相当于GIC最后只通过IRQ上报了中断的信息。那么,GIC是如何完成这个工作的呢?下面的图就是GICV2的逻辑示意图
上图中,左侧部分就是中断源,中间部分相当于GIC,右边的就是GIC向内核发送的中断信息。可以从上图里发现,GIC将外部中断分了3类:
SPI(Shared Peripheral Interrupt)共享中断(一定要注意这个SPI不是那个总线的SPI),就是所有Core都共享的中断,这个是最常见的,比如按键、串口等外部的中断都属于SPI中断,所有的Core都可以处理该中断。
PPI(Private Peripheral Interrupt)私有中断,我们上面说过,V2版本的GIC最多支持8个核,那么每个核都会有一些自己独有的中断,这些中断需要指定的核去处理。
SGI(Software-generated Interrupt)软件中断,有软件触发的中断,通过向寄存器GICD_SGIR写入数据来触发,系统会使用SGI中断来完成多核之间的通讯。
中断ID
因为所有的外部中断源最终都是引发的IRQ,那么为了区分不同的中断源需要给他们分配一个唯一的ID,这些ID就是中断ID。每一个CPU支持1020个中断ID,编号为ID0~ID1019.这1020个ID包含了SGI、PPI和SPI,他们是如下分配的:
- ID0~ID15:共16个,分配给SGI
- ID16~ID31:共16个分配给PPI
- ID32~ID1019:988个分配给SPI
这些ID是半导体厂商事先定义好的,I.MX6ULL(注意这里是6ULL)我们要去参考手册里查,可以发现6ULL使用了128个中断ID,所以加上前面的32个ID,一共有160个中断ID。这128个中断ID对应的中断源在参考手册第三章可以查到
注意IRQ的起始值不是从0开始的,是从32开始的,前面32个是预留给PPI和SPI的。我们前面移植了恩智浦官方的SDK,里面提供了一个枚举类型IRQn_Type,里面就枚举了所有的中断。(6UL和6ULL的中断描述不太一样,教程里截取的表格是6ULL的)
GIC的逻辑分区
GIC架构按照功能逻辑分了两个逻辑块:Distributor分发器和 CPU Interface,两个逻辑块作用是这样的
Distributor
负责处理各个中断时间段分发问题,决定中断事件发送到那个CPU Interface,分发器收集所有的中断源,控制每个控制的优先级并将优先级高的事件发送给CPU接口。其主要工作如下:
- 全局中断使能控制
- 控制每个中断的使能
- 设置每个中断的优先级
- 设置每个中断等目标处理器列表
- 设置每个外部中断等触发模式:电平触发或边沿触发
- 设置每个中断属于组0还是组1
CPU Interface
CPU端口是和CPU核相连接,所以每个CPU核都有个与其对应的CPU Interface。它是分发器和CPU核之间的桥梁,主要工作如下:
- 使能或关闭发送到CPU核对终端请求信号
- 应答中断
- 通知中断处理完成
- 设置优先级掩码,通过掩码来设置哪些中断不需要上报给CPU核处理
- 定义抢占策略
- 当多个中断请求时,选择优先级高度中断通知CPU核。
下面的表《Cortex-A7 Technical ReferenceManua.pdf》8.2.1GIC memory-map就是Distributor和CPU接口的偏移地址
CP15协处理器
这一小节讲的比较散,因为教程上是直接跳到这里就修改寄存器的值达到关闭I Cache、D Cache和MMU的目的。只有过程,原因不是很清楚。
我们在GIC手册里看一下相关寄存器的地址
注意表格抬头,是个偏移地址。这个偏移量基于GICD的偏移地址这个GICD就是上面那个GIC Memory Map的Distributor的地址上偏移来的(0x1000~0x1FFF),但是GICD的基地址也是个基于GIC基地址偏移量,要获取GIC的基地址,就要用到一个叫做CP15协处理器的东西。
CP15协处理器用于系统存储管理,但是在中断中也能用到,CP15一共只用16个32位的寄存器,需要通过下面两条指令完成操作
MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2> MRC{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2> cond:指令执行的条件码,如果忽略的话就表示无条件执行。 opc1:协处理器要执行的操作码。 Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。 CRn:CP15 协处理器的目标寄存器。 CRm:协处理器中附加的目标寄存器或者源操作数寄存器 CRm 设置为 C0,否则结果不可预测。 opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。
CP15协处理器的操作要在 《 ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第B3.17章节里和《Cortex-A7 Technical ReferenceManua.pdf》第四章里讲的有。
CP15里的16个32位寄存器为c0~c15,中断要用到c0、c1、c12和c15这四个寄存器,其他的寄存器作用可以在上面两个文档中查询。并且这个CP15里的16个寄存器,在opc1和opc2不同的话代表操作的寄存器是不同的,这个比较有意思。
从上面的c0的图表可以看出来,即便是指令前面的参数都一样,在opc2不同的时候代表的参数是不一样的。当opc1=0,CRm=c0,opc2=0时c0就是主ID寄存器MIDR,这个就是c0的基本作用,而c1的基本作用是SCTLR寄存器,用来完成控制作用,比如使能MMU、I Cache、
D Cache等。所有的c0~15一共16个寄存器的操作全在《 ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第B3.17里给了说明(Page1481:Table3-42给了详细的说明),在《Cortex-A7 Technical ReferenceManua.pdf》第4.2节也对每个寄存器的不同bit的作用进行过介绍,并且每个操作寄存器用到的指令都有说明。
c12寄存器我们需要定义为CBAR寄存器
这个VBAR通用名字就可以看出来是GIC的基地址,通过定义这个寄存器的数据就可以设置中断向量偏移。
c15的说明在《Cortex-A7 Technical ReferenceManua.pdf》里,我们需要它定义为CBAR寄存器,这个寄存器里保存了GIC的基地址。
到这里,就接到前面讲到内容了,如果我们需要获取当前中断ID,当前的中断ID是保存在 GICC_IAR中,而GICC_IAR属于CPU端口寄存器,地址偏移量为0xC,那么我们就从c15按照CBAR模式读取到GIC底基地址加上偏移量就是对应的值,再获取bit[9:0]就是中断ID
MRC p15,4,r1,c15,c0,0 @获取GIC基地址,保存至r1 ADD r1,r1,#0x2000 @GIC基地址加上0x2000得到CPU接口寄存器起始地址 LDR r0,[r1,#0xC] @读取端口起始地址+0xC地址,也就是GICC_IAR的值
所以也就是说,我们要操作中断,就要需要修改c0,c1,c12和c15的参数。
中断使能
中断使能包括两部分:IRQ或者FIQ总使能,还有针对ID0~ID1019这1020个中断源的使能。
总中断使能
想要使用中断,必须打开IRQ或FIQ中断使能这个是短短是通过当前程序状态寄存器CPSR控制的bit[7](IRQ)和bit[6](FIQ)控制的(0时为使能),汇编提供了更简单的指令来对其进行操作
指令 | 功能 |
cpsid i | 禁止IRQ中断 |
cpsie i | 使能IRQ中断 |
cpsid f | 禁止FIQ中断 |
cpsid f | 使能FIQ中断 |
中断ID使能
GIC寄存器GICD_ISENABLERn和GCID_ICENABLERn来完成外部中断等使能和禁止。Cortex-A7内核中断ID只使用了512个,那么就需要512/32=16个寄存器来完成中断的使能,同样需要16个GCID_ICENABLERn来禁止中断。其中,R0的bit[15:0]对应ID15~0的SGI中断,剩下的R1~R15就控制了区域的SPI中断。
优先级设置
和STM32,Cortex-A7分可以配置的抢占优先级和子优先级,并且支持256个优先级,数字越小优先级越高。I.MX6U提供了32个优先级,在使用中断等时候需要对GICC_PMR寄存器进行初始化,来定义使用几级优先级。该寄存器只有低8位有效,最多可以设置256个优先级
GICC_PMR的地址也是基于CPU Interface的地址偏移里0x4的。
要注意的是,这256个优先级不是按照8位2进制的数据模式换算的。对不同bit置零,可以指定定使用了多少个优先级。针对I.MX6U来说,支持32个优先级,所以GICC_BPR设置为0b11111000。
抢占优先级和子优先级
抢占优先级比子优先级的优先权更高,这意味抢占优先级更高的中断会先执行,而不管子优先级的优先权,数值越低优先级越高。同理,如果抢占优先级相同,那么就会比较子优先级,子优先级更高的中断将会先被执行,数值越低优先级越高。当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个;如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断表中的排位顺序决定先处理哪一个。抢占优先级和子优先级各占多少为是由寄存器GICC_BPR决定的,GICC_BPR的低三位决定了抢占优先级和子优先级的位数。
为了简单化,我们一般把所有的中断优先级都配置为抢占优先级,比如I.MX6U的优先级位数为5位所以可以把Binary Point设置为2,表示5个优先级位全部为抢占优先级。(说实话这里暂时还不太清楚!)
优先级设置
前面已经定了I.MX6U有32个抢占优先级,数字越小优先级越高,具体使用某个中断就可以将其优先级设置为0~31,某个中断的优先级配有一个优先级寄存器DIPRIORITYR来完成,所以512个中断就对应有512个D_IPRIORITRY寄存器,如果优先级个数为32的话,使用D_IPRIORITRY的bit[7:4]来设置优先级,也就是说实际的优先级要左移3位,比如要设置ID40的优先级为5,代码要这么写
GICD_IPRIORITYR[40]=5<<3
中断优先级的设置还是有些不清楚,大概先讲这么多,后面结合代码来看应该能清除一些!