17 IMX6ULL裸机开发:GPIO中断编程

创建时间:2022/2/6 19:49
更新时间:2022/2/12 14:46
作者:gi51wa2j
标签:100ask_IMX6ULL_v11, bingo, 操作, 正文


在学习本章节之前,有必须先去了解学习下列知识,并结合韦东山老师的IMX6ULL裸机开发手册学习
14 IMX6ULL裸机开发:异常处理(基础概念)
16 IMX6ULL裸机开发:中断处理(属于一种异常)          (重点了解GIC)
此外:值得注意的是需要了解向量表,以及中断程序的处理流程

一、GPIO中断介绍(通用概念)

中断也属于一种异常
CPU在运行过程中,也会被各种异常打断。这些异常

① 指令未定义

② 指令、数据访问有问题

③SWI(软中断)

④ 快中断

⑤ 中断

① 按键

② 定时器

③ ADC转换完成

④ UART发生完数据、接收数据等等


     这些众多的中断源,汇集于中断管理器,由中断管理器选择优先级最高的中断并通知CPU。CPU会根据中断的类型到跳转到不同的地址处理中断。发生中断后,CPU并不是随便跳到一个地址处理中断,而是根据异常向量表,跳转到对应的地址处理中断。

1.1 GPIO 中断

     GPIO中断,指由GPIO模块产生的中断,有边沿触发中断或者电平翻转中断。GPIO模块能检测到引脚上电平的变化,并向中断控制器(GIC)发出中断信号,GIC再向CPU发出中断信号。框架如下图:
    CPU在每执行完了条指令时会检查是否发生了中断,若是则会跳转到中断处理地址进行中断处理。为了避免破坏主任务数据,CPU会处理保存当前相关寄存器(保存现场)并进入中断服务函数,执行完中断服务函数后,CPU会恢复相关寄存器(恢复现场),回到主任务继续执行程序。

     程序发生GPIO中断后会根据异常向量表强制跳转到0x18(IRQ中断地址)。如下图:
     常向量表并不总是从0地址开始,IMX6ULL可以设置vector base寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

1.2 GIC中断控制器功能概述(IMX6ULL)

     IMX6ULL是Cortex-A7内核,采用GIC V2(Generic Interrupt Controller)中断控制器。在这里只简单的介绍一下GIC,具体可以参考16节中GIC的阐述

     如上图所示,硬件中断信号发送到GIC(Generic Interrupt Controller),GIC产生一个FIQ或IRQ信号给CPU。
     在初始化中断时,要初始化这4部分:产生中断的源头(GPIO模块或UART模块等)、GIC(内部有Distributor或CPU interface)、CPU本身(设置CPSR寄存器),如下图:
     GIC的主要作用可以归结为接受硬件中断信号,并进行简单的处理,按照一定的设置策略,分给对应的CPU处理。如下图:
       GIC可以通过四个信号向CPU核汇报中断情况:VIRQ(虚拟快速IRQ)、VFIQ(虚拟快速FIQ)、IRQ、FIQ。VIRQ、VFIQ是针对虚拟化,剩下就是IRQ和FIQ。GPIO中断属于IRQ中断,所以在本次实验中GIC上报IRQ信号给ARM内核。

1.2.1 GIC的内部过程

    中断源分为SPI(Shared Peripheral Interrupt)、PPI(Private Peripheral Interrupt)、SGI request(Software-generated Interrupt)外部中断都属于SPI中断源。

    GIC控制器包括分发器(Distributor)和CPU接口端(CPU interface)。

     中断信号先到达分发器,分发器根据该中断所设定的CPU,把中断发送到CPU对应的CPUinterface上;在CPUinterface里判断该中断的优先级是否足够高,能否抢断或打断当前的中端处理,如果可以,CPU interface就会发送一个物理的signa到CPU的IRQ线上;CPU接收到中断信号,转到中断处理模式进行处理。

