Cortex-A系列中断

1. 回顾STM32系统

1.1 中断向量表

ARM芯片冲0x00000000,在程序开始的地方存放中断向量表,按下中断时,就相当于告诉CPU进入的函数。描述很多个中断服务函数的表。
image
对于STM32来说,代码最开始存放栈顶指针(0x80000000),然后是Reset_Handler(0x80000004复位中断),以此类推

1.2 中断向量偏移

一般ARM是从0x00000000,32是从0x80000000,I.MX是0x87800000,所以要设置中断向量偏移,32中设置SCB的VTOR寄存器为新的中断向量表起始地址即可

1.3 nvic中断控制器,使能和关闭中断,设置中断优先级

1.4 中断服务函数编写

2. Cortex-A中断

主要是IRQ中断 0x18

2.1 Cortex-A 中断向量表

Cortex-A 中断向量有8个中断,其中重点关注IRQ,中断向量表需要用户自己去定义
image
image
这里面用pc因为pc执行完就进入下一个+0x04,在前面就定义各个中断
GIC V2是cortex7-A使用的,最多支持8个核,V3和V4是给64位芯片使用的
image

  • SPI:共享中断,那些外部中断都属于SPI
  • PPI:私有中断,GIC是多核的,每个核都有自己的私有中断
  • SGI:软件中断,由软件触发引起的中断,通过写入寄存器来完成,系统会使用它完成多核中断

2.2 中断号

为了区分不同的中断,引入了中断号,1020个中断号。
0-15是给SGI
15-31是给PPI。
剩下的给SPI,但在I.MX6U只用到了160个,SPI是128个中断,CortexA7有128个中断。
中断ID的作用的是让IRQ认识到是哪个中断。

2.3 中断服务函数

一个是IRQ中断服务函数的编写,另一个是在IRQ中断服务函数里面去查找并运行的具体的外设中断服务函数。

3. 代码(编写按键中断例程)

由原理图可知,Key0使用UARTA1_CTS这个IO

3.1 修改汇编文件

编写复位中断函数

  • 关闭ID Cache和MMU
  • 设置处理器9种模式下的对应sp指针,要使用中断必须设置IRQ模式下的sp指针,直接设置所有的sp指针
  • 清除bss段
  • 进入main
    ···

···

3.2 cp15协处理器

c0

image
image

c1

image
image

c12

image

c15

image
image

3.3 代码编写

IRQ中断服务函数编写,根据上面介绍的寄存器,要编写汇编代码和systemIRQhandler函数,具体过程已经写好。(汇编的介绍在中断栈分析里面讲过)

汇编代码

.global _start  		/* 全局标号 */
/*
 * 描述:	_start函数,程序从此函数开始执行,此函数主要功能是设置C
 *		 运行环境。
 */
_start:
	/* 编写中断向量表 */
	ldr pc,= Reset_Handler      /* 复位中断函数,名字可以随便写,下面对上*/
	ldr pc,= Undefined_Handler  /* 未定义指令中断 */
	ldr pc,= SVC_Handler		/* SVC中断 */
	ldr pc,= PrefAbort_Handler  /* 预取中止 */
	ldr pc,= DataAbort_Handler  /* 数据中止 */
	ldr pc,= NotUsed_Handler    /* 没用中断 */
	ldr pc,= IRQ_Handler		/* 中断 */
	ldr pc,= FIQ_Handler		/* 快速中断 */

 /* 复位中断 */ 
Reset_Handler:
	cpsid i 					/* 关闭全局中断 */
	/* 关闭 I,DCache 和 MMU 
	* 采取读-改-写的方式。
	*/
	mrc p15, 0, r0, c1, c0, 0 	/* 读取 CP15 的 C1 寄存器到 R0 中 */
	bic r0, r0, #(0x1 << 12) 	/* 清除 C1 的 I 位,关闭 I Cache */
	bic r0, r0, #(0x1 << 2) 	/* 清除 C1 的 C 位,关闭 D Cache */
	bic r0, r0, #0x2 			/* 清除 C1 的 A 位,关闭对齐检查 */
	bic r0, r0, #(0x1 << 11) 	/* 清除 C1 的 Z 位,关闭分支预测 */
	bic r0, r0, #0x1 			/* 清除 C1 的 M 位,关闭 MMU */
	mcr p15, 0, r0, c1, c0, 0 	/* 将 r0 的值写入到 CP15 的 C1 中 */

