Cortex-A系列中断
- 2021 年 11 月 24 日
- 筆記
- Linux嵌入式——裸机开发
1. 回顾STM32系统
1.1 中断向量表
ARM芯片冲0x00000000,在程序开始的地方存放中断向量表,按下中断时,就相当于告诉CPU进入的函数。描述很多个中断服务函数的表。
对于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,中断向量表需要用户自己去定义
这里面用pc因为pc执行完就进入下一个+0x04,在前面就定义各个中断
GIC V2是cortex7-A使用的,最多支持8个核,V3和V4是给64位芯片使用的
- 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
c1
c12
c15
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);
}
中断驱动
- 我们首先要设置GPIO的中断触发方式,也就是ICR1和ICR2寄存器。触发方式有低电平高电平上升沿下降沿,对于本例程来说,按键是属于下降沿触发
- IMR寄存器使能GPIO对应的中断
- edge寄存器是设置边沿触发的
- ISR寄存器,每一个IO都有一个,处理完中断之后要清除中断标志位,也就是清除ISR,清除不是写0,ISR是写1清除
GIC配置
- 使能相应的中断id,比如key是GPIO1——IO18,就要找到它的中断id,由图可知是67
注意这里面虽然是67,但是别忘了多核中断的32个,所以要67+32
- 设置相应的相应优先级
- 注册中断处理函数
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 错误解决方案
这种情况一般是没有在makefile添加搜索路径
按下按键后,没有反应并且卡死
从04开始必须是中断向量表,在链接文件中可以看到,中断并不是从04开始的,被bss段占了
第一种解决方案,就是中断向量偏移改成8开始,但是默认其低5位必须是0
第二种是屏蔽清理bss段 或者把它移动到清理之前的位置