1.3 GIC中断寄存器(IMX6ULL)

     GIC寄存器分为Distributor registerCPU interface register。寄存器数目较多,这里介绍本次实验中需要我们设置的寄存器。

     Distributor的寄存器名字中有“GICD_”前缀,CPU interface的寄存器名字中有“GICC_”前缀。

1.3.1 GICC_IAR寄存器

     GICC_IAR寄存器属于CPU interface register,作用是:保存中断ID,读取GICC_IAR寄存器可以获得中断ID,这个过程可以当作对中断的确认。

位域

读写

描述

[31:13]

-

 

保留

[12:10]

CPUID

R

对于SGI类中断,它表示谁发出了中断。例如,值为3表示该请求是通过对CPU interface 3上的GICD_SGIR的写操作生成的。

[9:0]

Interrupt ID

R

中断ID

1.3.2 GICC_EOIR寄存器

     写此寄存器,表示某中断已经处理完毕。GICC_IAR的值表示当前在处理的中断,把GICC_IAR的值写入GICC_EOIR就表示中断处理完了。

位域

读写

描述

[31:13]

-

 

保留

[12:10]

CPUID

W

对于SGI类中断,它的值跟GICD_IAR. CPUID的相同。

[9:0]

EOIINTID

W

中断ID,它的值跟GICD_IAR里的中断ID相同

1.4 CP15协处理器及相关寄存器

这部分的指令,我们一般用现成的即可。此处了解该类知识
    CP15协处理器介绍

     在基于ARM的嵌入式系统中,存储系统通常是协处理器CP15完成的。ARM处理器使用协处理器指令MCR和MRC来读写寄存器,控制cache、MMU、配置时钟(在bootloader时钟初始化时会用到)等。CP15包含16个32位寄存器,编号为0~15。

     在本次实验中,需要设置的寄存器有:SCTLR(System Control Register)寄存器,VBAR(Vector Base Address)寄存器

1.4.1 SCTLR(System Control Register)寄存器

设置SCTLR寄存器可以控制cache、MMU等。
把上面的英文注释翻译成中文,如下表:

位域

读写

描述

[13]

V

R/W

向量位,用来设置向量表的基地址。

0:Low exception vectors,基地址为0;如果有VBAR寄存器,则使用它来指定向量表基地址;

1:High exception vectors,向量表基地址为0xFFFF0000

[12]

I

R/W

指令cache使能位

0:指令cache禁止,这是默认值

1:指令cache使能

[11]

Z

RAO/WI

分支预测使能位,当MMU使能时该位自动使能

[2]

C

R/W

数据cache使能位

0:数据cache禁止,这是默认值

1:数据cache使能

[1]

A

R/W

字节对齐设置位,

0:地址对齐检查禁止,这是默认值

1:地址对齐检查使能

[0]

M

R/W

MMU使能位,

0:MMU禁止,这是默认值;

1:MMU使能

可以使用以下指令读写SCTLR寄存器:
MRC p15, 0, <Rt>, c1, c0, 0 ;把SCTLR寄存器的值读到ARM寄存器Rt中。 MRC p15, 0, <Rt>, c1, c0, 0 ;把ARM寄存器Rt的值写入SCTLR寄存器。

1.4.2 VBAR(Vector Base Address)寄存器

     通过VBAR寄存器,可以设置异常向量表的映射地址。如果不把异常向量表的映射地址告诉CPU,在发生异常时,CPU就找不到异常向量表,就无法处理异常。

位域

读写

描述

[31:5]

Vector_Base_Address

R/W

向量表基地址的b[31:5],这也意味着这个地址的低5位全为0

二、GPIO中断寄存器介绍(针对IMX6ULL)

2.1 GPIOx_ICR1(GPIO interrupt configuration register1)

GPIO中断配置寄存器1,用来配置GPIO中断1~15的触发类型。

位域

读写

描述

[2n+1:2n]

ICRn

R/W

