ARM的中断处理
这里想通过RT-Thread在qemu上模拟cortex-a9处理来理清楚arm中断处理的基本的流程。并且想通过qemu来看下底层GIC-V2的使用以及在中断处理的软硬件交互。
-
介绍中断初始化包括中断向量表的配置、GIC的初始化
-
通过qemu+GDB跟踪timer中断,并以timer中断为例,梳理arm中断的基本流程
本文使用RT-Thread主要因为其代码简单,不会像linux内核过于复杂,流程简洁清晰,便于梳理中断的处理流程。当然使用linux内核进行分析也是可以的。
1、 中断向量表初始化
1.1 ARM的寄存器以及模式
ARM一共有7中模式:System、User、RIQ、Supervisor、Abort、IRQ和Undefined。每种模式都会使用自己的寄存器。在模式切换时需要去保存模式切换前的寄存器。
| User | System | Supervisor | Abort | Undefined | IRQ | FIQ |
|---|---|---|---|---|---|---|
| R0 | R0 | R0 | R0 | R0 | R0 | R0 |
| R1 | R1 | R1 | R1 | R1 | R1 | R1 |
| R2 | R2 | R2 | R2 | R2 | R2 | R2 |
| R3 | R3 | R3 | R3 | R3 | R3 | R3 |
| R4 | R4 | R4 | R4 | R4 | R4 | R4 |
| R5 | R5 | R5 | R5 | R5 | R5 | R5 |
| R6 | R6 | R6 | R6 | R6 | R6 | R6 |
| R7 | R7 | R7 | R7 | R7 | R7 | R7 |
| R8 | R8 | R8 | R8 | R8 | R8 | R8_fiq |
| R9 | R9 | R9 | R9 | R9 | R9 | R9_fiq |
| R10 | R10 | R10 | R10 | R10 | R10 | R10_fiq |
| R11 | R11 | R11 | R11 | R11 | R11 | R11_fiq |
| R12 | R12 | R12 | R12 | R12 | R12 | R12_fiq |
| R13 | R13 | R13_svc | R13_abt | R13_und | R13_irq | R13_fiq |
| R14 | R14 | R14_svc | R14_abt | R14_und | R14_irq | R14_fiq |
| PC | PC | PC | PC | PC | PC | PC |
| CPSR | CPSR | CPSR | CPSR | CPSR | CPSR | CPSR |
| SPSR_svc | SPSR_abt | SPSR_und | SPSR_irq | SPSR_fiq |
其中没有带下角标的(上表中的黑体标识的寄存器名称)都是共用的,无论切换到哪个模式,这些寄存器都是共用的。ARM的7种模式一共有37个寄存器。所以在进行模式切换的时候如果有使用一些非公用的寄存器,则需要先保存原来的寄存器,避免寄存器数据被破坏。寄存器的保存在下个章节,处理中断时会看到保存寄存器的代码流程。
1.2 中断向量表的初始化
在cpu复位,走初始化流程流程,会对中断相关进行初始化:
/* initialize vector table */
rt_hw_vector_init(void rt_hw_interrupt_init(void)
{
rt_uint32_t gic_cpu_base;
rt_uint32_t gic_dist_base;
rt_uint32_t gic_irq_start;
/* initialize vector table */
rt_hw_vector_init(););
....
}
rt_hw_vector_init函数是初始化中断向量表。初始化中断向量表实质上就是把system_vectors地址写入到C15协处理器上。system_vectors声明在interrupt.c文件中,是个int类型的全局变量。而实际上system_vectors是保存了中断向量表的地址,在vector_gcc.S文件中:
.globl system_vectors
system_vectors:
ldr pc, _vector_reset
ldr pc, _vector_undef
ldr pc, _vector_swi
ldr pc, _vector_pabt
ldr pc, _vector_dabt
ldr pc, _vector_resv
ldr pc, _vector_irq
ldr pc, _vector_fiq
在编译链接后,system_vectors就是程序编译后的地址。所以可以看到每种异常实际上在异常向量表中占4个字节,这4个字节就是后面的跳转程序,跳转到对应的处理函数上。
.globl _reset
.globl vector_undef
.globl vector_swi
.globl vector_pabt
.globl vector_dabt
.globl vector_resv
.globl vector_irq
.globl vector_fiq
_vector_reset:
.word _reset
_vector_undef:
.word vector_undef
_vector_swi:
.word vector_swi
_vector_pabt:
.word vector_pabt
_vector_dabt:
.word vector_dabt
_vector_resv:
.word vector_resv
_vector_irq:
.word vector_irq
_vector_fiq:
.word vector_fiq
而每个地址都是word类型的,用来存放中断处理函数的地址,所有类型的中断处理函数的定义都在start_gcc.S文件中。后续会以vector_irq为例进行分析。
2 中断处理流程分析
中断处理过程如下:
(1)CPU 面对中断会自动做一些事情,例如,把当前的 PC 值保存到 ELR 中,把 PSTATE寄存器的值保存到 SPSR 中,然后跳转到异常向量表里面。
(2)在异常向量表里,CPU 会跳转到对应的汇编处理函数。对于 IRQ,若中断发生在内核态,则跳转到 el1_irq 汇编函数;若中断发生在用户态,则跳转到 el0_irq 汇编函数。
(3)在上述汇编函数里保存中断现场。
(4)跳转到中断处理函数。例如,在GIC 驱动里读取中断号,根据中断号跳转到设备中断处理程序。
(5)在设备中断处理程序里,处理这个中断。
(6)返回 el1_irq 或者 el0_irq 汇编函数,恢复中断上下文。
(7)调用 ERET 指令来完成中断返回。CPU 会把 ELR 的值恢复到 PC 寄存器,把 SPSR 的值恢复到 PSTATE 寄存器。
(8)CPU 继续执行中断现场的下一条指令
上面的黑体字部分是中断处理流程的重点,这部分黑体字是能够通过qemu调试跟踪这部分的处理的。
2.1 qemu跟踪中断处理
2.2.1 跳转异常向量表
在vector_gcc.S的28行设下断点:b vector_gcc.S:28
系统的定时器会产生中断,触发断点。也就说明了步骤(1)提到的:在cpu保存完相关的信息后,会根据异常的类型,跳转到对应的异常向量表的,并执行。这里定时器产生的是普通的IRQ,所以会跳转到:ldr pc, _vector_fiq执行。
2.2.2 跳转到中断处理函数
ldr pc, _vector_fiq实际上是跳转指令,会跳转到_vector_fiq所对应的函数地址,最后跳转到(start_gcc.S文件331行):
.globl vector_irq
vector_irq:
当中断产生后,CPU会将自动模式切换到IRQ mode,而内核是运行在SVC mode下的。
vector_irq:
stmfd sp!, {r0, r1}
cps #Mode_SVC
mov r0, sp /* svc_sp */
mov r1, lr /* svc_lr */
cps #Mode_IRQ
sub lr, #4
stmfd r0!, {r1, lr} /* svc_lr, svc_pc */
stmfd r0!, {r2 - r12}
ldmfd sp!, {r1, r2} /* original r0, r1 */
stmfd r0!, {r1 - r2}
mrs r1, spsr /* original mode */
stmfd r0!, {r1}
Thread 1 hit Breakpoint 3, vector_irq () at /home/xcm/shared/rt-thread/libcpu/arm/cortex-a/start_gcc.S:333
333 stmfd sp!, {r0, r1}
(gdb) p /x $cpsr
$26 = 0x20000192
通过GDB在执行vector_irq前,当前已经处于IRQ mode了。由于中断处理函数需要运行在SVC mode下,所以这里会切换到SVC mode下,获取当前的lr和sp,然后切换到IRQ mode将IRQ的所有寄存器全部都保存到栈中。
2.2.2 中断处理函数
保存完IRQ mode下的寄存器后,会跳转到真正的中断处理函数下:
bl rt_interrupt_enter
bl rt_hw_trap_irq
bl rt_interrupt_leave
rt_hw_trap_ir函数就是真正的处理函数。
void rt_hw_trap_irq(void)
{
void *param;
int ir, ir_real;
rt_isr_handler_t isr_func;
extern struct rt_irq_desc isr_table[];
ir = rt_hw_interrupt_get_irq(); // 读取interrupt id
ir_real = ir & 0x3ff;
if (ir == 1023)
{
/* Spurious interrupt */
return;
}
/* get interrupt service routine */
isr_func = isr_table[ir_real].handler; // 获取interrupt id所对应的处理函数
if (isr_func)
{
/* Interrupt for myself. */
param = isr_table[ir_real].param;
/* turn to interrupt service routine */
isr_func(ir, param); // 执行中断处理函数
}
/* end of interrupt */
rt_hw_interrupt_ack(ir); // 处理完后,写GICC_EOIR,表示当前中断处理完成
}
-
rt_hw_interrupt_get_irq读取GIC的GICC_IAR寄存器,读取GICC_IAR寄存器是被看做是对中断的响应,GIC会把响应的中断状态切换到active状态。 -
rt_hw_interrupt_ack写GIC的GICC_EOIR寄存器,表示当前中断处理完成。对于GIC的交互在后续的文章中会详细阐述。
2.2.3 中断返回
bl rt_scheduler_do_irq_switch
b rt_hw_context_switch_exit
.global rt_hw_context_switch_exit
rt_hw_context_switch_exit:
mov r0, sp
cps #Mode_IRQ
bl rt_signal_check
cps #Mode_SVC
mov sp, r0
ldmfd sp!, {r1}
msr spsr_cxsf, r1 /* original mode */
ldmfd sp!, {r0-r12,lr,pc}^ /* irq return */
msr spsr_cxsf, r1会切换到原来的modeldmfd sp!, {r0-r12,lr,pc}^会恢复原来的寄存器数据b rt_hw_context_switch_exit中断返回,恢复原来mode的寄存器数据,并跳转到原来的被中断的程序处执行。

浙公网安备 33010602011771号