#if 0
	/* 汇编版本设置中断向量表偏移 */ @也可以在c语言里面做
	ldr r0, =0X87800000
	dsb          				@同步指令 一个是数据同步一个是指令同步
	isb
	mcr p15, 0, r0, c12, c0, 0
	dsb
	isb
#endif

/* 清理bss段的代码 */
.global _bss_start
_bss_start:
	.word _bss_start   /* 相当于定义了一个段,类似声明一个数,word是一个word长度 */
.global _bss_end
_bss_end:
	.word _bss_end

	/* 清除BSS段 */
	ldr r0, _bss_start
	ldr r1, _bss_end
	mov r2, #0
bss_loop:
	stmia r0!, {r2} @将r2里面的数据转存到r0
	cmp r0, r1 @比较两个寄存器里面的值
	ble bss_loop @如果小于等于r1,继续清楚bss段

	/* 设置各个模式下的栈指针,
	* 注意:IMX6UL 的堆栈是向下增长的!
	* 堆栈指针地址一定要是 4 字节地址对齐的!!!
	* DDR 范围:0X80000000~0X9FFFFFFF 或者 0X8FFFFFFF
	*/

	/* 进入 IRQ 模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
	orr r0, r0, #0x12 	/* r0 或上 0x12,表示使用 IRQ 模式 */
	msr cpsr, r0 		/* 将 r0 的数据写入到 cpsr 中 */
	ldr sp, =0x80600000 /* IRQ 模式栈首地址为 0X80600000,大小为 2MB */	
	
	/* 进入 SYS 模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
	orr r0, r0, #0x1f 	/* r0 或上 0x13,表示使用 SYS 模式 */
	msr cpsr, r0 		/* 将 r0 的数据写入到 cpsr 中 */
	ldr sp, =0x80400000 /* SYS 模式栈首地址为 0X80400000,大小为 2MB */	

	/* 进入 SVC 模式 最后设置这个直接进入C语言*/
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
	orr r0, r0, #0x13 	/* r0 或上 0x13,表示使用 SVC 模式 */
	msr cpsr, r0 		/* 将 r0 的数据写入到 cpsr 中 */
	ldr sp, =0X80200000 /* SVC 模式栈首地址为 0X80200000,大小为 2MB */	

	cpsie i 			/* 打开全局中断 */

#if 0
	/* 使能 IRQ 中断 和cpsie i冲突啦,可以不屑*/
	mrs r0, cpsr 		/* 读取 cpsr 寄存器值到 r0 中 */
	bic r0, r0, #0x80 	/* 将 r0 寄存器中 bit7 清零,也就是 CPSR 中
						 * 的 I 位清零,表示允许 IRQ 中断
						 */
	msr cpsr, r0 		/* 将 r0 重新写入到 cpsr 中 */
#endif
	b main 				/* 跳转到 main 函数

/* 未定义中断 */
Undefined_Handler:
	ldr r0, =Undefined_Handler
	bx r0 

/* SVC 中断 */
SVC_Handler:
	ldr r0, =SVC_Handler
	bx r0 

/* 预取终止中断 */
PrefAbort_Handler:
	ldr r0, =PrefAbort_Handler 
	bx r0 

/* 数据终止中断 */
DataAbort_Handler:
	ldr r0, =DataAbort_Handler
	bx r0

/* 未使用的中断 */
NotUsed_Handler:
	ldr r0, =NotUsed_Handler
	bx r0

