目录
[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.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;
其大致可以分为三层:
-
PL011 模块调用 pl011_update() 设置中断,将中断信息传递给中断控制器 NVIC;
-
NVIC 接收到中断请求后进行中断路由,设置相关优先级、标志位等信息,然后调用 nvic_irq_update() 将中断信息发送给 CPU;
-
CPU 接收到中断请求后调用 arm_cpu_exec_interrupt() 对中断进行预处理,若中断可以执行则调用 arm_cpu_do_interrupt() 跳转至中断服务函数执行;