用来设置GPIO中断的触发类型,

00:低电平触发;

01:高电平触发;

10:上升沿触发;

11:下降沿触发

ICR0~ICR15对应GPIO interrupt 0-15。

2.2 GPIOx_ICR2(GPIO interrupt configuration register2)

GPIO中断配置寄存器2,用来配置GPIO中断16~31的触发类型。
与GPIOx_ICR1类似,ICR15~ICR31对应GPIO interrupt 16-31。

2.3 GPIOx_IMR( GPIO interrupt mask register)

GPIO中断屏蔽寄存器,用来屏蔽或使能某个GPIO中断。

位域

读写

描述

[n]

IMR

R/W

每一位对应一个GPIO中断,

0:中断被屏蔽

1:中断使能,未被屏蔽

2.4 GPIOx_ISR( GPIO interrupt status register)

GPIO中断状态寄存器,表示某个GPIO中断是否发生了。

位域

读写

描述

[n]

ISR

R/W

每一位对应一个GPIO中断,跟GPIO_IMR无关,就是说即使屏蔽了某个中断,还是可以在本寄存器中观察它的状态。

读:

0:中断未发生;

1:中断已发生。

写:某位写入1时,清零该位。

2.5 GPIOx_EDGE_SEL(GPIO edge select register)

GPIO中断边沿选择寄存器,它可以用来覆盖GPIOx_ICR1/2中的配置值。
每一位对应一个GPIO中断,一旦设置了GPIO_EDGE_SEL[n]时,GPIO会忽略ICR [n]设置,GPIO interrupt n的触发类型就是双边沿触发。

三、按键中断程序编程流程及示例

100ASK_IMX6ULL有2个按键,本节程序将设置它们的中断处理函数,在按键被按下或松开时进行打印;另外,还可以用KEY1来操作LED。

程序的总体流程是:

① 在中断向量中,保存现场,调用处理函数,恢复现场;

② 初始化:为KEY1、KEY2设置处理函数;初使化GPIO模块、初始化GIC;

③ 准备好一切之后,使能中断。

阅读代码时,建议按照下图来理解:

except.zip

3.1 管脚设置与中断号查询

     从上面的电路图可见KEY1接在GPIO5_1(SNVS_TAMPER1pad,ALT5)上,KEY4接在GPIO4_14(NAND_CE1_Bpad,ALT5)上。
     程序中使用IOMUXC_SetPinMux函数设置这两个引脚为GPIO模式。

     如何获取这两个GPIO的中断号呢?查阅数据手册的《chapter3,CORTEX A7interrupts》章节,这两个GPIO的中断号如下表所示。对应到GIC的SPI中断号需要在此编号基础上加上32,所以KEY1对应的GIC interrupt ID为(74+ 32= 106),KEY2对应的GIC interrupt ID为(72+ 32= 104)。

     当发生GIC 104号中断时,表示发生了GPIO4中interrupt 0~15,需要进一步细分出是GPIO4里的哪一个中断。

     当发生GIC 106号中断时,表示发生了GPIO5中interrupt 0~15,需要进一步细分出是GPIO5里的哪一个中断。

3.2 GIC控制器基地址的获取方法

直接查数据手册Table 2-1. System memory map,可以知道gic的基地址是0xA0000,如下图:
对于GIC基地址,还可以通过 CP15查询,下面指令将GIC的基地址读到r0寄存器:
mrc p15, 4, r0, c15, c0, 0

3.3 GIC初始化

gic.cgic.h
gic_init函数实现了如下功能:

①通过CP15获取GIC的基地址,

②读取GICD_TYPER寄存器获得中断的数目,

③ 往GICD_ ICENABLERn寄存器写入0xFFFFFFFF禁用所有的SGI,PPI和SPI;

④通过GICC_PMR设置优先级等级,设置为0xF8;

⑤将GICC_BPR设置为2,这允许各个优先级进行抢占;

⑥ 最后使能group0的distributor和CPU interface。

