1、STM32中断向量表
ARM芯片从0X00000000开始运行,执行指令。在程序开始的地方存放着中断向量表。中断向量表主要功能是描述中断对应的中断服务函数。
对于STM32来说代码最开始的地址存放堆栈栈顶指针。
2、中断向量偏移
一般ARM从0X000000000地址开始运行,对于STM32我们设置连接首地址为0X8000000。
如果代码一定要从0X8000000开始运行,那么需要告诉一下soc内核。也就是设置中断向量偏移。设置SCB的VTOR寄存器为新的中断向量表起始地址即可。
3、NVIC中断控制器。
NVIC就是中断管理机构。使能和关闭指定的中断、设置中断优先级。
4、中断服务函数的编写
中断服务函数就是中断要做的事情。当中断发生以后中断服务函数就会被调 用,我们要处理的工作就可以放到中断服务函数中去完成。
同样以 STM32F103 的 PE2 为例, 其中断服务函数如下所示:
/* 外部中断 2 服务程序 */
void EXTI2_IRQHandler(void)
{
/* 中断处理代码 */
}
当 PE2 引脚的中断触发以后就会调用其对应的中断处理函数 EXTI2_IRQHandler,我们可 以在函数 EXTI2_IRQHandler 中添加中断处理代码。
1、Cortex-A中断向量表
Cortex-A中断向量表有8个中断,其中重点关注IRQ。Cortex-A的中断向量表需要用户自己去定义。
Cortex-A 内核 CPU 的所有外部中 断都属于这个 IQR 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服 务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出 相应的处理。这些外部中断和 IQR 中断的关系如下图所示:
、复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做 一些初始化工作,比如初始化 SP 指针、DDR 等等。
、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI 指 令来引起软中断,通过软中断来陷入到内核空间。
、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
、IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中 断的发生。
、FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中。
在上面的 7 个中断中,我们常用的就是复位中断和 IRQ 中断,所以我们需要编写这两个中断的中断服务函数
2、中断向量偏移
我们的裸机历程都是从0X87800000开始的,因此要设置中断向量偏移。
3、GIC中断控制器。
同NVIC一样,GIC用于管理Cortex-A的中断。GIC提供了开关中断,设置中断优先级。
4、IMX6U中断号
为了区分不同的中断,引入了中断号。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包 含了 PPI、SPI 和 SGI
ID0-ID15是给SGI,ID16-ID31是给PPI。剩下的ID32-1019给SPI,也就是按键中断、串口中断。。。。
6ULL支持128个中断。
4、中断服务函数的编写
一个是IRQ中断服务函数的编写,另一个就是在IRQ中断服务函数里面去查找并运行的具体的外设中断服务函数,
.global _start /* 全局标号 */
/*描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_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(快速中断)未定义中断 */
/* 复位中断 */
Reset_Handler:
cpsid i /* 关闭全局中断 */
/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
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寄存器中 */
#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000
dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif
/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入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 /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
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,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */
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 */
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
第 6 到 14 行是中断向量表
第 17 到 81 行是复位中断服务函数 Reset_Handler,
第 19 行先调用指令“cpsid i”关闭 IRQ,
第 24 到 30 行是关闭 I/D Cache、MMU、对齐检测和分支预测。
第 33 行到 42 行是汇编版本的 中断向量表重映射。
第 50 到 68 行是设置不同模式下的 sp 指针,分别设置 IRQ 模式、SYS 模 式和 SVC 模式的栈指针,每种模式的栈大小都是 2MB。第 70 行调用指令“cpsie i”重新打开 IRQ 中断,
第 72 到 79 行是操作 CPSR 寄存器来打开 IRQ 中断。当初始化工作都完成以后就可 以进入到 main 函数了,
第 81 行就是跳转到 main 函数。
第 110 到 144 行是中断服务函数 IRQ_Handler,这个是本章的重点,因为所有的外部中断 最终都会触发 IRQ 中断,所以 IRQ 中断服务函数主要的工作就是区分去当前发生的什么中断 (中断 ID)?然后针对不同的外部中断做出不同的处理。
第 111 到 115 行是保存现场,
第 117 到 122 行是获取当前中断号,中断号被保存到了 r0 寄存器中。
第 131 和 132 行才是中断处理的重点,这两行相当于调用了函数 system_irqhandler,函数 system_irqhandler 是一个 C 语言函数, 此函数有一个参数,这个参数中断号,所以我们需要传递一个参数。汇编中调用 C 函数如何实现参数传递呢?根据 ATPCS(ARM-Thumb Procedure Call Standard)定义的函数参数传递规则,在 汇编调用 C 函数的时候建议形参不要超过 4 个,形参可以由 r0~r3 这四个寄存器来传递,如果 形参大于 4 个,那么大于 4 个的部分要使用堆栈进行传递。所以给 r0 寄存器写入中断号就可以了函数 system_irqhandler 的参数传递,在 136 行已经向 r0 寄存器写入了中断号了。中断的真正 处理过程其实是在函数 system_irqhandler 中完成,稍后需要编写函数 system_irqhandler。 这里先注释掉,#是汇编的行注释。