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

目录

[1.TCG 异常 & 中断捕获](#1.TCG 异常 & 中断捕获)

[1.1.异常捕获 - cpu_handle_exception()](#1.1.异常捕获 - cpu_handle_exception())

[1.2.中断捕获 - cpu_handle_interrupt()](#1.2.中断捕获 - cpu_handle_interrupt())

[2.ARM 中断相关基本概念](#2.ARM 中断相关基本概念)

[2.1.ARMv7 特权等级](#2.1.ARMv7 特权等级)

[2.2.ARMv8 异常等级](#2.2.ARMv8 异常等级)

[2.3..QEMU 异常/中断定义](#2.3..QEMU 异常/中断定义)

[2.4.ARMv8 架构的异常向量表](#2.4.ARMv8 架构的异常向量表)

[3.ARM-VEXPRESS 串口例程](#3.ARM-VEXPRESS 串口例程)

[3.1.CPU 添加中断 GPIO](#3.1.CPU 添加中断 GPIO)

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

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

[3.2.Cortex-A 的中断控制器 - GIC](#3.2.Cortex-A 的中断控制器 - GIC)

[3.2.1.创建 GIC - init_cpus()](#3.2.1.创建 GIC - init_cpus())

[3.2.2.平台实例化 - a9mp_priv_realize()](#3.2.2.平台实例化 - a9mp_priv_realize())

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

[3.2.4.中断入口 - gic_set_irq()](#3.2.4.中断入口 - gic_set_irq())

[3.3.ARM PL011 串口地址](#3.3.ARM PL011 串口地址)

[3.4.VEXPRESS 主板绑定串口](#3.4.VEXPRESS 主板绑定串口)

3.5.串口写入触发中断

[3.6.GIC 中断处理流程](#3.6.GIC 中断处理流程)

[3.6.1.GIC 设置中断 - gic_set_irq()](#3.6.1.GIC 设置中断 - gic_set_irq())

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

[3.7.CPU 处理中断](#3.7.CPU 处理中断)

[3.7.1.中断预处理 - arm_cpu_exec_interrupt()](#3.7.1.中断预处理 - arm_cpu_exec_interrupt())

[3.7.2.中断处理 - arm_cpu_do_interrupt()](#3.7.2.中断处理 - arm_cpu_do_interrupt())

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

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

[4.ARMv7 中断控制器 - NVIC](#4.ARMv7 中断控制器 - NVIC)

[4.1.创建 NVIC](#4.1.创建 NVIC)

[4.2.设置中断 - set_irq_level()](#4.2.设置中断 - set_irq_level())

[4.3.PL011 中断流程概览](#4.3.PL011 中断流程概览)


1.TCG 异常 & 中断捕获

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

cpp 复制代码
// 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 时执行常规异常的处理流程:

cpp 复制代码
// 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() 为例:

cpp 复制代码
// 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 中定义的中断函数进行处理

cpp 复制代码
// 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():

cpp 复制代码
// 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:

cpp 复制代码
// 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 还为目标平台定义了额外的中断源(外部中断源 & 内部中断源):

cpp 复制代码
// 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;

cpp 复制代码
// 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 模式分别有对应的寄存器,如下图所示:

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

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

cpp 复制代码
.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 与中断控制器之间传递中断信息的方式类似

cpp 复制代码
// 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 间接的访问中断控制器

cpp 复制代码
// 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 为例:

cpp 复制代码
// 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 的输入输出

cpp 复制代码
// 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()

cpp 复制代码
// 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 内存:

cpp 复制代码
// 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()

cpp 复制代码
// 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:

cpp 复制代码
// 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 开发板初始化时创建串口模块:

cpp 复制代码
// 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 的中断口和中断控制器相连

cpp 复制代码
// 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 实例化时配置:

cpp 复制代码
// 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

cpp 复制代码
// 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 的中断标志位:

cpp 复制代码
// 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 中定义中断/异常处理函数:

cpp 复制代码
// 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)类型定义如下:

cpp 复制代码
// 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() 方法处理中断:

cpp 复制代码
// 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 指针跳转等操作:

cpp 复制代码
// 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 指针指向该地址从而执行异常处理函数

cpp 复制代码
// 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 架构的相似,区别在于异常等级的处理和相关寄存器的差异

cpp 复制代码
// 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

cpp 复制代码
// 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():

cpp 复制代码
// 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 架构中的中断触发及处理流程如下所示:

cpp 复制代码
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() 跳转至中断服务函数执行;

相关推荐
444A4E1 小时前
深入解析 Linux 进程状态:从 task_struct 双链表到 R/S/D/Z 状态的内核奥秘
linux·操作系统
黑风风1 小时前
Ubuntu 22.04 上安装 PostgreSQL(使用官方 APT 源)
linux·ubuntu·postgresql
淘晶驰AK2 小时前
电机控制选 STM32 还是 DSP?技术选型背后的现实博弈
stm32·单片机·嵌入式硬件
行星0082 小时前
Ubuntu 中安装 PostgreSQL 及常规操作指南
linux·ubuntu·postgresql
奋斗者1号2 小时前
提升WSL中Ubuntu编译速度的完整指南
linux·运维·ubuntu
玄德公笔记2 小时前
ubuntu 22.04安装k8s高可用集群
linux·ubuntu·kubernetes·k8s·containerd·高可用集群·ubuntu 22.04
ZHOU_WUYI2 小时前
在 Ubuntu 上安装 NVM (Node Version Manager) 的步骤
linux·运维·ubuntu
简诚2 小时前
ubuntu 安装上传的 ffmpeg_7.1.1.orig.tar.xz并使用
linux·ubuntu·ffmpeg
Fxrain3 小时前
[嵌入式实验]实验二:LED控制
stm32·单片机·嵌入式硬件
~夕上林~4 小时前
单片机——keil5
单片机·嵌入式硬件