第十七章GPIO中断试验
中断系统是一个处理器重要的组成部分,中断系统极大的提高了CPU的执行效率,在学习STM32的时候就经常用到中断。本章就通过与STM32的对比来学习一下Cortex-A7(I.MX6U)中断系统和Cortex-M(STM32)中断系统的异同,同时,本章会将I.MX6U的一个IO作为输入中断,借此来讲解如何对I.MX6U的中断系统进行编程。
17.1 Cortex-A7中断系统详解
17.1.1 STM32中断系统回顾
STM32的中断系统主要有以下几个关键点:
①、中断向量表。
②、NVIC(内嵌向量中断控制器)。
③、中断使能。
④、中断服务函数。
1、中断向量表
中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程序的最前面,比如STM32F103的中断向量表如下所示:
- 示例代码17.1.1.1 STM32F103中断向量表
- 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
- 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
- 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
复制代码
“示例代码17.1.1.1”就是STM32F103的中断向量表,中断向量表都是链接到代码的最前面,比如一般ARM处理器都是从地址0X00000000开始执行指令的,那么中断向量表就是从0X00000000开始存放的。“示例代码17.1.1.1”中第1行的“__initial_sp”就是第一条中断向量,存放的是栈顶指针,接下来是第2行复位中断复位函数Reset_Handler的入口地址,依次类推,直到第27行的最后一个中断服务函数DMA2_Channel4_5_IRQHandler的入口地址,这样STM32F103的中断向量表就建好了。
我们说ARM处理器都是从地址0X00000000开始运行的,但是我们学习STM32的时候代码是下载到0X8000000开始的存储区域中。因此中断向量表是存放到0X8000000地址处的,而不是0X00000000,这样不是就出错了吗?为了解决这个问题,Cortex-M架构引入了一个新的概念——中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址处,中断向量表偏移配置在函数SystemInit中完成,通过向SCB_VTOR寄存器写入新的中断向量表首地址即可,代码如下所示:
示例代码17.1.1.2 STM32F103中断向量表偏移
- 1void 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中,也就是地址0X8000000处。第10行用到了FALSH_BASE和VECT_TAB_OFFSET,这两个都是宏,定义如下所示:
#define FLASH_BASE ((uint32_t)0x08000000)
#define VECT_TAB_OFFSET 0x0
因此第10行的代码就是:SCB->VTOR=0X080000000,中断向量表偏移设置完成。通过上面的讲解我们了解了两个跟STM32中断有关的概念:中断向量表和中断向量表偏移,那么这个跟I.MX6U有什么关系呢?因为I.MX6U所使用的Cortex-A7内核也有中断向量表和中断向量表偏移,而且其含义和STM32是一模一样的!只是用到的寄存器不通而已,概念完全相同!
2、NVIC(内嵌向量中断控制器)
中断系统得有个管理机构,对于STM32这种Cortex-M内核的单片机来说这个管理机构叫做NVIC,全称叫做Nested Vectored Interrupt Controller。关于NVIC本教程不作详细的讲解,既然Cortex-M内核有个中断系统的管理机构—NVIC,那么I.MX6U所使用的Cortex-A7内核是不是也有个中断系统管理机构?答案是肯定的,不过Cortex-A内核的中断管理机构不叫做NVIC,而是叫做GIC,全称是general interrupt controller,后面我们会详细的讲解Cortex-A内核的GIC。
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);
复制代码
上述代码就是使能PE2对饮过的EXTI2中断,同理,如果要使用I.MX6U的某个中断的话也需要使能其对应的中断。
4、中断服务函数
我们使用中断的目的就是为了使用中断服务函数,当中断发生以后中断服务函数就会被调用,我们要处理的工作就可以放到中断服务函数中去完成。同样以STM32F103的PE2为例,其中断服务函数如下所示:
/*外部中断2服务程序 */
void EXTI2_IRQHandler(void)
{
/*中断处理代码*/
}
当PE2引脚的中断触发以后就会调用其对应的中断处理函数EXTI2_IRQHandler,我们可以在函数EXTI2_IRQHandler中添加中断处理代码。同理,I.MX6U也有中断服务函数,当某个外设中断发生以后就会调用其对应的中断服务函数。
通过对STM32中断系统的回顾,我们知道了Cortex-M内核的中断处理过程,那么Cortex-A内核的中断处理过程是否是一样的,有什么异同呢?接下来我们带着这样的疑问来学习Cortex-A7内核的中断系统。
17.1.2 Cortex-A7中断系统简介
跟STM32一样,Cortex-A7也有中断向量表,中断向量表也是在代码的最前面。Cortex-A7内核有8个异常中断,这8个异常中断的中断向量表如表17.1.2.1所示:
表17.1.2.1 Cortex-A7中断向量表
中断向量表里面都是中断服务函数的入口地址,因此一款芯片有什么中断都是可以从中断向量表看出来的。从表17.1.2.1中可以看出,Cortex-A7一共有8个中断,而且还有一个中断向量未使用,实际只有7个中断。和“示例代码17.1.1.1”中的STM32F103中断向量表比起来少了很多!难道一个能跑Linux的芯片只有这7个中断?明显不可能的!那类似STM32中的EXTI9_5_IRQHandler、TIM2_IRQHandler这样的中断向量在哪里?I2C、SPI、定时器等等的中断怎么处理呢?这个就是Cortex-A和Cotex-M在中断向量表这一块的区别,对于Cortex-M内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于Cotex-A内核来说并没有这么做,在表17.1.2.1中有个IRQ中断,Cortex-A内核CPU的所有外部中断都属于这个IQR中断,当任意一个外部中断发生的时候都会触发IRQ中断。在IRQ中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理。这些外部中断和IQR中断的关系如图17.1.2.1所示:
图17.1.2.1外部中断和IRQ中断关系
在图17.1.2.1中,左侧的Software0_IRQn~PMU_IRQ2_IRQ这些都是I.MX6U的中断,他们都属于IRQ中断。当图17.1.2.1左侧这些中断中任意一个发生的时候IRQ中断都会被触发,所以我们需要在IRQ中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处理。
在表17.1.2.1中一共有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中断,所以我们需要编写这两个中断的中断服务函数,稍后我们会讲解如何编写对应的中断服务函数。首先我们要根据表17.1.2.1的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的start.S文件最前面,中断向量表如下:
- 示例代码17.1.1.1 Cortex-A向量表模板
- 1.global _start /* 全局标号 */
- 2
- 3 _start:
- 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
- 40 ldr r0,=NotUsed_Handler
- 41 bx r0
- 42
- 43/* IRQ中断!重点!!!!! */
- 44 IRQ_Handler:
- 45 /* 复位中断具体处理过程 */
- 46
- 47/* FIQ中断 */
- 48 FIQ_Handler:
- 49 ldr r0,=FIQ_Handler
- 50 bx r0
复制代码
第4到11行是中断向量表,当指定的中断发生以后就会调用对应的中断复位函数,比如复位中断发生以后就会执行第4行代码,也就是调用函数Reset_Handler,函数Reset_Handler就是复位中断的中断复位函数,其它的中断同理。
第14到50行就是对应的中断服务函数,中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数Reset_Handler和IRQ中断服务函数IRQ_Handler,其它的中断本教程没有用到,所以都是死循环。在编写复位中断复位函数和IRQ中断服务函数之前我们还需要了解一些其它的知识,否则的话就没法编写。
17.1.3 GIC控制器简介
1、GIC控制器总览
STM32(Cortex-M)的中断控制器叫做NVIC,I.MX6U(Cortex-A)的中断控制器叫做GIC,关于GIC的详细内容请参考开发板光盘中的文档《ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf》。
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,他们之间的关系如图17.1.3.1所示:
图17.1.3.1中断示意图
在图17.1.3.1中,GIC接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给ARM内核,这四个信号的含义如下:
VFIQ:虚拟快速FIQ。
VIRQ:虚拟快速IRQ。
FIQ:快速中断IRQ。
IRQ:外部中断IRQ。
VFIQ和VIRQ是针对虚拟化的,我们讨论虚拟化,剩下的就是FIQ和IRQ了,我们前面都讲了很多次了。本教程我们只使用IRQ,所以相当于GIC最终向ARM内核就上报一个IRQ信号。那么GIC是如何完成这个工作的呢?GICV2的逻辑图如图17.1.3.2所示:
图17.1.3.2 GICV2总体框图
图17.1.3.1中左侧部分就是中断源,中间部分就是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、中断ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一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对应哪个中断那就由半导体厂商根据实际情况去定义了。比如I.MX6U的总共使用了128个中断ID,加上前面属于PPI和SGI的32个ID,I.MX6U的中断源共有128+32=160个,这128个中断ID对应的中断在《I.MX6ULL参考手册》的“3.2 Cortex A7 interrupts”小节,中断源如表17.1.3.1所示:
表17.1.3.1 I.MX6U中断源
限于篇幅原因,表17.1.3.1中并没有给出I.MX6U完整的中断源,完整的中断源自行查阅《I.MX6ULL参考手册》的3.2小节。打开裸机例程“9_int”,我们前面移植了NXP官方SDK中的文件MCIMX6Y2C.h,在此文件中定义了一个枚举类型IRQn_Type,此枚举类型就枚举出了I.MX6U的所有中断,代码如下所示:
- 示例代码17.1.3.1 中断向量
- 1 #define NUMBER_OF_INT_VECTORS 160/* 中断源160个,SGI+PPI+SPI*/
- 2
- 3typedefenum IRQn {
- 4 /* Auxiliary constants */
- 5 NotAvail_IRQn =-128,
- 6
- 7 /* Core interrupts */
- 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 */
- 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;
复制代码
3、GIC逻辑分块
GIC架构分为了两个逻辑块:Distributor和CPU Interface,也就是分发器端和CPU接口端。这两个逻辑块的含义如下:
Distributor(分发器端):从图17.1.3.2可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个CPU Interface上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到CPU接口端。分发器端要做的主要工作如下:
①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组0还是组1。
CPU Interface(CPU接口端):CPU接口端听名字就知道是和CPU Core相连接的,因此在图17.1.3.2中每个CPU Core都可以在GIC中找到一个与之对应的CPU Interface。CPU接口端就是分发器和CPU Core之间的桥梁,CPU接口端主要工作如下:
①、使能或者关闭发送到CPU Core的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给CPU Core。
例程“9_int”中的文件core_ca7.h定义了GIC结构体,此结构体里面的寄存器分为了分发器端和CPU接口端,寄存器定义如下所示:
示例代码17.1.3.2 GIC控制器结构体
- /*
- * GIC寄存器描述结构体,
- * GIC分为分发器端和CPU接口端
- */
- 1typedefstruct
- 2{
- 3/* 分发器端寄存器 */
- 4uint32_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/ ) */
- 8uint32_t RESERVED1[29];
- 9 __IOM uint32_t D_IGROUPR[16];/* Offset: 0x1080 - 0x0BC (R/W) */
- 10uint32_t RESERVED2[16];
- 11 __IOM uint32_t D_ISENABLER[16];/* Offset: 0x1100 - 0x13C (R/W) */
- 12uint32_t RESERVED3[16];
- 13 __IOM uint32_t D_ICENABLER[16];/* Offset: 0x1180 - 0x1BC (R/W) */
- 14uint32_t RESERVED4[16];
- 15 __IOM uint32_t D_ISPENDR[16];/* Offset: 0x1200 - 0x23C (R/W) */
- 16uint32_t RESERVED5[16];
- 17 __IOM uint32_t D_ICPENDR[16]; /* Offset: 0x1280 - 0x2BC (R/W) */
- 18uint32_t RESERVED6[16];
- 19 __IOM uint32_t D_ISACTIVER[16];/* Offset: 0x1300 - 0x33C (R/W) */
- 20uint32_t RESERVED7[16];
- 21 __IOM uint32_t D_ICACTIVER[16];/* Offset: 0x1380 - 0x3BC (R/W) */
- 22uint32_t RESERVED8[16];
- 23 __IOM uint8_t D_IPRIORITYR[512];/* Offset: 0x1400 - 0x5FC (R/W) */
- 24uint32_t RESERVED9[128];
- 25 __IOM uint8_t D_ITARGETSR[512];/* Offset: 0x1800 - 0x9FC (R/W) */
- 26uint32_t RESERVED10[128];
- 27 __IOM uint32_t D_ICFGR[32];/* Offset: 0x1C00 - 0xC7C (R/W) */
- 28uint32_t RESERVED11[32];
- 29 __IM uint32_t D_PPISR; /* Offset: 0x1D00 (R/ ) */
- 30 __IM uint32_t D_SPISR[15]; /* Offset: 0x1D04 - 0xD3C (R/ ) */
- 31uint32_t RESERVED12[112];
- 32 __OM uint32_t D_SGIR; /* Offset: 0x1F00 ( /W) */
- 33uint32_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) */
- 36uint32_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/ ) */
- 62uint32_t RESERVED15[41];
- 63 __IOM uint32_t C_APR0; /* Offset: 0x20D0 (R/W) */
- 64uint32_t RESERVED16[3];
- 65 __IOM uint32_t C_NSAPR0; /* Offset: 0x20E0 (R/W) */
- 66uint32_t RESERVED17[6];
- 67 __IM uint32_t C_IIDR; /* Offset: 0x20FC (R/ ) */
- 68uint32_t RESERVED18[960];
- 69 __OM uint32_t C_DIR; /* Offset: 0x3000 ( /W) */
- 70} GIC_Type;
复制代码
“示例代码17.1.3.2”中的结构体GIC_Type就是GIC控制器,列举除了GIC控制器的所有寄存器,可以通过结构体GIC_Type来访问GIC的所有寄存器。
第5行是GIC的分发器端相关寄存器,其相对于GIC基地址偏移为0X1000,因此我们获取到GIC基地址以后只需要加上0X1000即可访问GIC分发器端寄存器。
第51行是GIC的CPU接口端相关寄存器,其相对于GIC基地址的偏移为0X2000,同样的,获取到GIC基地址以后只需要加上0X2000即可访问GIC的CPU接口段寄存器。
那么问题来了?GIC控制器的寄存器基地址在哪里呢?这个就需要用到Cortex-A的CP15协处理器了,下一小节就讲解CP15协处理器。
17.1.4 CP15协处理器
关于CP15协处理器和其相关寄存器的详细内容请参考下面两份文档:
《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第1469页“B3.17 Oranization ofthe CP15 registers in a VMSA implementation”。
《Cortex-A7 Technical ReferenceManua.pdf》第55页“Capter4 System Control”。
CP15协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15协处理器一共有16个32位寄存器。CP15协处理器的访问通过如下另个指令完成:
MRC: 将CP15协处理器中的寄存器数据读到ARM寄存器中。
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
CP15协处理器有16个32位寄存器,c0~c15,本章来看一下c0、c1、c12和c15这四个寄存器,因为我们本章实验要用到这四个寄存器,其他的寄存器大家参考上面的两个文档即可。
1、c0寄存器
CP15协处理器有16个32位寄存器,c0~c15,在使用MRC或者MCR指令访问这16个寄存器的时候,指令中的CRn、opc1、CRm和opc2通过不同的搭配,其得到的寄存器含义是不同的。比如c0在不同的搭配情况下含义如图17.1.4.1所示:
图17.1.4.1c0寄存器不同搭配含义
在图17.1.4.1中当MRC/MCR指令中的CRn=c0,opc1=0,CRm=c0,opc2=0的时候就表示此时的c0就是MIDR寄存器,也就是主ID寄存器,这个也是c0的基本作用。对于Cortex-A7内核来说,c0作为MDIR寄存器的时候其含义如图17.1.4.2所示:
图17.1.4.2c0作为MIDR寄存器结构图
在图17.1.4.2中各位所代表的含义如下:
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寄存器同样通过不同的配置,其代表的含义也不同,如图17.1.4.3所示:
图17.1.4.3c1寄存器不同搭配含义
在图17.1.4.3中当MRC/MCR指令中的CRn=c1,opc1=0,CRm=c0,opc2=0的时候就表示此时的c1就是SCTLR寄存器,也就是系统控制寄存器,这个是c1的基本作用。SCTLR寄存器主要是完成控制功能的,比如使能或者禁止MMU、I/D Cache等,c1作为SCTLR寄存器的时候其含义如图17.1.4.4所示:
图17.1.4.4c1作为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时使能。
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)寄存器中。
2、c12寄存器
c12寄存器通过不同的配置,其代表的含义也不同,如图17.1.4.4所示:
图17.1.4.4c12寄存器不同搭配含义
在图17.1.4.4中当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
3、c15寄存器
c15寄存器也可以通过不同的配置得到不同的含义,参考文档《Cortex-A7 Technical ReferenceManua.pdf》第68页“4.2.16c15 registers”,其配置如图17.1.4.5所示:
图17.1.4.5c15寄存器不同搭配含义
在图17.1.4.5中,我们需要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的值
关于CP15协处理器就讲解到这里,简单总结一下,通过c0寄存器可以获取到处理器内核信息;通过c1寄存器可以使能或禁止MMU、I/D Cache等;通过c12寄存器可以设置中断向量偏移;通过c15寄存器可以获取GIC基地址。关于CP15的其他寄存器,大家自行查阅本节前面列举的2份ARM官方资料。
17.1.5中断使能
中断使能包括两部分,一个是IRQ或者FIQ总中断使能,另一个就是ID0~ID1019这1020个中断源的使能。
1、IRQ和FIQ总中断使能
IRQ和FIQ分别是外部中断和快速中断的总开关,就类似家里买的进户总电闸,然后ID0~ID1019这1020个中断源就类似家里面的各个电器开关。要想开电视,那肯定要保证进户总电闸是打开的,因此要想使用I.MX6U上的外设中断就必须先打开IRQ中断(本教程不使用FIQ)。在“6.3.2 程序状态寄存器”小节已经讲过了,寄存器CPSR的 I=1禁止IRQ,当I=0使能IRQ;F=1禁止FIQ,F=0使能FIQ。我们还有更简单的指令来完成IRQ或者FIQ的使能和禁止,图表17.1.5.1所示:
表17.1.5.1开关中断指令
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中断,GICD_ISENABLER0的bit[31:16]对应ID31~16的PPI中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15就是控制SPI中断的。
17.1.6 中断优先级设置
1、优先级数配置
学过STM32都知道Cortex-M的中断优先级分为抢占优先级和子优先级,两者是可以配置的。同样的Cortex-A7的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置的。Cortex-A7最多可以支持256个优先级,数字越小,优先级越高!半导体厂商自行决定选择多少个优先级。I.MX6U选择了32个优先级。在使用中断的时候需要初始化GICC_PMR寄存器,此寄存器用来决定使用几级优先级,寄存器结构如图17.1.6.1所示:
图17.1.6.1 GICC_PMR寄存器
GICC_PMR寄存器只有低8位有效,这8位最多可以设置256个优先级,其他优先级数设置如表17.1.6.1所示:
表17.1.6.1优先级数设置
I.MX6U支持32个优先级,所以GICC_PMR要设置为0b11111000。
2、抢占优先级和子优先级位数设置
抢占优先级和子优先级各占多少位是由寄存器GICC_BPR来决定的,GICC_BPR寄存器结构如图17.1.6.2所示:
图17.1.6.2 GICC_BPR寄存器结构图
寄存器GICC_BPR只有低3位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如表17.1.6.2所示:
表17.1.6.2 GICC_BPR配置表
为了简单起见,一般将所有的中断优先级位都配置为抢占优先级,比如I.MX6U的优先级位数为5(32个优先级),所以可以设置Binarypoint为2,表示5个优先级位全部为抢占优先级。
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的优先级,也就是设置外设优先级。
17.2 硬件原理分析
本试验用到的硬件资源和第十五章的硬件资源一模一样。
17.3试验程序编写
本实验对应的例程路径为:开发板光盘-> 1、裸机例程->9_int。
本章试验的功能和第十五章一样,只是按键采用中断的方式处理。当按下按键KEY0以后就打开蜂鸣器,再次按下按键KEY0就关闭蜂鸣器。在第十六章的试验上完成本章试验。
17.3.1移植SDK包中断相关文件
将SDK包中的文件core_ca7.h拷贝到本章试验工程中的“imx6ul”文件夹中,参考试验“9_int”中core_ca7.h进行修改。主要留下和GIC相关的内容,我们重点是需要core_ca7.h中的10个API函数,这10个函数如表17.3.1.1所示:
表17.3.1.1 GIC相关API操作函数
移植好core_ca7.h以后,修改文件imx6ul.h,在里面加上如下一行代码:
#include "core_ca7.h"
17.3.2 重新编写start.S文件
重新在start.S中输入如下内容:
- 示例代码17.3.2.1 start.S文件代码
- /***************************************************************
- Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 文件名 : start.s
- 作者 : 左忠凯
- 版本 : V2.0
- 描述 : I.MX6U-ALPHA/I.MX6ULL开发板启动文件,完成C环境初始化,
- C环境初始化完成以后跳转到C代码。
- 其他 : 无
- 论坛 : www.openedv.com
- 日志 : 初版V1.0 2019/1/3 左忠凯修改
- V2.0 2019/1/4 左忠凯修改
- 添加中断相关定义
- **************************************************************/
- 1.global _start /* 全局标号 */
- 2
- 3/*
- 4 * 描述: _start函数,首先是中断向量表的创建
- 5 */
- 6 _start:
- 7 ldr pc,=Reset_Handler /* 复位中断 */
- 8 ldr pc,=Undefined_Handler /* 未定义指令中断 */
- 9 ldr pc,=SVC_Handler /* SVC(Supervisor)中断*/
- 10 ldr pc,=PrefAbort_Handler /* 预取终止中断 */
- 11 ldr pc,=DataAbort_Handler /* 数据终止中断 */
- 12 ldr pc,=NotUsed_Handler /* 未使用中断 */
- 13 ldr pc,=IRQ_Handler /* IRQ中断 */
- 14 ldr pc,=FIQ_Handler /* FIQ(快速中断) */
- 15
- 16/* 复位中断 */
- 17 Reset_Handler:
- 18
- 19 cpsid i /* 关闭全局中断 */
- 20
- 21/* 关闭I,DCache和MMU
- 22 * 采取读-改-写的方式。
- 23 */
- 24 mrc p15,0, r0, c1, c0,0 /* 读取CP15的C1寄存器到R0中 */
- 25 bic r0, r0, #(0x1<<12)/* 清除C1的I位,关闭I Cache */
- 26 bic r0, r0, #(0x1<<2) /* 清除C1的C位,关闭D Cache */
- 27 bic r0, r0, #0x2 /* 清除C1的A位,关闭对齐检查 */
- 28 bic r0, r0, #(0x1<<11) /* 清除C1的Z位,关闭分支预测 */
- 29 bic r0, r0, #0x1 /* 清除C1的M位,关闭MMU */
- 30 mcr p15,0, r0, c1, c0,0 /* 将r0的值写入到CP15的C1中 */
- 31
- 32
- 33 #if0
- 34 /* 汇编版本设置中断向量表偏移 */
- 35 ldr r0,=0X87800000
- 36
- 37 dsb
- 38 isb
- 39 mcr p15,0, r0, c12, c0,0
- 40 dsb
- 41 isb
- 42 #endif
- 43
- 44 /* 设置各个模式下的栈指针,
- 45 * 注意:IMX6UL的堆栈是向下增长的!
- 46 * 堆栈指针地址一定要是4字节地址对齐的!!!
- 47 * DDR范围:0X80000000~0X9FFFFFFF或者0X8FFFFFFF
- 48 */
- 49 /* 进入IRQ模式 */
- 50 mrs r0, cpsr
- 51 bic r0, r0, #0x1f /* 将r0的低5位清零,也就是cpsr的M0~M4 */
- 52 orr r0, r0, #0x12 /* r0或上0x12,表示使用IRQ模式 */
- 53 msr cpsr, r0 /* 将r0 的数据写入到cpsr中 */
- 54 ldr sp,=0x80600000 /* IRQ模式栈首地址为0X80600000,大小为2MB */
- 55
- 56 /* 进入SYS模式 */
- 57 mrs r0, cpsr
- 58 bic r0, r0, #0x1f /* 将r0的低5位清零,也就是cpsr的M0~M4 */
- 59 orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
- 60 msr cpsr, r0 /* 将r0 的数据写入到cpsr中 */
- 61 ldr sp,=0x80400000 /* SYS模式栈首地址为0X80400000,大小为2MB */
- 62
- 63 /* 进入SVC模式 */
- 64 mrs r0, cpsr
- 65 bic r0, r0, #0x1f /* 将r0的低5位清零,也就是cpsr的M0~M4 */
- 66 orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
- 67 msr cpsr, r0 /* 将r0 的数据写入到cpsr中 */
- 68 ldr sp,=0X80200000 /* SVC模式栈首地址为0X80200000,大小为2MB */
- 69
- 70 cpsie i /* 打开全局中断 */
- 71
- 72 #if0
- 73/* 使能IRQ中断 */
- 74 mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
- 75 bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中
- 76 * 的I位清零,表示允许IRQ中断
- 77 */
- 78 msr cpsr, r0 /* 将r0重新写入到cpsr中 */
- 79 #endif
- 80
- 81 b main /* 跳转到main函数 */
- 82
- 83/* 未定义中断 */
- 84 Undefined_Handler:
- 85 ldr r0,=Undefined_Handler
- 86 bx r0
- 87
- 88/* SVC中断 */
- 89 SVC_Handler:
- 90 ldr r0,=SVC_Handler
- 91 bx r0
- 92
- 93/* 预取终止中断 */
- 94 PrefAbort_Handler:
- 95 ldr r0,=PrefAbort_Handler
- 96 bx r0
- 97
- 98/* 数据终止中断 */
- 99 DataAbort_Handler:
- 100 ldr r0,=DataAbort_Handler
- 101 bx r0
- 102
- 103/* 未使用的中断 */
- 104 NotUsed_Handler:
- 105
- 106 ldr r0,=NotUsed_Handler
- 107 bx r0
- 108
- 109/* IRQ中断!重点!!!!! */
- 110 IRQ_Handler:
- 111 push {lr} /* 保存lr地址 */
- 112 push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
- 113
- 114 mrs r0, spsr /* 读取spsr寄存器 */
- 115 push {r0} /* 保存spsr寄存器 */
- 116
- 117 mrc p15,4, r1, c15, c0,0/* 将CP15的C0内的值到R1寄存器中
- 118 * 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
- 119 * Cortex-A7 Technical ReferenceManua.pdf P68 P138
- 120 */
- 121 add r1, r1, #0X2000 /* GIC基地址加0X2000,得到CPU接口端基地址 */
- 122 ldr r0,[r1, #0XC] /* CPU接口端基地址加0X0C就是GICC_IAR寄存器,
- 123 * GICC_IAR保存着当前发生中断的中断号,我们要根据
- 124 * 这个中断号来绝对调用哪个中断服务函数
- 125 */
- 126 push {r0, r1} /* 保存r0,r1 */
- 127
- 128 cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
- 129
- 130 push {lr} /* 保存SVC模式的lr寄存器 */
- 131 ldr r2,=system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
- 132 blx r2 /* 运行C语言中断处理函数,带有一个参数 */
- 133
- 134 pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
- 135 cps #0x12 /* 进入IRQ模式 */
- 136 pop {r0, r1}
- 137 str r0,[r1, #0X10] /* 中断执行完成,写EOIR */
- 138
- 139 pop {r0}
- 140 msr spsr_cxsf, r0 /* 恢复spsr */
- 141
- 142 pop {r0-r3, r12} /* r0-r3,r12出栈 */
- 143 pop {lr} /* lr出栈 */
- 144 subs pc, lr, #4 /* 将lr-4赋给pc */
- 145
- 146/* FIQ中断 */
- 147 FIQ_Handler:
- 148
- 149 ldr r0,=FIQ_Handler
- 150 bx r0
复制代码
第6到14行是中断向量表,17.1.2小节已经讲解过了。
第17到81行是复位中断服务函数Reset_Handler,第19行先调用指令“cpsid i”关闭IRQ,第24到30行是关闭I/D Cache、MMU、对齐检测和分支预测。第33行到42行是汇编版本的中断向量表重映射。第50到68行是设置不同模式下的sp指针,分别设置IRQ模式、SYS模式和SVC模式的栈指针,每种模式的栈大小都是2MB。第70行调用指令“cpsie i”重新打开IRQ中断,第72到79行是操作CPSR寄存器来打开IRQ中断。当初始化工作都完成以后就可以进入到main函数了,第81行就是跳转到main函数。
第110到144行是中断服务函数IRQ_Handler,这个是本章的重点,因为所有的外部中断最终都会触发IRQ中断,所以IRQ中断服务函数主要的工作就是区分去当前发生的什么中断(中断ID)?然后针对不同的外部中断做出不同的处理。第111到115行是保存现场,第117到122行是获取当前中断号,中断号被保存到了r0寄存器中。第131和132行才是中断处理的重点,这两行相当于调用了函数system_irqhandler,函数system_irqhandler是一个C语言函数,此函数有一个参数,这个参数中断号,所以我们需要传递一个参数。汇编中调用C函数如何实现参数传递呢?根据ATPCS(ARM-Thumb Procedure Call Standard)定义的函数参数传递规则,在汇编调用C函数的时候建议形参不要超过4个,形参可以由r0~r3这四个寄存器来传递,如果形参大于4个,那么大于4个的部分要使用堆栈进行传递。所以给r0寄存器写入中断号就可以了函数system_irqhandler的参数传递,在136行已经向r0寄存器写入了中断号了。中断的真正处理过程其实是在函数system_irqhandler中完成,稍后需要编写函数stem_irqhandler。
第151行向GICC_EOIR寄存器写入刚刚处理完成的中断号,当一个中断处理完成以后必须向GICC_EOIR寄存器写入其中断号表示中断处理完成。
第153到157行就是恢复现场。
第158行中断处理完成以后就要重新返回到曾经被中断打断的地方运行,这里为什么要将lr-4然后赋给pc呢?而不是直接将lr赋值给pc?ARM的指令是三级流水线:取指、译指、执行,pc指向的是正在取值的地址,这就是很多书上说的pc=当前执行指令地址+8。比如下面代码示例:
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”开始执行。
17.3.3通用中断驱动文件编写
在start.S文件中我们在中断服务函数IRQ_Handler中调用了C函数system_irqhandler来处理具体的中断。此函数有一个参数,参数是中断号,但是函数system_irqhandler的具体内容还没有实现,所以需要实现函数system_irqhandler的具体内容。不同的中断源对应不同的中断处理函数,I.MX6U有160个中断源,所以需要160个中断处理函数,我们可以将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。当中断发生以后函数system_irqhandler根据中断号从中断处理函数数组中找到对应的中断处理函数并执行即可。
在bsp目录下新建名为“int”的文件夹,在bsp/int文件夹里面创建bsp_int.c和bsp_int.h这两个文件。在bsp_int.h文件里面输入如下内容:
- 示例代码17.3.3.1 bsp_int.h文件代码
- 1 #ifndef _BSP_INT_H
- 2 #define _BSP_INT_H
- 3 #include "imx6ul.h"
- 4/***************************************************************
- 5 Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 6文件名 : bsp_int.h
- 7作者 : 左忠凯
- 8版本 : V1.0
- 9描述 : 中断驱动头文件。
- 10其他 : 无
- 11论坛 : www.openedv.com
- 12日志 : 初版V1.0 2019/1/4 左忠凯创建
- 13 ***************************************************************/
- 14
- 15/* 中断处理函数形式 */
- 16typedefvoid(*system_irq_handler_t)(unsignedint giccIar,
- void*param);
- 17
- 18/* 中断处理函数结构体*/
- 19typedefstruct _sys_irq_handle
- 20{
- 21 system_irq_handler_t irqHandler; /* 中断处理函数 */
- 22void*userParam; /* 中断处理函数参数 */
- 23} sys_irq_handle_t;
- 24
- 25/* 函数声明 */
- 26void int_init(void);
- 27void system_irqtable_init(void);
- 28void system_register_irqhandler(IRQn_Type irq,
- system_irq_handler_t handler,
- void*userParam);
- 29void system_irqhandler(unsignedint giccIar);
- 30void default_irqhandler(unsignedint giccIar,void*userParam);
- 31
- 32 #endif
复制代码
第16~23行是中断处理结构体,结构体sys_irq_handle_t包含一个中断处理函数和中断处理函数的用户参数。一个中断源就需要一个sys_irq_handle_t变量,I.MX6U有160个中断源,因此需要160个sys_irq_handle_t组成中断处理数组。
在bsp_int.c中输入如下所示代码:
第14行定义了一个变量irqNesting,此变量作为中断嵌套计数器。
第17行定了中断服务函数数组irqTable,这是一个sys_irq_handle_t类型的结构体数组,数组大小为I.MX6U的中断源个数,即160个。
第24~28行是中断初始化函数int_init,在此函数中首先初始化了GIC,然后初始化了中断服务函数表,最终设置了中断向量表偏移。
第36~46行是中断服务函数表初始化函数system_irqtable_init,初始化irqTable,给其赋初值。
第55~59行是注册中断处理函数system_register_irqhandler,此函数用来给指定的中断号注册中断处理函数。如果要使用某个外设中断,那就必须调用此函数来给这个中断注册一个中断处理函数。
第68~86行就是前面在start.S中调用的system_irqhandler函数,此函数根据中断号在中断处理函数表irqTable中取出对应的中断处理函数并执行。
第94~99行是默认中断处理函数default_irqhandler,这是一个空函数,主要用来给初始化中断函数处理表。
17.3.4修改GPIO驱动文件
在前几章节试验中我们只是使用到了GPIO最基本的输入输出功能,本章我们需要使用GPIO的中断功能。所以需要修改文件GPIO的驱动文件bsp_gpio.c和bsp_gpio.h,加上中断相关函数。关于GPIO中断内容已经在8.1.5小节进行了详细的讲解,这里就不赘述了。打开bsp_gpio.h文件,重新输入如下内容:
- 示例代码17.3.4.1 bsp_gpio.h文件代码
- 1 #ifndef _BSP_GPIO_H
- 2 #define _BSP_GPIO_H
- 3 #include "imx6ul.h"
- 4/***************************************************************
- 5 Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 6文件名 : bsp_gpio.h
- 7作者 : 左忠凯
- 8版本 : V1.0
- 9描述 : GPIO操作文件头文件。
- 10其他 : 无
- 11论坛 : www.openedv.com
- 12日志 : 初版V1.0 2019/1/4 左忠凯创建
- 13 V2.0 2019/1/4 左忠凯修改
- 14 添加GPIO中断相关定义
- 15
- 16 ***************************************************************/
- 17
- 18/*
- 19 * 枚举类型和结构体定义
- 20 */
- 21typedefenum _gpio_pin_direction
- 22{
- 23 kGPIO_DigitalInput =0U,/* 输入 */
- 24 kGPIO_DigitalOutput =1U,/* 输出 */
- 25} gpio_pin_direction_t;
- 26
- 27/*
- 28 * GPIO中断触发类型枚举
- 29 */
- 30typedefenum _gpio_interrupt_mode
- 31{
- 32 kGPIO_NoIntmode =0U, /* 无中断功能 */
- 33 kGPIO_IntLowLevel =1U, /* 低电平触发 */
- 34 kGPIO_IntHighLevel =2U, /* 高电平触发 */
- 35 kGPIO_IntRisingEdge =3U, /* 上升沿触发 */
- 36 kGPIO_IntFallingEdge =4U, /* 下降沿触发 */
- 37 kGPIO_IntRisingOrFallingEdge =5U, /* 上升沿和下降沿都触发 */
- 38} gpio_interrupt_mode_t;
- 39
- 40/*
- 41 * GPIO配置结构体
- 42 */
- 43typedefstruct _gpio_pin_config
- 44{
- 45 gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
- 46uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
- 47 gpio_interrupt_mode_t interruptMode; /* 中断方式 */
- 48} gpio_pin_config_t;
- 49
- 50
- 51/* 函数声明 */
- 52void gpio_init(GPIO_Type *base,int pin, gpio_pin_config_t *config);
- 53int gpio_pinread(GPIO_Type *base,int pin);
- 54void gpio_pinwrite(GPIO_Type *base,int pin,int value);
- 55void gpio_intconfig(GPIO_Type* base,unsignedint pin,
- gpio_interrupt_mode_t pinInterruptMode);
- 56void gpio_enableint(GPIO_Type* base,unsignedint pin);
- 57void gpio_disableint(GPIO_Type* base,unsignedint pin);
- 58void gpio_clearintflags(GPIO_Type* base,unsignedint pin);
- 59
- 60 #endif
复制代码
相比前面试验的bsp_gpio.h文件,“示例代码17.3.3.2”中添加了一个新枚举类型:gpio_interrupt_mode_t,枚举出了GPIO所有的中断触发类型。还修改了结构体gpio_pin_config_t,在,在里面加入了interruptMode成员变量。最后就是添加了一些跟中断有关的函数声明,bsp_gpio.h文件的内容总体还是比较简单的。
打开bsp_gpio.c文件,重新输入如下代码:
- 示例代码17.3.4.2 bsp_gpio.c文件代码
- 1 #include "bsp_gpio.h"
- 2/***************************************************************
- 3 Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 4文件名 : bsp_gpio.c
- 5作者 : 左忠凯
- 6版本 : V1.0
- 7描述 : GPIO操作文件。
- 8其他 : 无
- 9论坛 : www.openedv.com
- 10日志 : 初版V1.0 2019/1/4 左忠凯创建
- 11 V2.0 2019/1/4 左忠凯修改:
- 12修改gpio_init()函数,支持中断配置.
- 13添加gpio_intconfig()函数,初始化中断
- 14添加gpio_enableint()函数,使能中断
- 15 添加gpio_clearintflags()函数,清除中断标志位
- 16
- 17 ***************************************************************/
- 18
- 19/*
- 20 * @description : GPIO初始化。
- 21 * @param - base : 要初始化的GPIO组。
- 22 * @param - pin : 要初始化GPIO在组内的编号。
- 23 * @param - config : GPIO配置结构体。
- 24 * @return : 无
- 25 */
- 26void gpio_init(GPIO_Type *base,int pin, gpio_pin_config_t *config)
- 27{
- 28 base->IMR &=~(1U<< pin);
- 29
- 30if(config->direction == kGPIO_DigitalInput) /* GPIO作为输入 */
- 31{
- 32 base->GDIR &=~(1<< pin);
- 33}
- 34else /* 输出 */
- 35{
- 36 base->GDIR |=1<< pin;
- 37 gpio_pinwrite(base,pin, config->outputLogic);/* 设置默认电平 */
- 38}
- 39 gpio_intconfig(base, pin, config->interruptMode);/* 中断功能配置 */
- 40}
- 41
- 42/*
- 43 * @description : 读取指定GPIO的电平值。
- 44 * @param – base : 要读取的GPIO组。
- 45 * @param - pin : 要读取的GPIO脚号。
- 46 * @return : 无
- 47 */
- 48int gpio_pinread(GPIO_Type *base,int pin)
- 49{
- 50return(((base->DR)>> pin)&0x1);
- 51}
- 52
- 53/*
- 54 * @description : 指定GPIO输出高或者低电平。
- 55 * @param - base : 要输出的的GPIO组。
- 56 * @param - pin : 要输出的GPIO脚号。
- 57 * @param – value : 要输出的电平,1 输出高电平, 0 输出低低电平
- 58 * @return : 无
- 59 */
- 60void gpio_pinwrite(GPIO_Type *base,int pin,int value)
- 61{
- 62if(value ==0U)
- 63{
- 64 base->DR &=~(1U<< pin);/* 输出低电平 */
- 65}
- 66else
- 67{
- 68 base->DR |=(1U<< pin);/* 输出高电平 */
- 69}
- 70}
- 71
- 72/*
- 73 * @description : 设置GPIO的中断配置功能
- 74 * @param - base : 要配置的IO所在的GPIO组。
- 75 * @param - pin : 要配置的GPIO脚号。
- 76 * @param – pinInterruptMode: 中断模式,参考gpio_interrupt_mode_t
- 77 * @return : 无
- 78 */
- 79void gpio_intconfig(GPIO_Type* base,unsignedint pin,
- gpio_interrupt_mode_t pin_int_mode)
- 80{
- 81volatileuint32_t*icr;
- 82uint32_t icrShift;
- 83
- 84 icrShift = pin;
- 85
- 86 base->EDGE_SEL &=~(1U<< pin);
- 87
- 88if(pin <16)/* 低16位 */
- 89{
- 90 icr =&(base->ICR1);
- 91}
- 92else/* 高16位 */
- 93{
- 94 icr =&(base->ICR2);
- 95 icrShift -=16;
- 96}
- 97switch(pin_int_mode)
- 98{
- 99case(kGPIO_IntLowLevel):
- 100*icr &=~(3U<<(2* icrShift));
- 101break;
- 102case(kGPIO_IntHighLevel):
- 103*icr =(*icr &(~(3U<<(2* icrShift))))|
- (1U<<(2* icrShift));
- 104break;
- 105case(kGPIO_IntRisingEdge):
- 106*icr =(*icr &(~(3U<<(2* icrShift))))|
- (2U<<(2* icrShift));
- 107break;
- 108case(kGPIO_IntFallingEdge):
- 109*icr |=(3U<<(2* icrShift));
- 110break;
- 111case(kGPIO_IntRisingOrFallingEdge):
- 112 base->EDGE_SEL |=(1U<< pin);
- 113break;
- 114default:
- 115break;
- 116}
- 117}
- 118
- 119/*
- 120 * @description : 使能GPIO的中断功能
- 121 * @param - base : 要使能的IO所在的GPIO组。
- 122 * @param - pin : 要使能的GPIO在组内的编号。
- 123 * @return : 无
- 124 */
- 125void gpio_enableint(GPIO_Type* base,unsignedint pin)
- 126{
- 127 base->IMR |=(1<< pin);
- 128}
- 129
- 130/*
- 131 * @description : 禁止GPIO的中断功能
- 132 * @param - base : 要禁止的IO所在的GPIO组。
- 133 * @param - pin : 要禁止的GPIO在组内的编号。
- 134 * @return : 无
- 135 */
- 136void gpio_disableint(GPIO_Type* base,unsignedint pin)
- 137{
- 138 base->IMR &=~(1<< pin);
- 139}
- 140
- 141/*
- 142 * @description : 清除中断标志位(写1清除)
- 143 * @param - base : 要清除的IO所在的GPIO组。
- 144 * @param - pin : 要清除的GPIO掩码。
- 145 * @return : 无
- 146 */
- 147void gpio_clearintflags(GPIO_Type* base,unsignedint pin)
- 148{
- 149 base->ISR |=(1<< pin);
- 150}
复制代码
在bsp_gpio.c文件中首先修改了gpio_init函数,在此函数里面添加了中断配置代码。另外也新增加了4个函数,如下:
gpio_intconfig:配置GPIO的中断功能。
gpio_enableint:GPIO中断使能函数。
gpio_disableint:GPIO中断禁止函数。
gpio_clearintflags:GPIO中断标志位清除函数。
bsp_gpio.c文件重点就是增加了一些跟GPIO中断有关的函数,都比较简单。
17.3.5按键中断驱动文件编写
本例程的目的是以中断的方式编写KEY按键驱动,当按下KEY以后触发GPIO中断,然后在中断服务函数里面控制蜂鸣器的开关。所以接下来就是要编写按键KEY对应的UART1_CTS这个IO的中断驱动,在bsp文件夹里面新建名为“exit”的文件夹,然后在bsp/exit里面新建bsp_exit.c和bsp_exit.h两个文件。在bsp_exit.h文件中输入如下代码:
- 示例代码17.3.5.1 bsp_exit.h文件代码
- 1 #ifndef _BSP_EXIT_H
- 2 #define _BSP_EXIT_H
- 3/***************************************************************
- 4 Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 5文件名 : bsp_exit.h
- 6作者 : 左忠凯
- 7版本 : V1.0
- 8描述 : 外部中断驱动头文件。
- 9其他 : 配置按键对应的GPIP为中断模式
- 10论坛 : www.openedv.com
- 11日志 : 初版V1.0 2019/1/4 左忠凯创建
- 12 ***************************************************************/
- 13 #include "imx6ul.h"
- 14
- 15/* 函数声明 */
- 16void exit_init(void); /* 中断初始化 */
- 17void gpio1_io18_irqhandler(void);/* 中断处理函数 */
- 18
- 19 #endif
- bsp_exit.h就是函数声明,很简单。接下来在bsp_exit.c里面输入如下内容:
- 示例代码17.3.5.2 bsp_exit.c文件代码
- /***************************************************************
- Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 文件名 : bsp_exit.c
- 作者 : 左忠凯
- 版本 : V1.0
- 描述 : 外部中断驱动。
- 其他 : 配置按键对应的GPIP为中断模式
- 论坛 : www.openedv.com
- 日志 : 初版V1.0 2019/1/4 左忠凯创建
- ***************************************************************/
- 1 #include "bsp_exit.h"
- 2 #include "bsp_gpio.h"
- 3 #include "bsp_int.h"
- 4 #include "bsp_delay.h"
- 5 #include "bsp_beep.h"
- 6
- 7/*
- 8 * @description : 初始化外部中断
- 9 * @param : 无
- 10 * @return : 无
- 11 */
- 12void exit_init(void)
- 13{
- 14 gpio_pin_config_t key_config;
- 15
- 16 /* 1、设置IO复用 */
- 17 IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);
- 18 IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
- 19
- 20 /* 2、初始化GPIO为中断模式 */
- 21 key_config.direction = kGPIO_DigitalInput;
- 22 key_config.interruptMode = kGPIO_IntFallingEdge;
- 23 key_config.outputLogic =1;
- 24 gpio_init(GPIO1,18,&key_config);
- 25 /* 3、使能GIC中断、注册中断服务函数、使能GPIO中断*/
- 26 GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
- 27 system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
- (system_irq_handler_t)gpio1_io18_irqhandler,
- NULL);
- 28 gpio_enableint(GPIO1,18);
- 29}
- 30
- 31/*
- 32 * @description : GPIO1_IO18最终的中断处理函数
- 33 * @param : 无
- 34 * @return : 无
- 35 */
- 36void gpio1_io18_irqhandler(void)
- 37{
- 38 staticunsignedchar state =0;
- 39
- 40 /*
- 41 *采用延时消抖,中断服务函数中禁止使用延时函数!因为中断服务需要
- 42 *快进快出!!这里为了演示所以采用了延时函数进行消抖,后面我们会讲解
- 43 *定时器中断消抖法!!!
- 44 */
- 45
- 46 delay(10);
- 47 if(gpio_pinread(GPIO1,18)==0)/* 按键按下了 */
- 48 {
- 49 state =!state;
- 50 beep_switch(state);
- 51 }
- 52
- 53 gpio_clearintflags(GPIO1,18); /* 清除中断标志位 */
- 54}
复制代码
bsp_exit.c文件只有两个函数exit_init和gpio1_io18_irqhandler,exit_init是中断初始化函数。第14~24行都是初始化KEY所使用的UART1_CTS这个IO,设置其复用为GPIO1_IO18,然后配置GPIO1_IO18为下降沿触发中断。重点是第26~28行,在26行调用函数GIC_EnableIRQ来使能GPIO_IO18所对应的中断总开关,I.MX6U中GPIO1_IO16~IO31这16个IO共用ID99。27行调用函数system_register_irqhandler注册ID99所对应的中断处理函数,GPIO1_IO16~IO31这16个IO共用一个中断处理函数,至于具体是哪个IO引起的中断,那就需要在中断处理函数中判断了。28行通过函数gpio_enableint使能GPIO1_IO18这个IO对应的中断。
函数gpio1_io18_irqhandler就是27行注册的中断处理函数,也就是我们学习STM32的时候某个GPIO对应的中断服务函数。在此函数里面编写中断处理代码,第50行就是蜂鸣器开关控制代码,也就是我们本试验的目的。当中断处理完成以后肯定要清除中断标志位,第53行调用函数gpio_clearintflags来清除GPIO1_IO18的中断标志位。
17.3.6编写main.c文件
在main.c中输入如下代码:
- 示例代码17.3.6.1 main.c文件代码
- /**************************************************************
- Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
- 文件名 : mian.c
- 作者 : 左忠凯
- 版本 : V1.0
- 描述 : I.MX6U开发板裸机实验9 系统中断实验
- 其他 : 五
- 论坛 : www.openedv.com
- 日志 : 初版V1.0 2019/1/4 左忠凯创建
- **************************************************************/
- 1 #include "bsp_clk.h"
- 2 #include "bsp_delay.h"
- 3 #include "bsp_led.h"
- 4 #include "bsp_beep.h"
- 5 #include "bsp_key.h"
- 6 #include "bsp_int.h"
- 7 #include "bsp_exit.h"
- 8
- 9/*
- 10 * @description : main函数
- 11 * @param : 无
- 12 * @return : 无
- 13 */
- 14int main(void)
- 15{
- 16 unsignedchar state = OFF;
- 17
- 18 int_init(); /* 初始化中断(一定要最先调用!) */
- 19 imx6u_clkinit(); /* 初始化系统时钟 */
- 20 clk_enable(); /* 使能所有的时钟 */
- 21 led_init(); /* 初始化led */
- 22 beep_init(); /* 初始化beep */
- 23 key_init(); /* 初始化key */
- 24 exit_init(); /* 初始化按键中断 */
- 25
- 26 while(1)
- 27 {
- 28 state =!state;
- 29 led_switch(LED0, state);
- 30 delay(500);
- 31 }
- 32
- 33 return0;
- 34}
复制代码
main.c很简单,重点是第18行调用函数int_init来初始化中断系统,第24行调用函数exit_init来初始化按键KEY对应的GPIO中断。
17.4编译下载验证
17.4.1编写Makefile和链接脚本
在第十六章实验的Makefile基础上修改变量TARGET为int,在变量INCDIRS和SRCDIRS中追加“bsp/exit”和bsp/int,修改完成以后如下所示:
- 示例代码17.4.1.1 Makefile文件代码
- 1 CROSS_COMPILE ?= arm-linux-gnueabihf-
- 2 TARGET ?= int
- 3
- 4/* 省略掉其它代码...... */
- 5
- 6 INCDIRS:= imx6ul \
- 7 bsp/clk \
- 8 bsp/led \
- 9 bsp/delay \
- 10 bsp/beep \
- 11 bsp/gpio \
- 12 bsp/key \
- 13 bsp/exit \
- 14 bsp/int
- 15
- 16 SRCDIRS := project \
- 17 bsp/clk \
- 18 bsp/led \
- 19 bsp/delay \
- 20 bsp/beep \
- 21 bsp/gpio \
- 22 bsp/key \
- 23 bsp/exit \
- 24 bsp/int
- 25
- 26/* 省略掉其它代码...... */
- 27
- 28 clean:
- 29 rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS)$(SOBJS)
复制代码
第2行修改变量TARGET为“int”,也就是目标名称为“int”。
第13、14行在变量INCDIRS中添加GPIO中断和通用中断驱动头文件(.h)路径。
第23、24行在变量SRCDIRS中添加GPIO中断和通用中断驱动文件(.c)路径。
链接脚本保持不变。
17.4.2编译下载
使用Make命令编译代码,编译成功以后使用软件imxdownload将编译完成的int.bin文件下载到SD卡中,命令如下:
- chmod 777 imxdownload //给予imxdownload可执行权限,一次即可
- ./imxdownload int.bin /dev/sdd //烧写到SD卡中
复制代码
烧写成功以后将SD卡插到开发板的SD卡槽中,然后复位开发板。本试验效果其实和试验“8_key”一样,按下KEY就会打开蜂鸣器,再次按下就会关闭蜂鸣器。LED0会不断闪烁,周期大约500ms。