中断流程
保存现场-执行中断服务函数-返回现场
.S文件的修改
先看代码:
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 */
以下分析代码。
前面几行都是在保存寄存器,例如lr、r0这些。为什么不保存所有的寄存器是因为进入IRQ模式后,系统会自动保存一部分寄存器,而那些没有自动保存的就需要手动保存了。
然后是第一个需要仔细理解的:
mrc p15, 4, r1, c15, c0, 0
mrc是从CP15里读寄存器c15寄存器中的CBAR寄存器,存储到r1里。
CBAR寄存器是保存GIC(Generic Interrupt Controller)寄存器组的物理地址(首地址)。
GIC分发器
GIC寄存器组偏移0x1000~0x1FFF为GIC的分发器(Distributer)。分发器复杂处理各个中断事件的分发,也就是中断事件应该发送到哪个CPU接口上去。分发器收集所有的中断源,可以控制中断优先级,它总是将优先级最高的中断时间紧发送到CPU接口端。
GIC CPU接口
GIC寄存器组偏移0x2000~0x3FFF为GIC的CPU接口端。CPU接口端负责将分发器与CPU Core连接。
到这里,我们就可以访问GIC控制器了。类似STM32里的NVIC。
继续看代码:
add r1, r1, #0X2000
ldr r0, [r1, #0XC]
第一句是将r1的值偏移了0x2000,注意此时r1寄存器里面是GIC寄存器组的基地址,偏移之后就是CPU接口的基地址了。
第二句是将r1的值+0xC后写到r0寄存器里。注意此时r1寄存器里的值是CPU接口的基地址,+0xC之后得到的就是GICC_IAR寄存器的地址。那么r0寄存器里面保存的就是GICC_IAR寄存器的值。
GICC_IAR寄存器
CPU会读取这个寄存器的值,得到中断对应的ID。这次读取也意味着中断被感知到。
GICC_IAR[9:0]: 中断ID
GICC_IAR[12:10]: CPU ID
有了中断ID之后,才可以知道具体触发了哪个中断,进而执行这个ID对应的中断处理函数。
之后通过push将r0(中断ID) 和r1(CPU接口首地址)保存。
然后,通过cps #0x13
再次进入SVC模式。因为触发了IRQ后,CPU会工作在IRQ模式下。由于我们需要执行system_irqhandler
函数,因此需要先进入SVC模式,然后保存SVC模式下的lr寄存器,然后将system_irqhandler
放到r2寄存器里。之后执行blx r2
,跳转到r2里面,执行system_irqhandler
。注意,system_irqhandler
是具体的中断处理函数,此函数有一个参数,为GICC_IAR寄存的值。
执行完毕后pop {lr}
lr寄存器出栈。然后cps #0x12
再次进入IRQ模式。
GICC_EOIR寄存器
之后的代码仔细分析一下:
pop {r0, r1} str r0, [r1, #0X10]
首先将r0、r1寄存器出栈,此时r0保存的是GICC_IAR寄存器的值,r1保存的是GIC寄存器组中CPU接口的基地址。
然后将r0的值写入到GIC基地址+0X10的位置去。
GIC+0x10是GICC_EOIR寄存器。意思是中断结束寄存器。所以这两句代码的意思是将GIC_IAR寄存器的值写到GIC_EOIR寄存器里。
A7的架构决定了说任何一个CPU核在读取到有效的中断ID值(GICC_IAR)后,都必须将这个值写到GICC_EOIR中去。
GICC_EOIR寄存器的功能: 被写入时,标识一个中断处理的完成。
之后就是出栈了。不易理解的是:
subs pc, lr, #4
这里是将lr-4之后赋值给pc。
lr: link register 保存中断点
pc: program counter 当前运行的代码地址。
那么为什么要lr-4之后赋值给PC呢? 这里和CPU架构有关。ARM芯片是Fetch-Decode-Execute 这种三级流水线的架构。而PC的值是当前执行的指令地址+8.例如:
0X2000 MOV R1, R0 //EXECUTE
0X2004 MOV R2, R3 //DECODE
0X2008 MOV R4, R5 //FETCH
假设CPU已经在执行0X2000处的代码,那么此时PC里面已经保存了0X2008地址处的指令。假设此时发生中断,则lr寄存器里保存的地址就是pc的值,也就是0x2008。中断执行完成后,如果直接跳转到lr保存的地址处,就会出现位于0X2004处的指令没有执行。这样是不对的。因此,需要将lr-4的值赋予pc,此时pc = 0x2004,继续执行。