/* IRQ 中断!重点!!!!! */
IRQ_Handler:
	push {lr} 					/* 保存 lr 地址 */
	push {r0-r3, r12} 			/* 保存 r0-r3,r12 寄存器 */
	mrs r0, spsr 				/* 读取 spsr 寄存器 */
	push {r0} 					/* 保存 spsr 寄存器 */
	mrc p15, 4, r1, c15, c0, 0  /* 将 CP15 的 C0 内的值到 R1 寄存器中
								 * 参考文档 ARM Cortex-A(armV7)编程手册 V4.0.pdf P49
								 * Cortex-A7 Technical ReferenceManua.pdf P68 P138
								 */ 
	add r1, r1, #0X2000 		/* GIC 基地址加 0X2000,得到 CPU 接口端基地址 */
	ldr r0, [r1, #0XC] 			/* CPU 接口端基地址加 0X0C 就是 GICC_IAR 寄存器,
								 * GICC_IAR 保存着当前发生中断的中断号,我们要根据
								 * 这个中断号来绝对调用哪个中断服务函数
								 * 这样就通过R0传参啦
								 */
	push {r0, r1} 				/* 保存 r0,r1 */

	@ 中断来了必须再SVC模式下处理, R0,R1,R2传参
	cps #0x13 					/* 进入 SVC 模式,允许其他中断再次进去 */

	push {lr} 					/* 保存 SVC 模式的 lr 寄存器 */
	@ 必须用r2因为只有它能用
	ldr r2, =system_irqhandler  /* 加载 C 语言中断处理函数到 r2 寄存器中, 默认用r0传参*/
	blx r2 						/* 运行 C 语言中断处理函数,带有一个参数 */
	pop {lr}				 	/* 执行完 C 语言中断服务函数,lr 出栈 */
	@ 处理完毕后返回IRQ模
	cps #0x12 					/* 进入 IRQ 模式 */
	pop {r0, r1} 
	@ 处理完之后,将IAR写入到EOIR里面
	str r0, [r1, #0X10] 		/* 中断执行完成,写 EOIR */
	pop {r0} 
	msr spsr_cxsf, r0 			/* 恢复 spsr */
	pop {r0-r3, r12} 			/* r0-r3,r12 出栈 */
	pop {lr} 					/* lr 出栈 */
	subs pc, lr, #4 			/* 将 lr-4 赋给 pc */

/* FIQ 中断 */
FIQ_Handler:
	ldr r0, =FIQ_Handler 
	bx r0

在bsp文件夹里面建立int文件夹,来初始化中断服务函数。

bsp_int.h

#ifndef __BSP_INT_H
#define __BSP_INT_H
#include "imx6ul.h"
/* 定义中断处理函数的形式 */
/* 函数指针的形式定义 
 * gicciar: 中断号
 * param: 中断传过来的参数
 */
typedef void (*system_irq_handler_t)(unsigned int gicciar, 
                                             void *param);

/* 中断处理函数结构体,IMX6UL具有160个中断 */
typedef struct _sys_irq_handle
{
    system_irq_handler_t irq_handler;  /* 中断处理函数 */
    void *userParam;                   /* 中断处理函数的参数, 这个和上面的param是一个 */
}sys_irq_handle_t;

void int_init();
void default_irqhanler(unsigned int gicciar, void *param);
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam);
void system_irqhandler(unsigned int giccIar);

#endif // !__INT_H

bsp_int.c

#include "bsp_int.h"

static unsigned int irqNesting; //中断嵌套计数
/* 中断处理函数结构体 */
/* NUMBER_OF_INT_VECTORS有定义,160
 * 并在下面定义了枚举类型,表示各个中断号
 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
/* 既然有了函数表,就初始化中断处理函数表 */
/* 给这个表的每个函数都写上默认值 */
void system_irqtable_init(void)
{
    irqNesting = 0;             /* 初始化的时候清零中断嵌套,
                                 * 每进入system的时候,
                                 * +1 */
    unsigned int i = 0;
    for (i = 0; i < NUMBER_OF_INT_VECTORS; ++i)
    {
        /* code */
        irqTable[i].irq_handler = default_irqhanler;
        irqTable[i].userParam = NULL;
    }
    
}

/* 注册中断处理函数,每个中断都要注册,如果要用到这个中断 */
/* irq:枚举类型的中断号 
 * handler:中断处理函数
 * userParam:参数
 */
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
    irqTable[irq].irq_handler = handler;
    irqTable[irq].userParam = userParam;
}

/* 中断初始化 */
void int_init()
{
    /* GIC初始化,这个core_ca7里面已经定义好了 */
    GIC_Init();
    /* 中断表初始化 */
    system_irqtable_init();

    /* 中断向量偏移设置 , 也是core_ca7里面的函数*/
    __set_VBAR(0x87800000);
}
/* 具体的中断处理函数,IRQ_HANDLer会调用次函数 */
void system_irqhandler(unsigned int giccIar)
{
    /* giccIar是汇编中传进来的中断号,和前10位与一下可以得到中断号具体值 */
    /* 这里面的intNum是中断号,1023最大 */
    uint32_t intNum = giccIar & 0x3FF;
    /* 检查中断id */
    if(intNum == 1023 || intNum >= NUMBER_OF_INT_VECTORS)
    {
        return;
    }

    /* 根据中断id号,读取中断处理函数 */
    ++irqNesting;
    irqTable[intNum].irq_handler(intNum, irqTable[intNum].userParam);
    --irqNesting;  //处理完之后减1
}
/* 默认中断处理函数 */
void default_irqhanler(unsigned int gicciar, void *param)
{
    while(1);
}

