[IRQ] 01.QEMU ARM 异常 & 中断

发布于:2025-05-29 ⋅ 阅读:(23) ⋅ 点赞:(0)

目录

1.TCG 异常 & 中断捕获

1.1.异常捕获 - cpu_handle_exception()

1.2.中断捕获 - cpu_handle_interrupt()

2.ARM 中断相关基本概念

2.1.ARMv7 特权等级

2.2.ARMv8 异常等级

2.3..QEMU 异常/中断定义

2.4.ARMv8 架构的异常向量表

3.ARM-VEXPRESS 串口例程

3.1.CPU 添加中断 GPIO

3.1.1.创建中断输入 GPIO - qdev_init_gpio_in()

3.1.2.创建中断输出 - qdev_init_gpio_out()

3.2.Cortex-A 的中断控制器 - GIC

3.2.1.创建 GIC - init_cpus()

3.2.2.平台实例化 - a9mp_priv_realize()

3.2.3.中断控制器实例化 - arm_gic_realize()

3.2.4.中断入口 - gic_set_irq()

3.3.ARM PL011 串口地址

3.4.VEXPRESS 主板绑定串口

3.5.串口写入触发中断

3.6.GIC 中断处理流程

3.6.1.GIC 设置中断 - gic_set_irq()

3.6.2.将中断发送至 CPU - arm_cpu_set_irq()

3.7.CPU 处理中断

3.7.1.中断预处理 - arm_cpu_exec_interrupt()

3.7.2.中断处理 - arm_cpu_do_interrupt()

3.7.2.1.64 位架构的中断处理流程 - arm_cpu_do_interrupt_aarch64()

3.7.2.2.32 位架构的中断处理流程 - arm_cpu_do_interrupt_aarch32()

4.ARMv7 中断控制器 - NVIC

4.1.创建 NVIC

4.2.设置中断 - set_irq_level()

4.3.PL011 中断流程概览


1.TCG 异常 & 中断捕获

QEMU 模拟的 CPU 线程 cpu_exec_loop() 执行时,会检查异常和中断,如果存在异常或中断则进行处理:

