一、中断向量表及中断服务函数的编写
1.建立中断向量表
中断向量表应该位于程序运行的起始地址处, 在中断向量表前面不要有其他代码
根据A7提供的中断向量编写相应的中断向量表
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
2.中断复位服务函数
在复位服务函数里面通过CP15协处理器关闭I,DCache和MMU,要关闭全局中断(临界区保护)
cpsid i /* 关闭全局中断 */
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */
然后设置中断向量表的偏移完成中断向量表重映射,目的地址:0X87800000,也可以在跳转到C语言后,和STM32那样用C语言处理
ldr r0, =0X87800000
dsb /*确保在下一条指令开始执行前,所有的存储器访问已经完成*/
isb /*清除流水线并且确保在新指令执行时,之前的指令都已经执行完毕*/
mcr p15, 0, r0, c12, c0, 0
dsb
isb
ARM汇编指令介绍:
DSB | 数据同步屏障,确保在下一条指令开始执行前,所有的存储器访问已经完成。在符合CMSIS的设备驱动库中,可以使用“__DSB”函数实现该操作 |
ISB | 指令同步屏障,清除流水线并且确保在新指令执行时,之前的指令都已经执行完毕。在符合CMSIS的设备驱动库中,可以使用“__ISB”函数实现该操作 |
设置各个模式下的栈指针,IMX6UL的堆栈是向下增长的。堆栈指针地址一定要是4字节地址对齐的。CPSR 是当前程序状态寄存器,该寄存器包含了条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位,低4位用来控制处理器的模式
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
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_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /* 打开全局中断 */
最后还要打开全局中断,当初始化工作都完成以后就可以进入到 main 函数,也是该函数的返回值
b main /* 跳转到main函数 */
3.外部中断服务函数
有的外部中断都会触发 IRQ 中断, 所以 IRQ 中断服务函数主要的工作就是区分当前发生的什么中断(中断 ID),然后针对不同的外部中断做出不同的处理。
先保存现场,其余寄存器会自动保存,只有r0-r3, r12和spsr寄存器需要手动保存
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */
然后先通过 c15 寄存器可以获取 GIC 基地址,读取GICC_IAR寄存器获取当前中断号,中断号被保存到了 r0 寄存器中
mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C15寄存器内的值到R1寄存器中*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */
之后就是中断处理,调用了函数 system_irqhandler,函数 system_irqhandler 是一个 C 语言函数,此函数有一个参数,参数为中断号
根据 ATPCS(ARM-Thumb Procedure Call Standard)定义的函数参数传递规则,在汇编调用 C 函数的时候建议形参不要超过 4 个, 形参可以由 r0~r3 这四个寄存器来传递,如果形参大于 4 个, 那么大于 4 个的部分要使用堆栈进行传递。 所以给 r0 寄存器写入中断号就可以了函数system_irqhandler 的参数传递
cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */
pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */
中断的真正处理过程其实是在函数 system_irqhandler 中完成,稍后需要编写函system_irqhandle
注:当一个中断处理完成以后必须向 GICC_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 */
最后PC = LR-4的原因是ARM 的指令是三级流水线:取指、译指、执行, pc 指向的是正在取值的地址(pc=当前执行指令地址+8),lr保存的是中断发生是pc的值,下一步应该执行译指的地址,所以lr-4刚好指向译指的指令
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”开始执行
4.其他中断服务函数
其他中断服务函数都是死循环
/* 未定义中断 */
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
/* FIQ中断 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
二、通用中断驱动文件编写
在 汇编文件中我们在中断服务函数 IRQ_Handler 中调用了 C 函数 system_irqhandler 来处理具体的中断。此函数有一个参数,参数是中断号,但是函数 system_irqhandler 的具体内容还没有实现,所以需要实现函数 system_irqhandler 的具体内容。
不同的中断源对应不同的中断处理函数, MX6U 有 160 个中断源,所以需要 160 个中断处理函数,可以将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。当中断发生以后函数 system_irqhandler 根据中断号从中断处理函数数组中找到对应的中断处理函数并执行即可
定义中断需要的中断处理结构体和变量
/* 中断服务函数形式 */
typedef void (*system_irq_handler_t) (unsigned int giccIar, void *param);
/* 中断服务函数结构体*/
typedef struct _sys_irq_handle
{
system_irq_handler_t irqHandler; /* 中断服务函数 */
void *userParam; /* 中断服务函数参数 */
} sys_irq_handle_t;
/* 中断嵌套计数器 */
static unsigned int irqNesting;
/* 中断服务函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
结构体 sys_irq_handle_t 包含一个中断处理函数和中断处理函数的用户参数。一个中断源就需要一个 sys_irq_handle_t 变量, IMX6U 有 160 个中断源,因此需要 160 个 sys_irq_handle_t 组成中断处理数组
然后需要初始化中断:初始化GIC(中断控制),初始化中断表,将所有的中断服务函数设置为默认值,中断向量表的地址偏移也可以放在这里
//中断初始化函数
void int_init(void)
{
GIC_Init(); /* 初始化GIC */
system_irqtable_init(); /* 初始化中断表 */
__set_VBAR((uint32_t)0x87800000); /* 中断向量表偏移,偏移到起始地址 */
}
//初始化中断服务函数表
void system_irqtable_init(void)
{
unsigned int i = 0;
irqNesting = 0;
/* 先将所有的中断服务函数设置为默认值 */
for(i = 0; i < NUMBER_OF_INT_VECTORS; i++)
{
system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);
}
}
//给指定的中断号注册中断服务函数
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}
//默认中断服务函数
void default_irqhandler(unsigned int giccIar, void *userParam)
{
while(1)
{
}
}
system_register_irqhandler()是注册中断处理函数 ,此函数用来给指定的中断号注册中断处理函数。如果要使用某个外设中断,那就必须调用此函数来给这个中断注册一个中断处理函数
最后还差system_irqhandler()中断服务函数,此函数会通过GIC获取中断号,根据中断号在中断处理函数表 irqTable 中取出对应的中断处理函数并执行
void system_irqhandler(unsigned int giccIar)
{
uint32_t intNum = giccIar & 0x3FFUL;
/* 检查中断号是否符合要求 */
if (intNum >= NUMBER_OF_INT_VECTORS)
{
return;
}
irqNesting++; /* 中断嵌套计数器加一 */
/* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/
irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
irqNesting--; /* 中断执行完成,中断嵌套寄存器减一 */
}
irq汇编中断服务函数会调用此函数,此函数通过在中断服务列表中查找指定中断号所对应的中断处理函数并执行
三、外设中断配置
中断系统的大致框架已经搭好了,要使用中断我们要对相应的外设进行中断配置和编写相应的中断服务函数,下面以GPIO的中断为例
外设中断所需要的结构体和变量
/*
* 枚举类型和结构体定义
*/
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;
/*
* GPIO中断触发类型枚举
*/
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;
/*
* GPIO配置结构体
*/
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
gpio_interrupt_mode_t interruptMode; /* 中断方式 */
} gpio_pin_config_t;
在初始化GPIO的时候就把GPIO中断的初始化给配置好
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
base->IMR &= ~(1U << pin);
if(config->direction == kGPIO_DigitalInput) /* GPIO作为输入 */
{
base->GDIR &= ~( 1 << pin);
}
else /* 输出 */
{
base->GDIR |= 1 << pin;
gpio_pinwrite(base,pin, config->outputLogic); /* 设置默认输出电平 */
}
gpio_intconfig(base, pin, config->interruptMode); /* 中断功能配置 */
}
然后设置GPIO的中断配置功能,需要配置GPIOx_ICR1/2寄存器和EDGE_SEL寄存器 ( 具体功能参考手册)
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pin_int_mode)
{
volatile uint32_t *icr;
uint32_t icrShift;
icrShift = pin;
base->EDGE_SEL &= ~(1U << pin);
if(pin < 16) /* 低16位 */
{
icr = &(base->ICR1);
}
else /* 高16位 */
{
icr = &(base->ICR2);
icrShift -= 16;
}
switch(pin_int_mode)
{
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
case(kGPIO_IntHighLevel):
*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;
}
}
编写GPIO中断的使能和禁止功能(由IMR寄存器控制)以及中断完成后清除中断标志位函数,主要这里是写1清除
void gpio_enableint(GPIO_Type* base, unsigned int pin)
{
base->IMR |= (1 << pin);
}
void gpio_disableint(GPIO_Type* base, unsigned int pin)
{
base->IMR &= ~(1 << pin);
}
void gpio_clearintflags(GPIO_Type* base, unsigned int pin)
{
base->ISR |= (1 << pin);
}
四、具体外设中断实现
将按键设置为下降沿触发中断,并执行中断服务函数里面的程序
先配置对应IO的的中断初始化,然后使能GIC中对应的中断号,根据中断号注册相应的中断服务函数,使能中断相应的中断
void exit_init(void)
{
gpio_pin_config_t key_config;
/* 1、设置IO复用 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
/* 2、初始化GPIO为中断模式 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
system_register_irqhandler(GPIO1_Combined_16_31_IRQn, (system_irq_handler_t)gpio1_io18_irqhandler, NULL); /* 注册中断服务函数 */
gpio_enableint(GPIO1, 18);
}
编写相应的中断服务函数
void gpio1_io18_irqhandler(void)
{
static unsigned char state = 0;
delay(10);
if(gpio_pinread(GPIO1, 18) == 0) /* 按键按下了 */
{
state = !state;
beep_switch(state);
}
gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}
每次中断完成后要写1清除标志位