中断驱动

image

  • 我们首先要设置GPIO的中断触发方式,也就是ICR1和ICR2寄存器。触发方式有低电平高电平上升沿下降沿,对于本例程来说,按键是属于下降沿触发
    image
  • IMR寄存器使能GPIO对应的中断
  • edge寄存器是设置边沿触发的
    image
  • ISR寄存器,每一个IO都有一个,处理完中断之后要清除中断标志位,也就是清除ISR,清除不是写0,ISR是写1清除

GIC配置

  • 使能相应的中断id,比如key是GPIO1——IO18,就要找到它的中断id,由图可知是67
    注意这里面虽然是67,但是别忘了多核中断的32个,所以要67+32
    image
    image
  • 设置相应的相应优先级
  • 注册中断处理函数
    gpio.h
/* 为了方便gpio的驱动编写,编写一个gpio驱动文件 */
#ifndef __BSP_GPIO
#define  __BSP_GPIO
#include "imx6ul.h"
/* 枚举类型,用于描述中断触发类型 */
typedef enum _gpio_interrupt_mode
{
    /* 建议和寄存器里面的值对应 */
    kGPIO_NoIntMode = 0U,               /* 无触发 */
    kGPIO_IntLowLevel = 1U,             /* 低电平触发 */
    kGPIO_IntHighLevel = 2U,            /* 高电平触发 */
    kGPIO_IntRisingEdge = 3U,           /* 上升沿触发 */
    kGPIO_IntFallingEdge = 4U,          /* 下降沿触发 */
    kGPIO_IntRisingOrFallingEdge = 5U,  /* 上升沿和下降沿都触发 */
} gpio_interrupt_mode_t;

/* 枚举类型和结构体定义 */
typedef enum _gpio_pin_direction
{
    // 0U和1U是无符号整型的0和1
    kGPIO_DigitalInput = 0U, /* 输入 */
    kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;

/* GPIO 配置结构体 */
typedef struct _gpio_pin_config
{
    gpio_pin_direction_t direction;   /* GPIO 方向:输入还是输出 */
    uint8_t outputLogic;              /* 如果是输出的话,默认输出电平 */
    gpio_interrupt_mode_t interruptMode;  /* 中断类型 */
} gpio_pin_config_t;

/* 函数声明 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
int gpio_pinread(GPIO_Type *base, int pin);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);

/* 与中断有关的函数声明 */
void gpio_enable(GPIO_Type *base, int pin);
void gpio_disable(GPIO_Type *base, int pin);
void gpio_clearIntFlags(GPIO_Type *base, int pin);
void gpio_intConfig(GPIO_Type *base, int pin, gpio_interrupt_mode_t interruptMode);
#endif // !__BSP_GPIO

gpio.c

#include "bsp_gpio.h"
/*
* @description : GPIO 初始化。
* @param - base : 要初始化的 GPIO 组。
* @param - pin : 要初始化 GPIO 在组内的编号。
* @param - config : GPIO 配置结构体。
* @return : 无
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{   
    // base是GPIO的类型,比如DR,GDIR等
    // 我们一般操作用这两个比较多,比如配置输入输出
    // pin是第几个脚
    if(config->direction == kGPIO_DigitalInput) /* 输入 */
    {
        base->GDIR &= ~( 1 << pin);
    }else {/* 输出 */
        base->GDIR |= 1 << pin;
        gpio_pinwrite(base, pin, config->outputLogic);/* 默认输出电平 */
    }
    /* 配置中断 */
    gpio_intConfig(base, pin, config->interruptMode);
}
/*
* @description : 读取指定 GPIO 的电平值 。
* @param – base : 要读取的 GPIO 组。
* @param - pin : 要读取的 GPIO 脚号。
* @return : 无
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
    return (((base->DR) >> pin) & 0x1);
}
/*
* @description : 指定 GPIO 输出高或者低电平 。
* @param – base : 要输出的的 GPIO 组。
* @param - pin : 要输出的 GPIO 脚号。
* @param – value : 要输出的电平,1 输出高电平, 0 输出低低电平
* @return : 无
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
    if (value == 0U)
    {
        base->DR &= ~(1U << pin); /* 输出低电平 */
    }else{
        base->DR |= (1U << pin); /* 输出高电平 */
    }
}

/* 使能指定IO中断 */
void gpio_enable(GPIO_Type *base, int pin)
{
    base->IMR |= (1U << pin);     /* 1使能 */
}