void gic_init(void) { u32 i, irq_num; GIC_Type *gic = get_gic_base(); /* the maximum number of interrupt IDs that the GIC supports */ irq_num = (gic->D_TYPER & 0x1F) + 1; /* On POR, all SPI is in group 0, level-sensitive and using 1-N model */ /* Disable all PPI, SGI and SPI */ for (i = 0; i < irq_num; i++)   gic->D_ICENABLER[i] = 0xFFFFFFFFUL; /* The priority mask level for the CPU interface. If the priority of an * interrupt is higher than the value indicated by this field, * the interface signals the interrupt to the processor. */ gic->C_PMR = (0xFFUL << (8 - 5)) & 0xFFUL; /* No subpriority, all priority level allows preemption */ gic->C_BPR = 7 - 5; /* Enables the forwarding of pending interrupts from the Distributor to the CPU interfaces. * Enable group0 distribution */ gic->D_CTLR = 1UL; /* Enables the signaling of interrupts by the CPU interface to the connected processor * Enable group0 signaling */ gic->C_CTLR = 1UL;

3.4 中断异常处理汇编部分

start.S
start.S中对于中断的处理,概括如下:

① 在异常向量表偏移为0x18的地方使用“ldr pc, =IRQ_Handler”跳转;

② IRQ_Handler标号的处理可以简单分为:保存现场,执行C函数,恢复现场:


start.s代码如下
.text .global  _start, _vector_table _start: _vector_table: ldr pc, =Reset_Handler /* Reset    */ ldr pc, =Undefined_Handler /* Undefined instructions */ ldr pc, =SVC_Handler /* Supervisor Call    */ b halt//ldr pc, =PrefAbort_Handler /* Prefetch abort    */ b halt//ldr pc, =DataAbort_Handler /* Data abort    */ .word 0 /* RESERVED    */ ldr pc, =IRQ_Handler /* IRQ interrupt    */ b halt//ldr pc, =FIQ_Handler /* FIQ interrupt    */ ……… .align 2          /* 告诉汇编程序,本伪指令下面的内存变量必须从下一个能被2 整除的地址开始分配*/ IRQ_Handler: /* 执行到这里之前: * 1. lr_irq保存有被中断模式中的下一条即将执行的指令的地址 * 2. SPSR_irq保存有被中断模式的CPSR * 3. CPSR中的M4-M0被设置为10010, 进入到irq模式 * 4. 跳到0x18的地方执行程序 (向量表的偏移地址确定) */ /* 保存现场 */ /* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */ /* lr-4是异常处理完后的返回地址, 也要保存 */ sub lr, lr, #4 stmdb sp!, {r0-r12, lr}  /* 处理irq异常 */ bl handle_irq_c /* 恢复现场 */ ldmia sp!, {r0-r12, pc}^  /* ^会把spsr_irq的值恢复到cpsr里 */ .align 2 Reset_Handler: /* Reset SCTlr Settings */ mrc p15, 0, r0, c1, c0, 0   /* read SCTRL, Read CP15 System Control register */ bic r0,  r0, #(0x1 << 13)   /* Clear V bit 13 to use normal exception vectors */ bic r0,  r0, #(0x1 << 12)   /* Clear I bit 12 to disable I Cache */ bic r0,  r0, #(0x1 <<  2)   /* Clear C bit  2 to disable D Cache */ bic r0,  r0, #(0x1 << 2)   /* Clear A bit  1 to disable strict alignment */ bic r0,  r0, #(0x1 << 11)   /* Clear Z bit 11 to disable branch prediction */ bic r0,  r0, #0x1   /* Clear M bit  0 to disable MMU */ mcr p15, 0, r0, c1, c0, 0   /* write SCTRL, CP15 System Control register */     cps     #0x1B                /* Enter undef mode                */     ldr     sp, =0x80300000     /* Set up undef mode stack      */     cps     #0x12                /* Enter irq mode                */     ldr     sp, =0x80400000     /* Set up irq mode stack      */     cps     #0x13                /* Enter Supervisor mode         */     ldr     sp, =0x80200000     /* Set up Supervisor Mode stack  */ /* 设置异常向量表基地址 : VBAR */ ldr r0, =_vector_table mcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/ //mrc p15, 0, r0, c12, c0, 0  //read VBAR bl clean_bss bl system_init cpsie i /* Unmask interrupts   */ bl main halt: b  halt clean_bss: /* 清除BSS段 */ ldr r1, =__bss_start ldr r2, =__bss_end mov r3, #0 clean: cmp r1, r2 strlt r3, [r1] add r1, r1, #4 blt clean mov pc, lr

注意执行Reset_Handler时,CPU处于IRQ模式,用的是IRQ模式下的栈,需要先在Reset_Handler里设置好IRQ模式的栈,这样在中断模式里才可以使用栈,才能调用C函数。

注意在Reset_Handler里调用“cpsie i”打开中断,这是把CPSR中的I位清零。

注意在Reset_Handler里使用如下两条指令设置异常向量的基地址

ldr r0, =_vector_tablemcr p15, 0, r0, c12, c0, 0  /* set VBAR, Vector Base Address Register*/

3.5 中断异常处理C函数部分

gic.c
handle_irq_c函数功能简述如下:

①获取到gic的基地址;

②读取GICC_IAR获得中断号;

③根据中断号调用对应中断号的irq_handler函数,该函数是用户通过request_irq注册的中断处理函数,

④ 然后往GICC_EOIR写入中断号清除掉中断。

gic代码如下:
void handle_irq_c(void) { int nr; GIC_Type *gic = get_gic_base(); /* The processor reads GICC_IAR to obtain the interrupt ID of the * signaled interrupt. This read acts as an acknowledge for the interrupt */ nr = gic-> C_IAR; printf("irq %d is happened\r\n", nr); irq_table[nr].irq_handler(nr, irq_table[nr].param); /* write GICC_EOIR inform the CPU interface that it has completed * the processing of the specified interrupt */ gic->C_EOIR = nr; }
谁调用reqeust_irq设置了irq_table[nr].irq_handler?请看下一节。

3.6 GPIO中断初始化与安装中断处理函数

main.c
先看看初始化函数key_irq_init,功能如下(以KEY1为例):

① 对于KEY1,对应的引脚是GPIO5_01,通过EDGE_SEL设置成双边沿触发;

②设置IMR使能中断;

③ 为了防止误触发,先将ISR对应位写1清除掉中断;

④ 调用request_irq注册对应中断的中断处理函数,就是设置irq_table数组中某一项,设置函数指针:

对于GPIO5_01,处理函数是key_gpio5_handle_irq;对于GPIO4_14,处理函数是key_gpio4_handle_irq


初始化代码如下:
void key_irq_init(void) { /* if set detects any edge on the corresponding input signal*/ GPIO5->EDGE_SEL |= (1 << 1); /* if set 1, unmasked, Interrupt n is enabled */ GPIO5->IMR |= (1 << 1); /* clear interrupt first to avoid unexpected event */ GPIO5->ISR |= (1 << 1); GPIO4->EDGE_SEL |= (1 << 14); GPIO4->IMR |= (1 << 14); GPIO4->ISR |= (1 << 14); request_irq(GPIO5_Combined_0_15_IRQn, (irq_handler_t)key_gpio5_handle_irq, NULL); request_irq(GPIO4_Combined_0_15_IRQn, (irq_handler_t)key_gpio4_handle_irq, NULL); }


还是以KEY1为例讲解处理函数key_gpio5_handle_irq,它的功能如下:

①读取GPIO_DR寄存器,根据GPIO5_01的状态打印信息、操作LED

② 在GPIO模块内部清除中断


代码如下:

void key_gpio5_handle_irq(void) { /* read GPIO5_DR to get GPIO5_IO01 status*/ if((GPIO5->DR >> 1 ) & 0x1) { printf("key 1 is release\r\n"); /* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */ GPIO5->DR |= (1<<3); //led on } else { printf("key 1 is press\r\n"); /* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */ GPIO5->DR &= ~(1<<3); //led off } /* write 1 to clear GPIO5_IO03 interrput status*/ GPIO5->ISR |= (1 << 1); } void key_gpio4_handle_irq(void) { /* read GPIO4_DR to get GPIO4_IO014 status*/ if((GPIO4->DR >> 14 ) & 0x1) printf("key 2 is release\r\n"); else printf("key 2 is press\r\n"); /* write 1 to clear GPIO4_IO014 interrput status*/ GPIO4->ISR |= (1 << 14); }

3.7 特定中断号的中断使能与禁止

设置好一切之后,就是使能中断了。

对于GIC,程序里使用gic_enable_irq,它的功能为:

① 根据中断号找到对应的GICD_ISENABLERn寄存器;

② 往相应位中写入1,即可使能中断。


要关闭中断时,操作是类似的,函数是gic_disable_irq,通过往GICD_ICENABLERn对应的位写入1来禁止中断。
void gic_enable_irq(IRQn_Type nr) { GIC_Type *gic = get_gic_base(); /* The GICD_ISENABLERs provide a Set-enable bit for each interrupt supported by the GIC. * Writing 1 to a Set-enable bit enables forwarding of the corresponding interrupt from the * Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled. */ gic->D_ISENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL)); } void gic_disable_irq(IRQn_Type nr) { GIC_Type *gic = get_gic_base(); /* The GICD_ICENABLERs provide a Clear-enable bit for each interrupt supported by the * GIC. Writing 1 to a Clear-enable bit disables forwarding of the corresponding interrupt from      * the Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled. */ gic->D_ICENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL)); }

