IRQ 和前面的Reset 函数不大一样,当一个IRQ中断产生时,我们也不知道这个IRQ中断来自哪个外设,因此,需要先获取到中断ID,随后才会跳转到真正的中断服务函数执行处理逻辑。
整个 IRQ 中断处理可以看做是包含了两个部分:
- 汇编部分(环境准备):获取中断 ID,跳转中断处理函数
- C 语言部分:执行中断逻辑处理
目录
一、IRQ 环境准备
二、环境准备汇编实现
1、保存现场
2、获取中断ID
3、切换 SVC 模式
4、跳转通用中断服务函数
5、 恢复现场
三、完整写法
一、IRQ 环境准备
中断处理的整体思路是,先保存现场,切换到指定模式,随后再跳转中断处理函数,执行完处理逻辑以后再恢复到原状态继续向下执行。
这里要做的工作也是类似,唯独可能会略有不同的就是新增了和中断处理逻辑相关的内容:
- 保存现场(保存 LR、CPSR寄存器以及其他寄存器)
- 获取中断ID
- 切换 SVC 模式
- 跳转中断处理函数
- 恢复现场(恢复SPSR、回到中断发生前的下一个位置)
二、环境准备汇编实现
1、保存现场
因为即将跳转到中断服务函数,我们需要保存如下内容:
- 上下文内容:下面要用r0-r3 寄存器的内容,
- 下一条指令的地址:当中断发生时,LR 寄存器会自动保存下一条指令的地址
- SPSR 寄存器:用于保存CPSR寄存器的状态,以便于中断处理完毕后恢复到原状态
push {lr} /* 保存 lr 地址 */
push {r0-r3, r12} /* 保存 r0-r3,r12 寄存器 */
mrs r0, spsr /* 读取 spsr 寄存器 */
push {r0} /* 保存 spsr 寄存器 */
2、获取中断ID
(1) 获取寄存器地址
中断控制器 GIC 下包含了一系列寄存器(寄存器组),如记录寄存器状态、记录寄存器中断ID的寄存器。我们只要获得了这个寄存器组的首地址,后续就可以根据偏移拿到每一个寄存器的地址。
如下便是 CBAR寄存器,其中的 PERIPHBASE 便记录了 GIC 控制器的首地址。我们可以通过如下命令获得 GIC 的寄存器组首地址。现将其保存到 r1 寄存器。
mrc p15, 4, r1, c15, c0, 0 /* 将CP15协处理器的C0内的值保存到 r1 寄存器中 */
现在拿到首地址以后,还需要拿到偏移定位到指定寄存器。我们要关注的是 CPU Interface 这个寄存器组。我们先拿到 CPU Interface 这个寄存器组的首地址,相对于GIC 的首地址是 0x2000
add r1, r1, #0x2000 /* r1中本就保存了 GIC 基地址,现在需要加上偏移 0x2000 */
/* 于是就得到了CPU Interface 寄存器组的基地址 */
我们看到 GICC_IAR 寄存器,这个寄存器中保存了中断ID,这个寄存器相对于CPU Interface 寄存器组的偏移量为 0x000C
add r1, r1, #0x000C
(2) 获取中断ID
下面是 GICC_IAR 的字段分布,我们只需要获取到其中的 9-0 bit 即可。
- 先将 r1 寄存器指向的地址内容加载到 r0 寄存器
- 然后 r0 寄存器取出低10位
str r1, [r0] /* 将 r1 寄存器指向的内容保存到 r0 */
and r0, r0, #0x3ff /* 取出低10位 */
(3) 保存中断 ID
后续会跳转到中断服务函数,我们需要给函数传参,中断服务函数在调用的时候默认从r0-r3 寄存器中获取参数内容。第一个参数从 r0 获取,第二个参数从r1 获取,... 以此类推。
调用C函数传递参数时,参数个数小于 4 个,可以使用 r0 - r3 传递;如果大于 4 个,需要使用堆栈传递。
push {r0, r1} /* 保存 r0,r1 */
3、切换 SVC 模式
发生 IRQ 中断时,系统会自动进入 IRQ 模式,但是我们执行中断处理逻辑的时候,可能会产生其他中断,所以在处理中断逻辑的时候,我们可以先进入SVC模式,等处理完了再切换回 IRQ 模式。
下面我们使用 cps 命令直接切换到 SVC 模式
cps #0x13 /* 进入 SVC 模式,允许其他中断再次进去 */
4、跳转通用中断服务函数
万事具备,只等跳转。system_irqhandler 是我们在 C 语言中定义好的一个函数。
push {lr} /* 保存 SVC 模式的 lr 寄存器 */
ldr r2, =system_irqhandler /* 加载 C 语言中断处理函数到 r2 寄存器中*/
blx r2 /* 运行 C 语言中断处理函数,带有一个参数 */
pop {lr} /* 执行完 C 语言中断服务函数,lr 出栈 */
/*
* 参数 giccIar 是中断ID
*/
void system_irqhandler(unsigned int giccIar)
{
/* 中断处理逻辑 */
}
5、 恢复现场
剩下的就是一些出栈操作了。
- 切换到 IRQ 模式。原本切换到 SVC 模式下是为了在执行中断服务函数时,允许其他中断进入,现在中断服务函数执行完毕,需要切换回 IRQ 模式
- 每次从 GICC_IAR 中读取中断ID以后,需要将中断ID值写入到 GICC_EOIR 寄存器。
- 恢复 SPSR、r0 - r3、r12 寄存器
- subs pc, lr, #4
cps #0x12 /* 切换回 IRQ 模式 */
pop {r0, r1} /* 对应之前的 push {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 */
解析:subs pc, lr, #4
这里涉及到指令流水线的问题,指令执行的时候,译码器和PC计数器并不会闲着,而是先一步获取下一次要用到的指令。这就造成了PC 计数器中保存的指令地址 和 实际在执行的指令地址相差 8 个字节。
假设在执行指令1 的时候产生了一个中断,执行完手上的指令后会调转到中断向量表,然后去调用对应的中断服务函数,但是等到回来的时候,要执行 PC 指向的指令,所以我们发现这里直接跳过了指令2。
因此在中断服务函数执行完毕后,我们需要让 PC 寄存器的值减4,即回到上一条指令继续运行。
三、完整写法
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 寄存器中 */
add r1, r1, #0x2000 /* r1中本就保存了 GIC 基地址,现在需要加上偏移 0x2000 */
/* 于是就得到了CPU Interface 寄存器组的基地址 */
add r1, r1, #0x000C
str r1, [r0] /* 将 r1 寄存器指向的内容保存到 r0 */
and r0, r0, #0x3ff /* 取出低10位(获取中断ID) */
push {r0, r1} /* 保存 r0,r1 */
cps #0x13 /* 进入 SVC 模式,允许其他中断再次进去 */
push {lr} /* 保存 SVC 模式的 lr 寄存器 */
ldr r2, =system_irqhandler /* 加载 C 语言中断处理函数到 r2 寄存器中*/
blx r2 /* 运行 C 语言中断处理函数,带有一个参数 */
pop {lr} /* 执行完 C 语言中断服务函数,lr 出栈 */
cps #0x12 /* 切换回 IRQ 模式 */
pop {r0, r1} /* 对应之前的 push {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 */