/* 关闭指定IO中断 */
void gpio_disable(GPIO_Type *base, int pin)
{
    base->IMR &= ~(1U << pin);     /* 0关闭 */
}

/* 清楚中断标志位 */
void gpio_clearIntFlags(GPIO_Type *base, int pin)
{
    base->ISR |= (1U << pin);    /* 1使能 */
}


/* GPIO中断初始化函数 */
void gpio_intConfig(GPIO_Type *base, int pin, 
                    gpio_interrupt_mode_t interruptMode)
{
    /* 定义一个指针 */
    /* 用这个来表示具体使用的哪一个ICR寄存器 , 因为有两个寄存器!*/
    volatile uint32_t *icr;  //数据类吆喝base里面的一样
    uint32_t icrShift;

    icrShift = pin;
    /* 先清零,如果它置1,ICR就无效啦 */
    base->EDGE_SEL &= ~(1 << pin);
    if(pin < 16)
    {
        icr = &(base->ICR1);
    }else{
        icr = &(base->ICR2);
        //注意是高位的话,ICR2从0开始,也就是pin是16,但是用ICR2就是0
        icrShift -= 16;
    }
    switch (interruptMode)
    {
        /* 00 : 低电平触发
         * 01 :高点平触发
         * 10 :上升沿
         * 11 :下降沿
         * 边沿触发:EDGE_SEL为1
         */
        case(kGPIO_IntLowLevel):
            *icr &= ~(3U << (2 * icrShift));
            break;
        case(kGPIO_IntHighLevel):
            //先清0
            *icr = (*icr & (~(3U << (2 * icrShift)))) | (1U << (2 * icrShift));
            break;
        case(kGPIO_IntRisingEdge):
            *icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));
            break;
        case(kGPIO_IntFallingEdge):
            *icr |= (3U << (2 * icrShift));
            break;
        case(kGPIO_IntRisingOrFallingEdge):
            base->EDGE_SEL |= (1U << pin);
            break;
        default:
            break;
    }
}

外部中断
exit.h

#ifndef __BSP_EXIT_H
#define __BSP_EXIT_H
#include "imx6ul.h"

void exit_init();
void gpio1_io18_irqhandler(uint32_t giccIar, void *param);
#endif // !__BSP_EXIT_H

exit.c

#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"

void exit_init()
{
    gpio_pin_config_t key_config;
    /* 初始化IO复用,为GPIO */
    IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
    /* 2、、配置 UART1_CTS_B 的 IO 属性 
    *bit 16:0 HYS 关闭
    *bit [15:14]: 11 默认 22K 上拉
    *bit [13]: 1 pull 功能
    *bit [12]: 1 pull/keeper 使能
    *bit [11]: 0 关闭开路输出
    *bit [7:6]: 10 速度 100Mhz
    *bit [5:3]: 000 关闭输出
    *bit [0]: 0 低转换率
    */
    IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);

    /* 初始化这个GPIO为输入 */
    key_config.outputLogic = kGPIO_DigitalInput;
    key_config.interruptMode = kGPIO_IntFallingEdge;
    gpio_init(GPIO1, 18, &key_config);

    /* GIC使能,已经定义好啦 */
    GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
    /* 注册中断服务函数 */
    system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
                               gpio1_io18_irqhandler ,NULL);
    /* gpio使能 */
    gpio_enable(GPIO1, 18);
}

/* 中断处理函数格式 */
void gpio1_io18_irqhandler(uint32_t giccIar, void *param)
{
    static unsigned char state = 0;
    /*
    *采用延时消抖,中断服务函数中禁止使用延时函数!因为中断服务需要
    *快进快出!!这里为了演示所以采用了延时函数进行消抖,后面我们会讲解
    *定时器中断消抖法!!!
    */
    delay(10);
    if(gpio_pinread(GPIO1, 18) == 0) /* 按键按下了 */
    {
        state = !state;
        beep_switch(state);
    }

    gpio_clearIntFlags(GPIO1, 18); /* 清除中断标志位 */
}

3.4 错误解决方案

image
这种情况一般是没有在makefile添加搜索路径

按下按键后,没有反应并且卡死

image
从04开始必须是中断向量表,在链接文件中可以看到,中断并不是从04开始的,被bss段占了
第一种解决方案,就是中断向量偏移改成8开始,但是默认其低5位必须是0
第二种是屏蔽清理bss段 或者把它移动到清理之前的位置