ARM的中断处理

这里想通过RT-Thread在qemu上模拟cortex-a9处理来理清楚arm中断处理的基本的流程。并且想通过qemu来看下底层GIC-V2的使用以及在中断处理的软硬件交互。

  1. 介绍中断初始化包括中断向量表的配置、GIC的初始化

  2. 通过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,表示当前中断处理完成
}
  1. rt_hw_interrupt_get_irq读取GIC的GICC_IAR寄存器,读取GICC_IAR寄存器是被看做是对中断的响应,GIC会把响应的中断状态切换到active状态。

  2. 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 */
  1. msr spsr_cxsf, r1会切换到原来的mode
  2. ldmfd sp!, {r0-r12,lr,pc}^ 会恢复原来的寄存器数据
  3. b       rt_hw_context_switch_exit中断返回,恢复原来mode的寄存器数据,并跳转到原来的被中断的程序处执行。
posted @ 2024-07-14 23:37  cockpunctual  阅读(132)  评论(0)    收藏  举报