文章目录
- IRQ中断服务函数流程解释
- 0. 基本流程步骤
- 1. 入口部分
- 2. 读取中断号
- 3. 切换模式并调用C语言处理函数
- 4. 清理和恢复环境
- 5. 完整代码
本文章结合了正点原子的 i.mx6u嵌入式Linux开发指南和笔者的理解。
IRQ中断服务函数流程解释
IRQ Interrupt Request 外部中断
0. 基本流程步骤
整个IRQ中断服务函数的流程可以简洁地概括为以下几个关键步骤:
-
保存环境:
- 保存当前任务的关键寄存器(r0-r3, r12, lr)和程序状态寄存器(SPSR)以保护执行环境。
-
获取中断源:
- 从Generic Interrupt Controller (GIC)读取当前触发的中断号,这是识别和处理特定中断的关键步骤。
-
模式切换:
- 切换到SVC(Supervisor Call)模式以允许中断嵌套,即在处理当前中断时允许其他中断的处理。
-
执行中断处理:
- 调用C语言编写的中断处理函数
system_irqhandler
,处理具体的中断逻辑。
- 调用C语言编写的中断处理函数
-
中断结束处理:
- 将处理完成的中断号写入GICC_EOIR寄存器,通知GIC中断已被处理。
-
恢复环境并退出:
- 恢复之前保存的各寄存器和程序状态,通过更新程序计数器完成从中断返回到主程序的流程。
这些步骤确保了系统在响应中断时能够保持稳定和可预测,同时保护了任务的执行状态不受中断影响。
1. 入口部分
- 保存寄存器状态:
这些步骤是中断服务例程的标准做法,用于保护执行环境,使得中断处理过程不会影响当前执行的任务。push {lr} /* 保存链接寄存器(lr)的值,即返回地址 */ push {r0-r3, r12} /* 保存通用寄存器r0到r3和r12 */
- R0-R3 用作传入函数参数,传出函数返回值。在子程序调用之间,可以将 r0-r3 用于任何用途。被调用函数在返回之前不必恢复 r0-r3。如果调用函数需要再次使用 r0-r3 的内容,则它必须保留这些内容。
- R4-R11 被用来存放函数的局部变量。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。
- R12 是内部调用暂时寄存器 ip。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复
r12。- R13 是栈指针 sp。它不能用于任何其它用途。sp 中存放的值在退出被调用函数时必须与进入时的值相同。
- R14 是链接寄存器 lr。如果您保存了返回地址,则可以在调用之间将 r14 用于其它用途,程序返回时要恢复
- R15 是程序计数器 PC。它不能用于任何其它用途。
- 注意:在中断程序中,所有的寄存器都必须保护,编译器会自动保护R4~R11
- 保存程序状态寄存器:
SPSR保存了中断发生时的处理器状态,需要在中断处理后恢复。mrs r0, spsr /* 读取保存程序状态寄存器(SPSR)到r0 */ push {r0} /* 将SPSR的值压栈保存 */
- CPSR: Current Program Status Register 当前程序状态寄存器
- SPSR: Saved Program Status Register 程序状态保存寄存器 当特定的异常中断发生时,SPSR用来保存当前程序状态寄存器(CPSR)的值,当异常退出以后,可以用 SPSR 中保存的值来恢复 CPSR。
2. 读取中断号
- 获取中断号:
这部分代码是从GIC(Generic Interrupt Controller)中获取当前触发的中断号,这是处理中断的关键步骤,用于确定具体的中断处理函数。mrc p15, 4, r1, c15, c0, 0 /* 从CP15协处理器中获取GIC基地址 */ add r1, r1, #0x2000 /* 计算GIC的CPU接口端基地址 */ ldr r0, [r1, #0xC] /* 从GICC_IAR寄存器读取当前的中断号 */ push {r0, r1} /* 保存中断号和GIC基地址 */
- c15寄存器
Cortex-A7 Technical ReferenceManua.pdf
P68
- mrc p15, 4, r1, c15, c0, 0 通过c15寄存器读取了CBAR寄存器
Cortex-A7 Technical ReferenceManua.pdf
P138
- 获取GIC块在基地址上的偏移大小
Cortex-A7 Technical ReferenceManua.pdf
P138
- 获取GICC_IAR寄存器的在CPU interface上的偏移地址(记录了中断号)为0x000C
Cortex-A7 Technical ReferenceManua.pdf
P188
3. 切换模式并调用C语言处理函数
- 切换到SVC模式以允许中断嵌套:
这允许在处理当前中断的过程中再次接受其他中断,实现中断嵌套。cps #0x13 /* 切换到SVC(Supervisor Call)模式 */ push {lr} /* 保存SVC模式的链接寄存器 */
- CPS 是 Change Processor State 的缩写 [6],它可以改变 CPSR 中的 A、I、F 中断屏蔽位以及 M 模式位,而不会改变其他 CPSR 位 [6]。
- cps #0x13 就是改变了M模式位(处理器模式控制位)
【正点原子】LMX6U嵌入式Linux驱动开发指南V1.81.pdf
P294
ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf
P1139
- 调用C语言中断处理函数:
这里,ldr r2, =system_irqhandler /* 将C语言编写的中断处理函数地址加载到r2 */ blx r2 /* 使用链接寄存器跳转到该函数执行 */
system_irqhandler
是实际处理中断的C语言函数,可以根据r0中的中断号执行相应的操作。
- blx 的全称是 Branch and Link with eXchange。它是一个 ARM 指令,用于跳转到一个新的地址并保存当前的 PC 值到 LR 寄存器。
- blx 指令可以改变处理器状态,从 ARM 状态切换到 Thumb 状态,或者从 Thumb 状态切换到 ARM 状态。这取决于目标地址的最低位。
- 如果目标地址的最低位为 0,则处理器状态将切换到 ARM 状态。如果目标地址的最低位为 1,则处理器状态将切换到 Thumb 状态。
4. 清理和恢复环境
- 恢复IRQ模式并完成中断处理:
这些步骤标志着中断处理的结束,通过向GICC_EOIR寄存器写入中断号来通知GIC中断已被处理。pop {lr} /* 恢复SVC模式的lr寄存器 */ cps #0x12 /* 切回IRQ模式 */ pop {r0, r1} /* 恢复r0, r1寄存器值 */ str r0, [r1, #0x10] /* 向GICC_EOIR寄存器写入中断结束信号 */
-
pop {lr} 指令用于从堆栈中取出(pop)一个值并将其存入到链接寄存器(Link Register, 简称 LR)中
-
为什么中断发生时,要进入SVC模式后进行中断函数的执行,执行完成后又进入IRQ模式
- 中断函数执行完后需要从 SVC 模式变回 IRQ 模式。 这是因为 SVC 模式用于处理系统调用,而 IRQ 模式用于处理外部中断。
- 中断发生时,处理器会从当前模式切换到 IRQ 模式,并执行相应的中断服务函数。
- 中断服务函数可能需要调用系统调用来完成某些操作。 例如读取设备寄存器或发送数据。
- 为了执行系统调用,处理器需要切换到 SVC 模式。
- 系统调用执行完毕后,处理器需要返回到 IRQ 模式,继续处理中断。
- 因此,中断函数执行完后需要从 SVC 模式变回 IRQ 模式,以确保中断处理流程的完整性。
-
写结束信号
- 获取GICC_EOIR寄存器的在CPU interface上的偏移地址(记录了中断号)为0x00010
Cortex-A7 Technical ReferenceManua.pdf
P189
- GICC_EOIR寄存器结构
ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf
P214
- 获取GICC_EOIR寄存器的在CPU interface上的偏移地址(记录了中断号)为0x00010
- 恢复处理器状态并退出中断:
这最后的步骤将处理器状态恢复到中断发生前的状态,并通过pop {r0} /* 恢复保存的SPSR */ msr spsr_cxsf, r0 /* 将SPSR的值恢复到处理器状态寄存器 */ pop {r0-r3, r12} /* 恢复通用寄存器 */ pop {lr} /* 恢复链接寄存器 */ subs pc, lr, #4 /* 通过链接寄存器设置程序计数器,完成中断返回 */
subs pc, lr, #4
指令安全地返回到中断点。
- 为什么这里要将lr-4 然后赋给 pc 呢?而不是直接将 lr 赋值给 pc?
核心原因: ARM 处理器采用流水线执行指令,当前执行的指令地址和下一条将要执行的指令地址并不一致。
具体解释:
- ARM 处理器流水线: ARM 处理器采用三级流水线,分别为取指、译指和执行。这意味着处理器会同时进行取指、译指和执行三个步骤,而不是顺序执行。
- PC 指针: PC 寄存器指向的是正在取值的指令地址,而不是正在执行的指令地址。
- 中断发生时: 当中断发生时,处理器会保存当前 PC 指针的值到 LR 寄存器。此时,LR 寄存器保存的是下一条将要执行的指令地址,而不是当前正在执行的指令地址。
- 中断返回: 中断处理完成后,需要返回到中断发生的位置继续执行。如果直接将 LR 赋值给 PC,那么处理器会跳过当前正在执行的指令,导致程序执行错误。
lr - 4
赋值给pc
: 为了确保程序正常执行,需要将 LR 寄存器的值减去 4,然后赋值给 PC 寄存器。这样,PC 指针就会指向中断发生时正在执行的指令的下一条指令,从而保证程序能够正常执行。
示例:
0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC
- 中断发生时,LR 寄存器保存的是 0X2008,因为 PC 指针指向的是下一条将要执行的指令地址。
- 如果直接将 LR 赋值给 PC,处理器会跳过 0X2004 处的指令
MOV R2, R3
,导致程序执行错误。 - 将
lr - 4
赋值给 PC,即PC = 0X2004
,处理器会从 0X2004 处的指令MOV R2, R3
开始执行,保证程序正常执行。
总结:
将 lr - 4
赋值给 pc
是为了解决 ARM 处理器流水线执行指令带来的问题,确保中断处理完成后能够正常返回到中断发生的位置继续执行程序。
5. 完整代码
@ IRQ中断服务函数
IRQ_Handler:
push {lr} /* 保存lr地址,Link Register, R14 寄存器*/
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 */