前面的按键实验是通过死循环一直读取按钮电平值来判断是否有按下按钮,接下来将使用另外一个更优雅的方式实验按键按下功能-中断。
CPU在正常处理指令的时候会遇到外设打断当前执行逻辑,我们称为异常中断。一系列中断处理集中在一起管理,我们称为异常中断向量表。
中断向量表
Coretex-A系列的中断向量表就是存放在程序起始位置(链接起始地址)的一组由4字节组成的一组数据,Coretex-A 32位处理器每一条指令长度就是4个字节,所以本质上这个中断向量表是一组固定地址的指令。Coretext-A系统CPU总共支持8个中断:
这8个中断里面需要特别注意也是需要开发的主要是复位中断
与IRQ中断
。复位中断
在上电或者按下Reset按钮后硬件加载程序同时PC
寄存器位置重置为0x0或者链接起始地址时触发,IRQ中断
则是外设触发。每一个中断发生时PC
寄存器会被设置成一个固定的地址,而这个地址则对应中断向量表中一条指令。
中断向量表添加到汇编最开始的位置:
/* 从链接起始地址开始,8条4字节的指令组成了ARM的中断向量表 */
/* 中断向量表放在最开始的位置,每一条指令对应了具体的中断处理 */
/* 当发生对应中断时,硬件会把对应的地址设置到pc寄存器,从而执行对应的中断服务函数 */
ldr pc, =Reset_Handler /* 0x00: 复位中断 */
ldr pc, =Undefine_Instruction_Handler /* 0x04: 未定义中断指令 */
ldr pc, =Software_Interrupt_Handler /* 0x08: 软中断, SVC特权模式 */
ldr pc, =Prefetch_Abort_Handler /* 0x0c: 指令预取中止中断 */
ldr pc, =Data_Abort_Handler /* 0x10: 数据访问中止中断 */
ldr pc, =Not_Used_Handler /* 0x14: 未使用的中断 */
ldr pc, =IRQ_Handler /* 0x18: 外部设备中断 */
ldr pc, =FIQ_Handler /* 0x1c: 快速中断 */
复位中断服务函数
上电后第一个要触发的则是复位中断
,通过向量表中定义的指令可以将程序切换到Reset_Handler
处开始执行
- 关闭IRQ
- 关闭I,D Cache,以及MMU
- 设置中断的起始地址,即设置成链接起始地址(因为程序是从链接起始地址开始运行的)
- 设置IRQ,SVC以及SYS模式下C语言的运行环境(C语言的SP指针栈顶)
- 打开IRQ
- 调转到C语言的main函数开始运行
cpsid i /* 关闭IRQ, 此时IRQ还没有配置完成,所以关闭*/
/*
在设备上电启动时,执行的代码访问的外设都是实际地址,
mmu与cache此时的意义不大,
这个时候为了防止cache与mmu可能导致的问题会先将mmu与cache关闭
*/
/* CP15: SCTLR */
/* 关闭 I-Cache, D-Cache, MMU */
MRC p15, 0, r0, c1, c0, 0 /* 将SCTLR寄存器读取到r0寄存器 */
bic r0, r0, #(1 << 0) /* 关闭MMU */
bic r0, r0, #(1 << 1) /* 关闭对齐 */
bic r0, r0, #(1 << 11) /* 关闭分支预测 */
bic r0, r0, #(1 << 12) /* 关闭i-cache */
bic r0, r0, #(1 << 2) /* 关闭d-cache */
MCR p15, 0, r0, c1, c0, 0 /* 将r0寄存器数据写入到SCTLR寄存器 */
/* 设置中断向量偏移,在发生中断之前设置即可,也可以在C语言中设置 */
ldr r0, =0x87800000 /* 将0x87800000这个立即数写入到 r0寄存器, 也就是链接起始地址*/
dsb /* 这里涉及到了改变读取内存的地址起始地址,需要使用内存屏障指令进隔断,保证前后读取指令都是正常的地址 */
isb /* 这里涉及到了改变读取内存的地址起始地址,需要使用内存屏障指令进隔断,保证前后读取指令都是正常的地址 */
MCR p15, 0, r0, c12, c0, 0 /* 将r0的数据写入到VBAR寄存器中,表示向量偏移地址是0x87800000 */
dsb /* 这里涉及到了改变读取内存的地址起始地址,需要使用内存屏障指令进隔断,保证前后读取指令都是正常的地址 */
isb /* 这里涉及到了改变读取内存的地址起始地址,需要使用内存屏障指令进隔断,保证前后读取指令都是正常的地址 */
/* 设置不同模式下的sp指针,每一个模式的sp对应不同的物理地址,当进入不同工作模式时C语言会在不同的物理sp指针指向的栈内存上工作 */
/* 进入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_c中 */
ldr sp, =0x80600000 /* 设置栈指针 */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x1f,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置栈指针 */
/* 进入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 /* 设置栈指针 */
cpsie i /* 打开IRQ */
b main /* 跳转到C语言main函数 */
IRQ外设中断服务函数
当一个外设触发中断(比如按键按下后)会执行IRQ_Handler
函数。
- 中断发生是首先保护现场(lr, r0-r12寄存器, 保存spsr寄存器数据)
- 读取GIC寄存器组的起始地址
- 通过对GIC寄存器组基地址偏移得到CPU Interface寄存器组
- 通过对CPU Interface基地址进行偏移得到GICC_IAR寄存器,它保存了触发中断的CPU号与中断号
- 读取中断号(目前只有一个CPU内核,可以不管CPU号)放入r0寄存器,调用对应的C语言函数执行中断
- 在执行中断前,首先需要将模式切换到SVC,这样在执行中断的时候可以允许新的IRQ中断触发
- 执行C语言的中断逻辑后切换到IRQ模式,继续完成中断收尾工作
- 恢复spsr寄存器数据
- 恢复中断执行前的现场(lr, r0-r12)
- 将lr地址减4字节再给到pc寄存器,恢复中断前的执行指令
/*
中断发生时, IRQ模式下的lr(LR_svc物理)寄存器保存中断时刻的PC寄存器
通过使用push命令将lr的值压入栈,这样的目的是为了在执行完当前中断服务函数
后可以顺利的返回到中断前的执行位置,因为在执行中断服务函数的时候lr里面的值可能发生变化
比如: 在内部使用了blx调用其它函数,新的IRQ中断进入
*/
push {lr}
/*
保存中断发生时的执行现场(r0-r12)
从User/Sys模式切换到IRQ模式,r0-r12寄存器是通用的,所以需要将这些寄存器都压入栈保存起来,
由于在执行IRQ中断函数时模式已经切换,此时的sp指针已经是IRQ模式下的栈地址了,所以r0-r12保存到了
IRQ对应的栈空间中,恢复现场的时候只需要入栈即可
*/
push {r0-r12}
// push {r0-r3, r12}
/*
将spsr(SPSR_irq)寄存器的值压入栈,spsr保存了中断发生时的cpsr寄存器的值,
中断执行完成之后需要恢复
*/
mrs r0, spsr
push {r0}
/* GIC寄存器组的基地址(起始地址,通过起始地址可以访问得所有的GIC寄存器) */
/* 将GIC基地址读取到r1寄存器中 */
MRC p15, 4, r1, c15, c0, 0 // Read Configuration Base Address Register
/* 0x2000 - 0x3FFF 是GIC中CPU Interface的范围 */
/* 将r1中保存的GIC基地址偏移0x2000再保存到r1中 */
/* 此时r1中保存的是CPU Interface的基地址 */
add r1, r1, #0x2000
/*
r1(CPU Interface基地址)偏移0x0c得到GICC_IAR寄存器地址,
将GICC_IAR寄存器的值读取到r0中,
GICC_IAR保存了IRQ中断的中断号与CPU号(多核时使用),
通过中断号即可明确具体的中断来源并对中断进行响应
*/
ldr r0, [r1, #0x0c]
/*
由于要进入到SVC模式了,需要将r0, r1两个通用寄存器的数据保存到栈里,
防止在SVC模式下后r0,r1数据丢失
此时r0, r1保存到的是IRQ模式下的栈空间,
*/
push {r0, r1}
/*
将CPSR寄存器的M[4:0]值改成10011, 让CPU进入到SVC模式,
进行SVC模式之后,当我们处理当前中断时,
系统可以再次响应IRQ中断
*/
cps #0x13 // 进入到SVC
/*
进入到svc模式后先将lr的数据压入栈,执行完后再恢复
因为接下来要使用blx调用C语言函数,会改变lr寄存器的数据
*/
push {lr}
ldr r2, =system_irq_handler // 将C语言写的中断服务函数的地址加载到r2寄存器
blx r2 // 调用C语言的中断处理函数, r0为函数参数
pop {lr} // 调用完具体中断处理函数后,lr恢复
cps #0x12 // 进入到IRQ,执行完中断服务函数后进入IRQ不能再次响应IRQ中断,直到当前的IRQ中断完成
pop {r0, r1} // 恢复IRQ模式下r0,r1寄存器的数据
/*
此时r0保存的是GICC_IAR寄存器的数据,
将GICC_IAR的数据写入到GICC_EOIR寄存器,表示当前IRQ中断处理完成
*/
str r0, [r1, #0x10]
pop {r0} // 将栈顶的数据(此时栈顶保存的是spsr寄存器的值)出栈到r0寄存器
/// spsr_cxsf其中(cxsf表示4个不同的8bit位数据,后续表示此次命令影响的数据位), spsr_cxsf等于spsr
msr spsr_cxsf, r0 // 恢复spsr寄存器数据
pop {r0-r12} // 恢复r0-r12寄存器的数据
pop {lr} // 恢复lr寄存器的数据
subs pc, lr, #4 // 将lr - 4字节赋值给lc, 恢复中断前的执行命令继续执行
IRQ中断服务通用逻辑处理函数
我们需要编写一个通用的中断处理函数,从参数(r0寄存器中的GICC_IAR寄存器的数据)中提取中断号,根据对应的中断号再调用注册进来的具体的中断函数,比如: 按键中断函数
void system_irq_handler(unsigned int gicciar)
{
uint32_t irqNum = gicciar & 0x3FF;
if (irqNum >= NUMBER_OF_INT_VECTORS)
return;
Interrupt_Irq_Count++;
Interrupt_Irq_Data iid = _irqInterruptTables[irqNum];
iid.handler(irqNum, iid.context);
Interrupt_Irq_Count--;
}
外设中断驱动
- GPIO复用以及配置电气属性
- 配置GPIO的输入与输出
- 初始化GPIO中断
void GPIO_Init_Interrupt(GPIO_Type *base, int pin, GPIO_INTERRUPT_MODE mode)
{
/// 首先将GPIO的edge_sel寄存器对应pin位清0,如果为1则会使ICR寄存器的配置无效
base->EDGE_SEL &= ~(1 << pin);
/// 对应ICR的索引(按2位为一个单元)
int icrOffset = pin;
/// 具体的icr寄存器地址
__IO uint32_t *p_icr = NULL;
if (pin < 16)
{
p_icr = &(base->ICR1);
}
else
{
p_icr = &(base->ICR2);
icrOffset -= 16;
}
switch (mode)
{
case GPIO_INTERRUPT_MODE_NO_INTERRUPT:
break;
case GPIO_INTERRUPT_MODE_LOW:
*p_icr &= ~(3 << (2 * icrOffset));
break;
case GPIO_INTERRUPT_MODE_HIGH:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 1 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_RISING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 2 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_FALLING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 3 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_RISING_AND_RALLING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
base->EDGE_SEL |= (1 << pin);
break;
}
}
- I.MX6ULL的GIC使能对应中断ID的中断
/// 使用GPIO1的IO18对应的IRQ中断ID(GPIO1_Combined_16_31_IRQn)
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
- 注册对应中断ID的中断服务处理函数
/// 注册对应IRQ中断号的中断服务函数
Interrupt_Irq_Handler_Register(GPIO1_Combined_16_31_IRQn, (Interrupt_Irq_Handler)Key0_Interrupt_Irq_Handler, NULL);
/// 使用GPIO01_IO18中断
- GPIO使能中断
/// 使用GPIO01_IO18中断
GPIO_Enable_Interrupt(GPIO1, 18);
- 在中断服务处理函数中处理中断
void Key0_Interrupt_Irq_Handler(unsigned int gicciar, void *context)
{
/// 中断服务函数要求快进快出,这里没有定时器
/// 为了处理抖动暂时使用Delay来解决
/// 以后使用定时器来处理
Delay(10);
if (GPIO_ReadValue(GPIO1, 18) == 0)
{
Beep_On();
Led_On();
Delay(350);
Beep_Off();
Led_Off();
}
/// 中断处理完成后,清楚中断标志位
GPIO_Clean_Interrupt_Flag(GPIO1, 18);
}
发布评论请先 登录
相关推荐
评论