3.8 修改CPSR使能中断

在start.S中,可以看到如下代码,它把CP15中SCTRL的值读出后,把I bit清零,再写入。这就是在CPU核中使能IRQ中断。

代码如下:

Reset_Handler: /* Reset SCTRL Settings */ mrc p15, 0, r0, c1, c0, 0   /* read SCTRL, Read CP15 System Control register */ bic r0,  r0, #(0x1 << 13)   /* Clear V bit 13 to use normal exception vectors */ bic r0,  r0, #(0x1 << 12)   /* Clear I bit 12 to disable I Cache */ bic r0,  r0, #(0x1 <<  2)   /* Clear C bit  2 to disable D Cache */ bic r0,  r0, #(0x1 << 2)   /* Clear A bit  1 to disable strict alignment */ bic r0,  r0, #(0x1 << 11)   /* Clear Z bit 11 to disable branch prediction */ bic r0,  r0, #0x1   /* Clear M bit  0 to disable MMU */ mcr p15, 0, r0, c1, c0, 0   /* write SCTRL, CP15 System Control register */

3.9 主函数调用

main.c
main函数调用system_init进行系统初始化,system_init做了这些事情:

①调用system_init_irq_table初始化中断跳转表;

②调用key_irq_init初始化按键中断:配置GPIO、注册中断处理函数;

③调用gic_init初始化GIC控制器;

④ 最后通过gic_enable_irq使能中断。

void system_init() { init_pins(); led_gpio_init(); led_ctl(0);//turn off led boot_clk_gate_init(); boot_clk_init(); uart1_init(); puts("hello world\r\n"); system_init_irq_table();    //初始化中断跳转表 key_irq_init();        //初始化按键中断、配置GPIO、注册中断处理函数 gic_init();        //初始化GIC控制器 gic_enable_irq(GPIO5_Combined_0_15_IRQn);//使能中断 gic_enable_irq(GPIO4_Combined_0_15_IRQn);//使能中断 }

3.10 程序编译烧录运行