// accel/tcg/cpu-exec.c
static int __attribute__((noinline))
cpu_exec_loop(CPUState *cpu, SyncClocks *sc)
{
    ...
    while (!cpu_handle_exception(cpu, &ret)) { // 异常捕获
        ...
        while (!cpu_handle_interrupt(cpu, &last_tb)) { // 中断捕获
        ...
            cpu_loop_exec_tb(cpu, tb, pc, &last_tb, &tb_exit);

1.1.异常捕获 - cpu_handle_exception()

CPUState 为异常和中断定义了对应的 Index

cpu_handle_exception() 检查 exception_index 的范围,当 exception_index 小于 EXCP_INTERRUPT 且不为 -1 时执行常规异常的处理流程:

// include/hw/core/cpu.h
struct CPUState {
    ...
    uint32_t interrupt_request;
    ...
    int32_t exception_index;
---------------------------------------------------------------

// accel/tcg/cpu-exec.c
static inline bool cpu_handle_exception(CPUState *cpu, int *ret)
{
    if (cpu->exception_index < 0) {
        ...
        return false;
    }
    
    if (cpu->exception_index >= EXCP_INTERRUPT) {
        ...
        return true;
    } else {
        ...
        if (replay_exception()) {
            CPUClass *cc = CPU_GET_CLASS(cpu);
            ...
            cc->tcg_ops->do_interrupt(cpu);
            ...
            cpu->exception_index = -1;

平台接收到中断或异常请求时,根据中断或异常的类型设置对应的 exception_index 标志,以 ARM 平台的中断预处理方法 arm_cpu_exec_interrupt() 为例:

// include/exec/cpu-common.h
#define EXCP_INTERRUPT  0x10000 /* async interruption */
#define EXCP_HLT        0x10001 /* hlt instruction reached */
#define EXCP_DEBUG      0x10002 /* cpu stopped after a breakpoint or singlestep */
#define EXCP_HALTED     0x10003 /* cpu is halted (waiting for external event) */
#define EXCP_YIELD      0x10004 /* cpu wants to yield timeslice to another */
#define EXCP_ATOMIC     0x10005 /* stop-the-world and emulate atomic */
------------------------------------------------------------------

// target/arm/cpu.c
static bool arm_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
{
    ...
     if (interrupt_request & CPU_INTERRUPT_HARD) {
        excp_idx = EXCP_IRQ;
        ...
        if (arm_excp_unmasked(cs, excp_idx, target_el,
                              cur_el, secure, hcr_el2)) {
            goto found;
    ...

found:
    cs->exception_index = excp_idx;
    ...

1.2.中断捕获 - cpu_handle_interrupt()

cpu_handle_interrupt() 检查 interrupt_request 标志位,若标志位被设置则调用 tcg_ops 中定义的中断函数进行处理

// accel/tcg/cpu-exec.c
static inline bool cpu_handle_interrupt(CPUState *cpu,
                                        TranslationBlock **last_tb)
{
    ...
    if (unlikely(qatomic_read(&cpu->interrupt_request))) { // 检查中断标志位
        ...
        interrupt_request = cpu->interrupt_request;
        ...
        else {
            CPUClass *cc = CPU_GET_CLASS(cpu);

            if (cc->tcg_ops->cpu_exec_interrupt &&
                cc->tcg_ops->cpu_exec_interrupt(cpu, interrupt_request)) { // 处理中断

平台通过自定义的方法设置 Interrupt_request 标志位,例如 ARM 平台对应的方法 arm_cpu_set_irq():

// include/exec/cpu-all.h
/* External hardware interrupt pending.
   This is typically used for interrupts from devices. */
#define CPU_INTERRUPT_HARD        0x0002
...
--------------------------------------------------------------

// target/arm/cpu.c
static void arm_cpu_set_irq(void *opaque, int irq, int level)
{
    static const int mask[] = {
        [ARM_CPU_IRQ] = CPU_INTERRUPT_HARD,
        ...

    case ARM_CPU_IRQ:
    case ARM_CPU_FIQ:
        ...
            cpu_interrupt(cs, mask[irq]);
--------------------------------------------------------------

// system/cpus.c
void cpu_interrupt(CPUState *cpu, int mask)
{
    ...
        generic_handle_interrupt(cpu, mask);
--------------------------------------------------------------

// system/cpus.c
static void generic_handle_interrupt(CPUState *cpu, int mask)
{
    cpu->interrupt_request |= mask; // mask --> CPU_INTERRUPT_HARD

2.ARM 中断相关基本概念

为了提升系统的安全性,现代操作系统一般通过用户态和内核态控制程序对硬件资源的访问,这两种状态根据处理器的特权等级 PL (Privilege Level) 或运行模式进行切换

2.1.ARMv7 特权等级

ARMv7 支持安全扩展和虚拟化扩展,处理器实现了安全扩展后,会区分出 Normal World 和 Secure World,这样可以将敏感资源和普通资源隔离,从而提升系统安全性,如下图所示:

处理器实现了虚拟化扩展后,会新增 Hypervisor Mode (Hyp),同时新增 PL2 特权模式,这样程序可以根据自身需求运行在对应的模式下:

  • 虚拟化扩展允许在 Normal World 运行多个操作系统;

  • Hypervisor 只能运行在 Normal World;

  • Trusted OS 和 Trusted Services 运行在 Secure World;

ARMv7 提供了 9 种 CPU 模式,如下图所示:

各模式的描述如下:

  • User:用户模式,用户程序运行在 User 模式,访问系统资源受限;

  • FIQ:快速中断/异常处理模式,发生 FIQ 中断时处理器的模式,相较于普通中断,快速中断拥有更高的响应等级和更低的延迟;

  • IRQ:中断/异常处理模式,发生 IRQ 中断时处理器的模式;

  • Supervisor(SVC):管理员模式,操作系统通常运行在该模式下,处理器复位或应用程序调用 SVC 指令时会进入该模式,系统调用就是通过 SVC 指令完成的;

  • Abort(ABT):异常终止模式,发生 Data Abort Exception 或 Prefetch Abort Exception 时进入该模式;

  • Undefined(UND):未定义指令模式,执行未定义指令时进入该模式;

  • System(SYS):系统模式,系统模式和用户模式共享寄存器视图,目前大多数系统未使用该模式,利用该特性可以在处理器启动时,通过设置系统模式的 SP 寄存器达到设置用户模式堆栈的目的,用户模式的其他寄存器也可以这样操作;

  • Monitor(MON):监视器模式,实现了安全扩展的处理器才有该模式,该模式下执行处理器 Secure 和 Non-Secure 状态的切换;

  • Hyp:实现了虚拟化扩展的处理器才有该模式;

处理器模式、特权等级和安全状态的关系如下图所示:

非安全状态下有 3 种特权等级 PL0 ~ PL2,其描述如下所示:

  • PL0:用户模式 User Mode 中运行的应用程序处于 PL0 特权等级,运行在该模式下的程序称为非特权程序,非特权程序对于系统资源的访问受限,对应于 Linux 的用户态;

  • PL1:除用户模式和 Hyp 模式,其他模式下运行的程序均为 PL1 特权等级,操作系统运行在 PL1 特权等级;

  • PL2:实现了虚拟化扩展后,Hyp 模式运行的系统管理程序处于 PL2 特权等级,系统管理程序控制多个操作系统在同一处理器上的共存和执行;

2.2.ARMv8 异常等级

ARM 平台将中断和异常统称为异常 Exception,区别于 ARMv7 架构中使用的特权等级,ARMv8 架构中定义了四个异常等级(Exception Level,EL),如下图所示:

  • EL0:非特权模式,应用程序;

  • EL1:特权模式,内核;

  • EL2:虚拟化监控程序,如 Hypervisor;

  • EL3:安全模式,如 Secure Monitor;

程序运行在四个异常等级中的一个,可以认为 ELn 对应着 PLn,各种软件(如应用程序、系统内核)一般只在某一个异常等级下运行,但虚拟机管理程序(如 KVM、VMWare)可以跨 EL1 和 EL2 工作(进入 EL2 管理虚拟机)

ARMv8 架构定义了 AArch32 和 AArch64 两种运行状态,AArch64 使用 64-bit 通用寄存器,AArch32 使用 32-bit 通用寄存器,ARMv8 中 AArch32 保留了 ARMv7 的特权等级,而在 AArch64 中特权等级被描述为异常等级(因此可以认为 ELn 对应 PLn)

异常等级的切换需要遵守以下规则:

  • 切换至更高的异常等级(例如从 EL0 到 EL1)表明增加了软件的执行权限;

  • 异常不能切换到更低的等级;

  • EL0 没有异常处理,异常只有在更高的异常等级(大于 EL0)中才会被处理;

  • 异常会改变程序的正常执行流程,异常处理程序(Exception Handler)在大于 EL0 的异常等级下开始执行;

  • 程序从异常返回可以保持相同的异常等级,或者变成较低的异常等级,但异常等级不能变大;

  • 除非从 EL3 返回到非安全模式,否则安全模式不会随着异常等级的更改而更改;

2.3..QEMU 异常/中断定义

中断一般分为外部中断和内部中断,外部中断一般由外设驱动程序产生,内部中断一般由程序异常导致

QEMU 将外部中断统称为硬件中断 HARD,内部中断细分为 EXITTB、HALT、DEBUG、RESET:

// include/exec/cpu-all.h
/* External hardware interrupt pending.  
   This is typically used for interrupts from devices.  */
#define CPU_INTERRUPT_HARD        0x0002

/* Exit the current TB.  This is typically used when some system-level device
   makes some change to the memory mapping.  E.g. the a20 line change.  */
#define CPU_INTERRUPT_EXITTB      0x0004

/* Halt the CPU.  */
#define CPU_INTERRUPT_HALT        0x0020

/* Debug event pending.  */
#define CPU_INTERRUPT_DEBUG       0x0080

/* Reset signal.  */
#define CPU_INTERRUPT_RESET       0x0400

除此以外,QEMU 还为目标平台定义了额外的中断源(外部中断源 & 内部中断源):

// include/exec/cpu-all.h
/* Several target-specific external hardware interrupts.  Each target/cpu.h
   should define proper names based on these defines.  */
#define CPU_INTERRUPT_TGT_EXT_0   0x0008
#define CPU_INTERRUPT_TGT_EXT_1   0x0010
#define CPU_INTERRUPT_TGT_EXT_2   0x0040
#define CPU_INTERRUPT_TGT_EXT_3   0x0200
#define CPU_INTERRUPT_TGT_EXT_4   0x1000


/* Several target-specific internal interrupts.  These differ from the
   preceding target-specific interrupts in that they are intended to
   originate from within the cpu itself, typically in response to some
   instruction being executed.  These, therefore, are not masked while
   single-stepping within the debugger.  */
#define CPU_INTERRUPT_TGT_INT_0   0x0100
#define CPU_INTERRUPT_TGT_INT_1   0x0800
#define CPU_INTERRUPT_TGT_INT_2   0x2000

使用额外的中断源需要对其重新定义,以 ARM Cortex-A 平台为例,其中断与异常统称为异常,共包含 5 种类型:

  • IRQ - Interrupt Request;

  • FIQ - Fast Interrupt Request;

  • VIRQ - Virtual Interrupt Request;

  • VFIQ - Virtual Interrupt Request;

  • VSERR - Virtual System Error;

// target/arm/cpu.h
/* ARM-specific interrupt pending bits.  */
#define CPU_INTERRUPT_FIQ   CPU_INTERRUPT_TGT_EXT_1
#define CPU_INTERRUPT_VIRQ  CPU_INTERRUPT_TGT_EXT_2
#define CPU_INTERRUPT_VFIQ  CPU_INTERRUPT_TGT_EXT_3
#define CPU_INTERRUPT_VSERR CPU_INTERRUPT_TGT_INT_0

其中,Cortex-A 平台的 IRQ 直接使用 QEMU 定义的宏 CPU_INTERRUPT_HARD

2.4.ARMv8 架构的异常向量表

ARMv8 异常向量表存储在寄存器 VBAR 中,32-bit 和 64-bit 模式分别有对应的寄存器,如下图所示:

其中,基地址为对应寄存器的地址,各种类型的异常有对应的偏移量,每个偏移量都指向了对应的异常处理函数:

用户程序中会定义对应的异常处理函数,该函数的地址与对应的异常地址关联,示例如下所示:

.balign 0x800            // 向量表2k(2048字节)大小对齐
Vector_table_el3:
curr_el_sp0_sync:        // synchronous处理程序
                         // 来自当前EL的异常,使用SP0
.balign 0x80
curr_el_sp0_irq:         // IRQ中断处理程序
                         // 来自当前EL的异常,使用SP0
.balign 0x80
curr_el_sp0_fiq:         // FIQ快速中断处理程序
                         // 来自当前EL的异常,使用SP0
.balign 0x80
curr_el_sp0_serror:      // Serror系统错误的处理程序
                         // 来自当前EL的异常,使用SP0

.balign 0x80
lower_el_aarch64_sync:   // synchronous处理程序
                         // 来自低EL且处于AArch64的异常
.balign 0x80
lower_el_aarch64_irq:    // IRQ中断处理程序
                         // 来自低EL且处于AArch64的异常
.balign 0x80
lower_el_aarch64_fiq:    // FIQ快速中断处理程序
                         // 来自低EL且处于AArch64的异常
.balign 0x80
lower_el_aarch64_serror: // Serror系统错误的处理程序
                         // 来自低EL且处于AArch64的异常

3.ARM-VEXPRESS 串口例程

以一次串口写入触发中断为例,完整梳理中断从产生到处理的过程

3.1.CPU 添加中断 GPIO

ARM 通过 arm_cpu_set_irq() 设置中断,CPU 实例化时通过 qdev_init_gpio_in() 将其与 IRQ 输入绑定,中断控制器调用 qemu_set_irq() 设置中断时会调用该函数将中断信息发送给 CPU

3.1.1.创建中断输入 GPIO - qdev_init_gpio_in()

qdev_init_gpio_in() 会创建默认名称为 sysbus-irq[x] (x = 0, 1, 2, ...) 的中断输入口

qemu_allocate_irq() 会将用户定义的中断处理函数和中断 IRQ 绑定

将对应的 IRQ 与该 GPIO 绑定后,若产生了该类型的 IRQ,则会直接通过该 GPIO 将中断传递给中断控制器,如 GIC、NVIC 等,CPU 与中断控制器之间传递中断信息的方式类似

// target/arm/cpu.c
static void arm_cpu_initfn(Object *obj)
{
    ...
    /* Our inbound IRQ and FIQ lines */
    ...
        qdev_init_gpio_in(DEVICE(cpu), arm_cpu_set_irq, 4);
------------------------------------------------------------

// hw/core/gpio.c
void qdev_init_gpio_in(DeviceState *dev, qemu_irq_handler handler, int n)
{
    qdev_init_gpio_in_named(dev, handler, NULL, n);
        |--> qdev_init_gpio_in_named_with_opaque(dev, handler, dev, name, n);
}
------------------------------------------------------------

// hw/core/gpio.c
void qdev_init_gpio_in_named_with_opaque(DeviceState *dev,
                                         qemu_irq_handler handler,
                                         void *opaque,
                                         const char *name, int n)
{
    ...
    NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, name);
    ...
    gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in,
                                        handler, opaque, n);
    ...
    for (i = gpio_list->num_in; i < gpio_list->num_in + n; i++) {
        gchar *propname = g_strdup_printf("%s[%u]", name, i);

        object_property_add_child(OBJECT(dev), propname, OBJECT(gpio_list->in[i]));
        ...
------------------------------------------------------------

// hw/core/irq.c
qemu_irq *qemu_extend_irqs(qemu_irq *old, int n_old, qemu_irq_handler handler,
                           void *opaque, int n)
{
    ...
    for (i = n_old; i < n + n_old; i++) {
        s[i] = qemu_allocate_irq(handler, opaque, i);
    ...
------------------------------------------------------------

// hw/core/irq.c
qemu_irq qemu_allocate_irq(qemu_irq_handler handler, void *opaque, int n)
{
    IRQState *irq;

    irq = IRQ(object_new(TYPE_IRQ));
    irq->handler = handler; // handler --> arm_cpu_set_irq()
    irq->opaque = opaque;
    irq->n = n;

    return irq;
}

3.1.2.创建中断输出 - qdev_init_gpio_out()

qdev_init_gpio_out() 为模块创建中断输出口

通过 qdev_connect_gpio_out() 可以将一个模块的中断输出口和另一个模块的中断输入口连接起来

例如,架构模块中包含了 CPU、中断控制器等基本模块,如果不希望外部设备直接访问中断控制器,则可以将中断控制器的 GPIO 创建为输出,架构模块的中断 GPIO 创建为输入,然后再通过 qdev_connect_gpio_out() 将两者连接起来,这样外部中断只能通过架构模块的中断 GPIO 间接的访问中断控制器

// target/arm/cpu.c
static void arm_cpu_initfn(Object *obj)
{
    ...
    /* Our inbound IRQ and FIQ lines */
    ...
    qdev_init_gpio_out(DEVICE(cpu), cpu->gt_timer_outputs,
                       ARRAY_SIZE(cpu->gt_timer_outputs));

    qdev_init_gpio_out_named(DEVICE(cpu), &cpu->gicv3_maintenance_interrupt,
                             "gicv3-maintenance-interrupt", 1);
    ...
--------------------------------------------------------------------

// hw/core/gpio.c
void qdev_init_gpio_out(DeviceState *dev, qemu_irq *pins, int n)
{
    qdev_init_gpio_out_named(dev, pins, NULL, n);
}
--------------------------------------------------------------------

// hw/core/gpio.c
void qdev_init_gpio_out_named(DeviceState *dev, qemu_irq *pins,
                              const char *name, int n)
{
    ...
    NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, name);
    ...
    for (i = 0; i < n; ++i) {
        ...
        object_property_add_link(OBJECT(dev), propname, TYPE_IRQ,
                                 (Object **)&pins[i],
                                 object_property_allow_set_link,
                                 OBJ_PROP_LINK_STRONG);

3.2.Cortex-A 的中断控制器 - GIC

ARM Cortex-A 平台的中断控制器 GIC (Generic Interrupt Controller) 在 QEMU 中使用 SysBusDevice 设备实现

以 VEXPRESS 平台为例,初始化过程中会实例化该设备,由于 Cortex-A9 和 Cortex-A15 的差异,该设备被再次封装在对应平台的私有设备类 PrivState 中,以 Cortex-A9 为例:

// include/hw/cpu/a9mpcore.h
#define TYPE_A9MPCORE_PRIV "a9mpcore_priv"
OBJECT_DECLARE_SIMPLE_TYPE(A9MPPrivState, A9MPCORE_PRIV)

struct A9MPPrivState {
    /*< private >*/
    SysBusDevice parent_obj;
    
    /*< public >*/
    ...
    GICState gic;

3.2.1.创建 GIC - init_cpus()

VEXPRESS 主板初始化时,根据 CPU 的差异调用对应方法实例化其中的 GIC 设备,同时连接 CPU 与 GIC

init_cpus() 创建平台对应的 privdev,其中包含了 GIC,然后调用其实例化方法并配置 GIC 的输入输出

// hw/arm/vexpress.c
static void vexpress_common_init(MachineState *machine)
{
    ...
    daughterboard->init(vms, machine->ram_size, machine->cpu_type, pic);
--------------------------------------------------------------

// hw/arm/vexpress.c
static void a9_daughterboard_init(VexpressMachineState *vms,
                                  ram_addr_t ram_size,
                                  const char *cpu_type,
                                  qemu_irq *pic)
{
    ...
    /* 0x1e000000 A9MPCore (SCU) private memory region */
    init_cpus(machine, cpu_type, TYPE_A9MPCORE_PRIV, 0x1e000000, pic,
              vms->secure, vms->virt);
--------------------------------------------------------------

// hw/arm/vexpress.c
static void init_cpus(MachineState *ms, const char *cpu_type,
                      const char *privdev, hwaddr periphbase,
                      qemu_irq *pic, bool secure, bool virt)
{
    ...
    /* Create the private peripheral devices (including the GIC); ... */
    dev = qdev_new(privdev);
    ...
    /* Connect the CPUs to the GIC */
    for (n = 0; n < smp_cpus; n++) {
        DeviceState *cpudev = DEVICE(qemu_get_cpu(n));

        sysbus_connect_irq(busdev, n, qdev_get_gpio_in(cpudev, ARM_CPU_IRQ));
        sysbus_connect_irq(busdev, n + smp_cpus, qdev_get_gpio_in(cpudev, ARM_CPU_FIQ));

3.2.2.平台实例化 - a9mp_priv_realize()

Cortex-A9 私有设备实例化时创建并实例化 GIC 设备

GIC 实例化时会创建 GPIO 内存并绑定中断设置函数,同时定义 iomem 内存的读写函数,其输入接口为 a9mp_priv_set_irq(),该方法实际调用 gic_set_irq()

// hw/cpu/a9mpcore.c
static void a9mp_priv_realize(DeviceState *dev, Error **errp)
{
    ...
    gicdev = DEVICE(&s->gic);
    ...
    gicbusdev = SYS_BUS_DEVICE(&s->gic);
    ...
    /* Pass through outbound IRQ lines from the GIC */
    sysbus_pass_irq(sbd, gicbusdev);

    /* Pass through inbound GPIO lines to the GIC */
    qdev_init_gpio_in(dev, a9mp_priv_set_irq, s->num_irq - 32);
-----------------------------------------------------------------

// hw/cpu/a9mpcore.c
static void a9mp_priv_set_irq(void *opaque, int irq, int level)
{
    A9MPPrivState *s = (A9MPPrivState *)opaque;

    // qdev_get_gpio_in(DEVICE(&s->gic) --> gic_set_irq()
    qemu_set_irq(qdev_get_gpio_in(DEVICE(&s->gic), irq), level);
}

3.2.3.中断控制器实例化 - arm_gic_realize()

GIC 实例化时创建 GPIO 内存并绑定中断设置函数,同时定义 iomem 内存读写函数,将 gic_set_irq() 作为 GIC 的 GPIO 子节点挂载,创建中断 GPIO 及 IO 内存:

// hw/intc/arm_gic.c
static const MemoryRegionOps gic_ops[2] = {
    {
        .read_with_attrs = gic_dist_read,
        .write_with_attrs = gic_dist_write,
-----------------------------------------------------------------

// hw/intc/arm_gic.c
static void arm_gic_realize(DeviceState *dev, Error **errp)
{
    ...
    gic_init_irqs_and_mmio(s, gic_set_irq, gic_ops, gic_virt_ops);
-----------------------------------------------------------------

// hw/intc/arm_gic_common.c
void gic_init_irqs_and_mmio(GICState *s, qemu_irq_handler handler,
                            const MemoryRegionOps *ops,
                            const MemoryRegionOps *virt_ops)
{
    ...
    qdev_init_gpio_in(DEVICE(s), handler, i); // handler --> gic_set_irq()

    for (i = 0; i < s->num_cpu; i++) {
        sysbus_init_irq(sbd, &s->parent_irq[i]);
    }
    ...
    /* Distributor */
    memory_region_init_io(&s->iomem, OBJECT(s), ops, s, "gic_dist", 0x1000);

3.2.4.中断入口 - gic_set_irq()

gic_set_irq() 调用 gic_update_internal() 查找 GICState 对应的中断输入口并设置中断,该方法执行中断预处理,并将其发送至对应的中断输入口 irq_lines = parent_irq,parent_irq 在平台初始化时通过 arm_gic_realize() 完成配置,irq_lines 对应 CPU 中断处理函数 arm_cpu_set_irq()

// hw/intc/arm_gic.c
/* Process a change in an external IRQ input.  */
static void gic_set_irq(void *opaque, int irq, int level)
{
    ...
    } else {
        gic_set_irq_generic(s, irq, level, cm, target);
            |--> s->irq_state[irq].level |= (cm)
            |--> s->irq_state[irq].pending |= (cm)
    ...
    gic_update(s);
        |--> gic_update_internal(s, false);
}
-----------------------------------------------------------------

// hw/intc/arm_gic.c
static inline void gic_update_internal(GICState *s, bool virt)
{
    ...
    qemu_irq *irq_lines = virt ? s->parent_virq : s->parent_irq;
    ...
        qemu_set_irq(irq_lines[cpu], irq_level);
            |--> arm_cpu_set_irq()

3.3.ARM PL011 串口地址

VEXPRESS 平台包含两个 UART 模块,其寄存器组的起始地址分别为 0x10009000 和 0x1000a000:

// hw/arm/vexpress.c
enum {
    VE_SYSREGS,
    ...
    VE_UART0,
    VE_UART1,
    ...
-------------------------------------------

// hw/arm/vexpress.c
static hwaddr motherboard_legacy_map[] = {
    ...
    [VE_UART0] = 0x10009000,
    [VE_UART1] = 0x1000a000,

3.4.VEXPRESS 主板绑定串口

VEXPRESS 开发板初始化时创建串口模块:

// hw/arm/vexpress.c
static void vexpress_class_init(ObjectClass *oc, void *data)
{
    ...
    mc->init = vexpress_common_init;
---------------------------------------------------------------

// hw/arm/vexpress.c
static void vexpress_common_init(MachineState *machine)
{
    ...
    qemu_irq pic[64];
    ...
    daughterboard->init(vms, machine->ram_size, machine->cpu_type, pic);
    ...
    // pic[5] --> a9mp_priv_set_irq()
    pl011_create(map[VE_UART0], pic[5], serial_hd(0));
    pl011_create(map[VE_UART1], pic[6], serial_hd(1));

这里会在主板上创建 PL011 模块,同时会将 PL011 的中断口和中断控制器相连

// include/hw/char/pl011.h
struct PL011State {
    SysBusDevice parent_obj;
    ...
    qemu_irq irq[6];
    ...
};
---------------------------------------------------------------

// hw/char/pl011.c
static void pl011_init(Object *obj)
{
    ...
    for (i = 0; i < ARRAY_SIZE(s->irq); i++) {
        sysbus_init_irq(sbd, &s->irq[i]);
    }
    ...
---------------------------------------------------------------

// hw/char/pl011.c
DeviceState *pl011_create(hwaddr addr, qemu_irq irq, Chardev *chr)
{
    DeviceState *dev;
    SysBusDevice *s;

    dev = qdev_new("pl011");
    s = SYS_BUS_DEVICE(dev);
    qdev_prop_set_chr(dev, "chardev", chr);
    sysbus_realize_and_unref(s, &error_fatal);
    sysbus_mmio_map(s, 0, addr);
    sysbus_connect_irq(s, 0, irq); // 连接中断控制器的中断输入口

    return dev;
}

3.5.串口写入触发中断

串口在写入完成后调用 pl011_update() 触发中断,进而通过 qemu_set_irq() 调用 IRQ 的 handler() 设置中断,该 handler() 在 CPU 实例化时配置:

// hw/char/pl011.c
static void pl011_write(void *opaque, hwaddr offset,
                        uint64_t value, unsigned size)
{
    ...
    switch (offset >> 2) {
    case 0: /* UARTDR */
        ...
        } else {
            ch = value;
            qemu_chr_fe_write_all(&s->chr, &ch, 1);
        }
        s->int_level |= INT_TX;
        pl011_update(s);
        break;
---------------------------------------------------------------

// hw/char/pl011.c
static void pl011_update(PL011State *s)
{
    ...
    flags = s->int_level & s->int_enabled;
    ...
    for (i = 0; i < ARRAY_SIZE(s->irq); i++) {
        qemu_set_irq(s->irq[i], (flags & irqmask[i]) != 0);
    }
}
---------------------------------------------------------------

// hw/core/irq.c
void qemu_set_irq(qemu_irq irq, int level)
{
    ...
    irq->handler(irq->opaque, irq->n, level);
        |--> a9mp_priv_set_irq()
            |--> gic_set_irq()

3.6.GIC 中断处理流程

ARM Cortex-A 的中断控制器 GIC 在接收到中断请求后,保存中断优先级与中断状态,然后将中断发送给 CPU

3.6.1.GIC 设置中断 - gic_set_irq()

保存中断优先级、中断状态等信息,然后调用 qemu_set_irq() 将中断信息发送给 CPU

// hw/intc/arm_gic.c
/* Process a change in an external IRQ input.  */
static void gic_set_irq(void *opaque, int irq, int level)
{
    ...
    GICState *s = (GICState *)opaque;
    ...
    } else {
        gic_set_irq_generic(s, irq, level, cm, target);
            |--> s->irq_state[irq].level |= (cm)
            |--> s->irq_state[irq].pending |= (cm)
    ...
    gic_update(s);
        |--> gic_update_internal(s, false);
}
---------------------------------------------------------------

// hw/intc/arm_gic.c
static inline void gic_update_internal(GICState *s, bool virt)
{
    ...
    qemu_irq *irq_lines = virt ? s->parent_virq : s->parent_irq;
    ...
        qemu_set_irq(irq_lines[cpu], irq_level);
            |--> arm_cpu_set_irq()

3.6.2.将中断发送至 CPU - arm_cpu_set_irq()

QEMU 虚拟的 CPU 本质是一个循环,在 cpu_exec_loop() 执行的过程中,会进行中断/异常请求检查,如果中断产生则会进行处理,arm_cpu_set_irq() 主要负责设置 CPU 的中断标志位:

// target/arm/cpu.c
static void arm_cpu_set_irq(void *opaque, int irq, int level)
{
    ...
    static const int mask[] = {
        [ARM_CPU_IRQ] = CPU_INTERRUPT_HARD,
        [ARM_CPU_FIQ] = CPU_INTERRUPT_FIQ,
        [ARM_CPU_VIRQ] = CPU_INTERRUPT_VIRQ,
        [ARM_CPU_VFIQ] = CPU_INTERRUPT_VFIQ
    };
    ...

    if (level) {
        env->irq_line_state |= mask[irq];
    ..

    switch (irq) {
    ...
    case ARM_CPU_IRQ:
    case ARM_CPU_FIQ:
        if (level) {
            cpu_interrupt(cs, mask[irq]);
                |--> cpu->interrupt_request |= mask;
        } else {
            cpu_reset_interrupt(cs, mask[irq]);
                |--> cpu->interrupt_request &= ~mask;

3.7.CPU 处理中断

ARM 平台在 TCGCPUOps 中定义中断/异常处理函数:

// target/arm/cpu.c
static const struct TCGCPUOps arm_tcg_ops = {
    ...
    .tlb_fill = arm_cpu_tlb_fill,
    .cpu_exec_interrupt = arm_cpu_exec_interrupt,
    .do_interrupt = arm_cpu_do_interrupt,
    ...
};

ARM 平台的异常(Exception,Excp)类型定义如下:

// target/arm/cpu.h
#define EXCP_UDEF            1   /* undefined instruction */
#define EXCP_SWI             2   /* software interrupt */
...
#define EXCP_IRQ             5
#define EXCP_FIQ             6
...
#define EXCP_VIRQ           14
#define EXCP_VFIQ           15
...
#define EXCP_VSERR          24
#define EXCP_GPC            25   /* v9 Granule Protection Check Fault */

arm_cpu_exec_interrupt() 对 IRQ 和 FIQ 使用 arm_phys_excp_target_el() 查表确定目标异常等级,其他异常的目标异常等级 target_el 为 1,之后,对于所有异常调用 arm_excp_unmasked() 进行中断路由,路由成功调用 arm_cpu_do_interrupt() 处理

3.7.1.中断预处理 - arm_cpu_exec_interrupt()

ARM 平台的中断预处理方法 arm_cpu_exec_interrupt() 会检查中断/异常等级,并检查是否可以执行中断,若可以执行则调用 tcg_ops 的 do_interrupt() 方法处理中断:

// target/arm/cpu.c
static bool arm_cpu_exec_interrupt(CPUState *cs, int interrupt_request)
{
    ...
    if (interrupt_request & CPU_INTERRUPT_HARD) {
        excp_idx = EXCP_IRQ;
        target_el = arm_phys_excp_target_el(cs, excp_idx, cur_el, secure);
        if (arm_excp_unmasked(cs, excp_idx, target_el, cur_el, secure, hcr_el2)) {
            goto found;
    ...
    if (interrupt_request & CPU_INTERRUPT_VIRQ) {
        excp_idx = EXCP_VIRQ;
        target_el = 1;
        if (arm_excp_unmasked(cs, excp_idx, target_el, cur_el, secure, hcr_el2)) {
            goto found;
        }
    }
    ...
found:
    cs->exception_index = excp_idx;
    env->exception.target_el = target_el;
    cc->tcg_ops->do_interrupt(cs);
    ...

3.7.2.中断处理 - arm_cpu_do_interrupt()

tcg_ops 的 do_interrupt() 方法由用户依据平台实现,该方法为 CPU 处理中断的具体操作,在该流程中保存上下文,进行用户栈/中断栈切换、PC 指针跳转等操作:

// target/arm/helper.c
void arm_cpu_do_interrupt(CPUState *cs)
{
    ...
    if (arm_el_is_aa64(env, new_el)) {
        arm_cpu_do_interrupt_aarch64(cs);
    } else {
        arm_cpu_do_interrupt_aarch32(cs);
    }

ARM 异常处理分为 32-bit 和 64-bit 架构,分别有对应的中断/异常处理函数

3.7.2.1.64 位架构的中断处理流程 - arm_cpu_do_interrupt_aarch64()

获取异常向量寄存器基地址,根据异常类型加上对应的偏移量得到最终的异常向量地址,该地址即为该类型异常对应的处理函数,将 PC 指针指向该地址从而执行异常处理函数

// target/arm/helper.c
static void arm_cpu_do_interrupt_aarch64(CPUState *cs)
{
    ...
    switch (cs->exception_index) {
        ...
    case EXCP_IRQ:
    case EXCP_VIRQ:
        addr += 0x80; // 获取中断处理函数地址
        break;
    ...
    if (is_a64(env)) {
        ...
        aarch64_save_sp(env, arm_current_el(env)); // 保存堆栈
            |--> env->sp_el[el] = env->xregs[31];
    ...
    env->pc = addr; // 执行中断处理函数
3.7.2.2.32 位架构的中断处理流程 - arm_cpu_do_interrupt_aarch32()

32-bit 架构的异常/中断处理流程与 64-bit 架构的相似,区别在于异常等级的处理和相关寄存器的差异

// target/arm/helper.c
static void arm_cpu_do_interrupt_aarch32(CPUState *cs)
{
    ...
    uint32_t addr;
    ...
    switch (cs->exception_index) {
    ...
    case EXCP_IRQ:
        new_mode = ARM_CPU_MODE_IRQ;
        addr = 0x18;
        ...
        break;
    ...
    take_aarch32_exception(env, new_mode, mask, offset, addr);
        |--> env->regs[15] = newpc; // newpc = addr

4.ARMv7 中断控制器 - NVIC

ARMv7 架构的中断控制器为 NVIC,其在结构上与 GIC 存在一定差别,但大致的中断处理逻辑一致

4.1.创建 NVIC

ARMv7 中断控制器 NVIC 在 armv7m_instance_init() 平台实例化时创建

通过 sysbus_connect_irq() 连接 NVIC 和 CPU

// include/hw/arm/armv7m.h
struct ARMv7MState {
    /*< private >*/
    SysBusDevice parent_obj;

    /*< public >*/
    NVICState nvic;
----------------------------------------------------------------------

// include/hw/intc/armv7m_nvic.h
struct NVICState {
    /*< private >*/
    SysBusDevice parent_obj;
    ...
    VecInfo vectors[NVIC_MAX_VECTORS]; // 中断向量
    ...
    qemu_irq excpout;
----------------------------------------------------------------------

// hw/arm/armv7m.c
static void armv7m_instance_init(Object *obj)
{
    ARMv7MState *s = ARMV7M(obj);
    ...
    object_initialize_child(obj, "nvic", &s->nvic, TYPE_NVIC);
        |--> armv7m_nvic_instance_init()
            |--> sysbus_init_irq(sbd, &nvic->excpout);
----------------------------------------------------------------------

// hw/intc/armv7m_nvic.c
static void armv7m_nvic_realize(DeviceState *dev, Error **errp)
{
    NVICState *s = NVIC(dev);
    ...
    qdev_init_gpio_in(dev, set_irq_level, s->num_irq);
    ...
    memory_region_init_io(&s->sysregmem, OBJECT(s), &nvic_sysreg_ops, s,
                          "nvic_sysregs", 0x1000);
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->sysregmem);
}
----------------------------------------------------------------------

// hw/arm/armv7m.c
static void armv7m_realize(DeviceState *dev, Error **errp)
{
    ...
    /* Wire the NVIC up to the CPU */
    sbd = SYS_BUS_DEVICE(&s->nvic);
    sysbus_connect_irq(sbd, 0, qdev_get_gpio_in(DEVICE(s->cpu), ARM_CPU_IRQ));

4.2.设置中断 - set_irq_level()

NVIC 的中断入口函数为 set_irq_level():

// hw/intc/armv7m_nvic.c
/* callback when external interrupt line is changed */
static void set_irq_level(void *opaque, int n, int level)
{
    NVICState *s = opaque;
    VecInfo *vec;
    ...

    vec = &s->vectors[n];
    if (level != vec->level) {
        vec->level = level;
        if (level) {
            armv7m_nvic_set_pending(s, n, false);
                |--> do_armv7m_nvic_set_pending(s, irq, secure, false);
----------------------------------------------------------------------

// hw/intc/armv7m_nvic.c
static void do_armv7m_nvic_set_pending(void *opaque, int irq, bool secure,
                                       bool derived)
{
    ...
    NVICState *s = (NVICState *)opaque;
    ...
    VecInfo *vec;
    ...

    vec = (banked && secure) ? &s->sec_vectors[irq] : &s->vectors[irq];
    ...
    if (!vec->pending) {
        vec->pending = 1;
        nvic_irq_update(s);
----------------------------------------------------------------------

// hw/intc/armv7m_nvic.c
static void nvic_irq_update(NVICState *s)
{
    ...
    pend_prio = nvic_pending_prio(s);
        |--> return s->vectpending_prio;
    ...
    lvl = (pend_prio < s->exception_prio);
    ...
    qemu_set_irq(s->excpout, lvl); // s->excpout --> arm_cpu_set_irq()
        |--> arm_cpu_set_irq()
}

4.3.PL011 中断流程概览

PL011 串口模块在 ARMv7 架构中的中断触发及处理流程如下所示:

pl011_read()   // PL011
    |--> pl011_update()
        |--> qemu_set_irq(s->irq[i], (flags & irqmask[i]) != 0)
            |--> set_irq_level()  // NVIC
                |--> armv7m_nvic_set_pending()
                    |--> do_armv7m_nvic_set_pending(s, irq, secure, false)
                        |--> do_armv7m_nvic_set_pending()
                            |--> nvic_irq_update(s)
                                |--> qemu_set_irq(s->excpout, lvl)
                                    |--> arm_cpu_set_irq()  // CPU
                                        |--> cpu_interrupt(cs, mask[irq])
                                            |--> arm_cpu_exec_interrupt()
                                                |--> arm_cpu_do_interrupt()
                                                    |--> arm_cpu_do_interrupt_aarch32(cs);
                                                    |--> arm_cpu_do_interrupt_aarch64()
                                                        |--> ...
                                                        |--> env->pc = addr;                            

其大致可以分为三层:

  1. PL011 模块调用 pl011_update() 设置中断,将中断信息传递给中断控制器 NVIC;

  2. NVIC 接收到中断请求后进行中断路由,设置相关优先级、标志位等信息,然后调用 nvic_irq_update() 将中断信息发送给 CPU;

  3. CPU 接收到中断请求后调用 arm_cpu_exec_interrupt() 对中断进行预处理,若中断可以执行则调用 arm_cpu_do_interrupt() 跳转至中断服务函数执行;

 


网站公告

今日签到

点亮在社区的每一天
去签到