时间轴

2025-10-22

init

参考文档:

ARM64异常处理之中断处理

  • ARM核心两个和中断相关的管脚:nIRQnFIQ
  • 每个CPU核心有一对这样的中断相关的管脚

ARM® Cortex®-A72 MPCore Processor
Revision: r0p3
Technical Reference Manual

  • PSTATE状态中有两个比特位和中断相关
    • I 用来屏蔽IRQ中断
    • F 用来屏蔽FIQ中断

ARM64中的GIC控制器

  • ARM提供了标准的GIC控制器,例如树莓派4b上支持GIC-400
  • 树莓派3b上支持传统的中断方式(legacy interrupt)

Legacy Interrupt

Legacy Interrupt

中断的处理过程

中断处理的过程

Interrupt Handler in C code

当处理器接受一个异常到 AArch64 执行状态时,所有的 PSTATE 中断掩码都会自动设置。这意味着后续的
异常将被禁用。如果软件要支持异常嵌套,例如,允许高优先级的中断去打断对低优先级源的处理,那么软
件需要显式地重新启用中断。
对于下面的指令:

1
MSR DAIFClr, #imm

Handling nested Interrupts

嵌套处理程序需要一些额外的代码。它必须在堆栈上保存 SPSR_EL1 和 ELR_EL1 的内容。在确定(并清除)
中断源之后,我们还必须重新启用 IRQ。

树莓派4B上的中断源

树莓派4B上的中断源

  • ARM Core n (ARM核心的中断源)

    • PNS timer IRQ对应 A Non-secure EL1 physical timer
    • PS timer IRQ对应A secure EL1 physical timer
    • HP timer IRQ(Hypervisor)对应A Non-secure EL2 physical timer
    • V timer IRQ 对应 A virtual timer
    • PMUPerformance Monitor Unit(性能监控单元)
    • repeated 4 times 表示每个核心都有一组这样的中断源,树莓派有4个核心因此repeated 4 times

    Generic Timer

  • ARM_LOCAL (只有CPU才能访问的中断源)

  • ARMC (CPU和GPU都能访问的中断源)

  • VideoCore (GPU核心的中断 源)

    包括的中断如下表:

    VC(VideoCore) peripheral IRQs

    VideoCore Peripheral IRQs

    VideoCore Peripheral IRQs

  • ETH_PCIe (PCIe的中断)

树莓派4BLegacy Interrupt Routing

Legacy Interrupt Routing

ARM Core IRQs直接路由到pre-core routing

ARMC和VC通过ARMC routing 硬件单元路由

Legacy IRQ status registers

Legacy IRQ status registers

  • FIQn/IRQn_PENDING2

  • FIQn/IRQn_PENDING0

  • FIQn/IRQn_PENDING1

  • FIQ/IRQ_SOURCEn

    当source 寄存器的bit8被设置了,那么你需要读PENDING2状态寄存器

    如果PENDING2的bit24置位了,那么你需要读PENDING0的状态寄存器

    如果PENDING2的bit25置位了,那么你需要读PENDING1的寄存器

    软件需要一步一步读取中断状态寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
       ┌──────────────┐
│ SOURCE寄存器 │ ← ARM_LOCAL_IRQ_SOURCE0
└───────┬──────┘

┌──────────┴──────────┐
│ │
本地中断? bit8=1?
(定时器等) (有外设/GPU中断)
│ │
↓ ↓
handle_timer_irq() ┌───────────────┐
│ PENDING2寄存器│
└───────┬───────┘

┌───────────┬───────────┐
│ │ │
bit24=1? bit25=1? 其它位?
去PENDING0 去PENDING1 直接是GPU中断

Example

树莓派文档中判断类型的例子

以ARM Core的generic timer为例

  • Cortex-A72支持4个ARM Core的generic timer
    • CNT_PS_IRQ Secure EL1 Physical Timer Event Interrupt
    • CNT_PNS_IRQ Nonsecure EL1 Physical Timer Event interrupt
    • CNT_HP_IRQ Hypervisor Physical Timer Event interrupt, EL2
    • CNT_V_IRQ Virtual Timer Event interrupt EL3

CNTP_CTL_ELx和CNTP_TVAL_ELx

Timer支持两种触发方式

Timer的两种触发方式

CNTP_CTL_EL0

XXX_ELn → 表示这个系统寄存器 可在 ELn 及更高特权级访问

  • 比如 CNTP_CTL_EL0 就是 EL0 和 EL1 都能访问

CNTP_TVAL_EL0

EL1的Nonsecure generic timer的中断处理流程

  1. 初始化timer,设置cntp_ctl_el0寄存器的enable域为1
  2. 给timer的TimeValue一个初值,设置cntp_tval_el0寄存器
  3. 打开树莓派中断控制器中和timer相关的中断,设置TIMER_CNTRL0寄存器中的CNT_PNS_IRQ为1

TIMER_CNTRLx

TIMER_CNTRLx

  1. 打开PSTATE寄存器中的IRQ中断总开关

  2. Timer中断发生

  3. CPU跳转到el1_irq汇编函数
  4. 保存中断上下文(使用kernel_entry宏)
  5. 跳转到中断处理函数
  6. 读取ARM_LOCAL中中断状态寄存器IRQ_SOURCE0
  7. 判断是否CNT_PNS_IRQ中断发生
  8. 如果是,重新设置TimeValue
  9. 返回到el1_irq汇编函数
  10. 恢复中断上下文
  11. 返回中断现场

中断现场

  • 中断发生瞬间,CPU的状态包括:
    • PSTATE寄存器
    • PC值
    • SP值
    • x0~x30寄存器
  • 使用一个栈框数据结构来描述需要保存的中断现场(struct pt_regs)

栈框

保存中断现场

保存中断现场

恢复中断现场

恢复中断现场

中断实验1:在树莓派上实现generic timer

实验1

树莓派firmware启动时默认加载GIC控制器而不是使用Legacy Interrupt因此可以在QEMU上跑

  1. 初始化timer,设置cntp_ctl_el0寄存器的enable域为1

    初始化timer

  2. 给timer的TimeValue一个初值,设置cntp_tval_el0寄存器

    设置cntp_tval_el0

  3. 打开树莓派中断控制器中和timer相关的中断,设置TIMER_CNTRL0寄存器中的CNT_PNS_IRQ为1

    CNT_PNS_IRQ宏定义

    树莓派TIMER_CNTRLx的地址

    设置TIMER_CNTRL0的CNT_PNS_IRQ

  4. 打开PSTATE寄存器中的IRQ中断总开关

    打开PSTATE寄存器的IRQ中断总开关

  5. Timer中断发生

  6. CPU跳转到el1_irq汇编函数

    异常向量表

    el1_irq处理逻辑

  7. 保存中断上下文(使用kernel_entry宏)

    中断保存上下文的宏定义

  8. 跳转到中断处理函数

  9. 读取ARM_LOCAL中中断状态寄存器IRQ_SOURCE0

    ARM_LOCAL_IRQ_SOURCE0

    中断转发函数

  10. 判断是否CNT_PNS_IRQ中断发生

  11. 如果是,重新设置TimeValue

timer的中断处理函数

  1. 返回到el1_irq汇编函数
  2. 恢复中断上下文
  3. 返回中断现场

image-20250827234104251

恢复中断现场的宏定义

另一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include "timer.h"
#include "asm/arm_local_reg.h"
#include "io.h"
#include "irq.h"
#include "mydef.h"
#include "printk.h"

static inline unsigned long read_cntfrq(void) {
unsigned long v;
asm volatile("mrs %0, cntfrq_el0" : "=r"(v));
return v;
}
// ms -> ticks
static inline unsigned long ms_to_ticks(unsigned int ms) {
unsigned long f = read_cntfrq();
return (f / 1000UL) * (unsigned long)ms;
}

// 设置cntp_ctl_el0 enable域为1 开启EL1 Nonsecure generic timer
static int generic_timer_init(void) {
asm volatile("mov x0, #1\n"
"msr cntp_ctl_el0, x0"
:
:
: "memory");
return 0;
}
// 设置cntp_ctl_el0 enable域为0 关闭EL1 Nonsecure generic timer
static int generic_timer_deinit(void) {
asm volatile("mov x0, #0\n"
"msr cntp_ctl_el0, x0"
:
:
: "memory");
return 0;
}
// %[name] → 引用约束变量
// %w[name] → 引用 低 32 位寄存器(如 w0, w1)
// %x[name] → 引用 完整 64 位寄存器(如 x0, x1)

static int generic_timer_reset(unsigned int val) {
asm volatile("msr cntp_tval_el0, %x[timer_val]"
:
: [timer_val] "r"(val)
: "memory");
return 0;
}

static void enable_timer_interrupt(void) { writel(CNT_PNS_IRQ, TIMER_CNTRL0); }

static void (*timer_callback)(void) = NULL;
void timer_init(unsigned int ms, void (*callback)(void)) {

// 初始化timer, cntp_ctl_el0 enable为1
unsigned int ticks = ms_to_ticks(ms); // ms -> tick
generic_timer_reset(ticks);
// 打开中断控制器和timer相关的中断
enable_timer_interrupt();
timer_callback = callback;
}

void timer_start(void) {
raw_local_irq_disable(); // 关闭PSTATE中断控制器总开关,防止初始化时被打断
generic_timer_init(); // 打开cntp_ctl_el0.enbale=1,启动计数
raw_local_irq_enable(); // 打开PSTATE中断控制器总开关,防止初始化时被打断
}

void timer_stop(void) {
raw_local_irq_disable(); // 关闭PSTATE中断控制器总开关,防止初始化时被打断
generic_timer_deinit();
raw_local_irq_enable(); // 打开PSTATE中断控制器总开关,防止初始化时被打断
}

void timer_reset(unsigned int ms) {
unsigned int ticks = ms_to_ticks(ms); // ms -> tick
generic_timer_reset(ticks);
}

void handle_timer_irq(void) {
generic_timer_deinit();
if (timer_callback) {
timer_callback();
timer_callback = NULL;
}
printk("Core0 Timer interrupt received\r\n");
}

void kernel_main(void) {
uart_init();
init_printk_done();
printk("Welcome to arm64 mini OS!\r\n");

raw_local_irq_enable(); // 打开PSTATE中断控制器总开关
printk("timer test\n");
timer_init(2000, test_function);
timer_start();
asm volatile("wfi");
/* my test*/
my_test();
// data_abort在QEMU中不起作用
// trigger_sync_data_abort();

trigger_sync_instruction_alignment();

while (1) {
uart_send(uart_recv());
}
}


顺序(推荐的顺序)

  1. cntp_tval_el0 ← 写定时器值
  2. TIMER_CNTRL0.CNT_PNS_IRQ ← 打开外设中断路径
  3. PSTATE.I ← 关闭 CPU IRQ 总开关
  4. cntp_ctl_el0.enable ← 启动定时器
  5. PSTATE.I ← 打开 CPU IRQ 总开关

    这种顺序保证 在 CPU 开 IRQ 前,外设和定时器都准备好了,所以一旦中断触发,CPU 能立刻收到并处理。

裸机/内核初始化阶段,定时器还没准备好,如果这时候 CPU 允许 IRQ,就可能被 别的外设中断打断,导致:

  • 初始化流程被中断,配置寄存器可能只做了一半。
  • 定时器或者外设中断配置还没完成,就有中断 pending → 结果是中断丢失或乱序执行。

中断实验2:使用汇编函数的方式来保存和恢复中断现场

实验2

关键在于使用函数方式lr寄存器被破坏

kernel_entry

el1_irq

kernel_exit

GIC中断控制器

  • 传统的中断控制器,例如树莓派4b上的legacy interrupt controller
    • 中断enable寄存器
    • 中断disable寄存器
    • 中断状态状态寄存器
  • 传统的使用简单状态寄存器的方式来管理中断,变得越来越难管理
    • 中断源变得越来越多
    • 不同类型的中断,比如多核间的中断,中断优先级,软件定义的中断等、

GIC版本

GIC 主要有两个主要的功能块

  • Distributor:系统中的所有中断源都连接到 distributor。Distributor 提供寄存器来控制各个中断的属性,例
    如优先级、状态、安全性、路由信息和启用状态。distributor 通过附加的 CPU 接口确定将哪个中断转发到内
    核。
  • CPU Interface:cpu 通过它接收中断。CPU 接口提供寄存器来屏蔽、识别和控制转发到该内核的中断状态。
    系统中的每个内核都有一个单独的 CPU 接口。

GIC支持的中断类型

  • SGISoftware Generated Interrupt),软件产生的中断,软中断,用于给其他CPU核心发送中断信号

  • PPIPrivate Peripheral Interrupt),私有的外设中断,该中断时某个指定的CPU独有的

  • SPIShared Peripheral Interrupt),共享的外设中断,所有CPU都可以访问这个中断

  • LPILocality-specific Peripheral Interrupt),本地特殊外设中断,GICv3新增的中断类型基于消息传递的中断类型

    GIC支持的中断类型

中断的触发类型

每个中断类型要么是Edge-triggered的要么是Level-sensitive的:

中断的触发类型

GICD_ICFGRn寄存器控制

  • Edge-triggered: 当 GIC 检测到相关输入的上升沿时被认为是有效的,并且在清除之前保持有效
  • Level-sensitive: 仅当 GIC 的相关输入为高电平时才有效

中断号

中断号

banked interrupt 意思是对于 SGI 和 PPI,虽然它们的 ID 在整个系统里一样(例如 Timer PPI 在所有核都是 ID30),但是 它们是私有的,每个 CPU 都有自己的一份,互不干扰。

  • SGI (0–15):某核触发 SGI0,不会影响别的核的 SGI0。
  • PPI (16–31):核 0 的 Timer PPI(比如 ID30)和核 1 的 Timer PPI(ID30)是独立的,它们不会共享。

    中断号 1020~1023是保留的

    Special Interrupt numbers

中断优先级

每个中断优先级设置在GICD_IPRIORITYRn寄存器中

优先级字段的宽度

  • 每个中断优先级字段是8bit
  • 实际实现支持的优先级级数 = 2ⁿ,n ∈ [4, 8](即 16 ~ 256 级)。
  • 如果硬件实现的比 8 小,比如只实现 5 bit,则写入低 3 bit 是 RAZ/WI(读为 0、写无效)。

数值大小与优先级

  • 数值越小,优先级越高
  • 最大优先级值依赖实现
  • 初始化默认应当给所有中断一个中等优先级,避免打断系统调度,常见是0xa0或0x80

中断状态

  • inactive (不活跃状态):中断处于无效状态

  • pending (等待状态):中断处于有效状态,但是等待CPU响应该中断

  • active (活跃状态):CPU已经响应该中断

  • active and pending (活跃并等待状态):CPU正在响应该中断,但是该中断源又发送中断过来

    中断状态机

中断路由

Distributor

GICD_ITARGETSRn (Interrupt Processor Targets Registers)

GICD_ITARGETSRn

GICD_ITARGETSRn寄存器用来配置Distributor可以把中断路由到哪个CPU上,是一个 32 位寄存器,通常每个中断有一个对应的寄存器

  • 8bit来表示一个中断源,共四个中断源,每个bit代表能路由的CPU
  • byte-accessible
  • 某个bit设置了,说明该中断源可以路由到这个CPU上
  • 前32个中断源的路由配置是硬件设置好的,RO
  • 第33~1019号中断,可以由软件来配置其路由,RW

对应计算方法

计算方法

假设m是中断ID, n是对应寄存器号

  • GICD_ITARGETSRn寄存器与中断源的关系
    • 每个GICD_ITARGETSRn控制四个中断源
    • n = m DIV 4
    • 计算n后要加上GICD_ITARGETSRn寄存器的起始地址偏移0x800
  • Priority字段的字节偏移
    • m MOD 4用来确定目标中断源对应的字节偏移
      • 偏移 0 对应寄存器的 [7:0] 位(即最低字节),对应中断 ID m % 4 == 0
      • 偏移 1 对应寄存器的 [15:8] 位,适用于 m % 4 == 1
      • 偏移 2 对应寄存器的 [23:16] 位,适用于 m % 4 == 2
      • 偏移 3 对应寄存器的 [31:24] 位,适用于 m % 4 == 3

GICD_ITARGETSRn 是一个数组寄存器

CPU targets

GICV2中断控制器

GICV2中断控制器

  • The Distributor registers (GICD_) 包含了中断设置和配置
  • The CPU Interface registers (GICC_) 包含CPU相关的特殊设置

GIC logical partitioning

中断传递的优先级和目标 core 都在 distributor 中配置。

外围设备 distributor 发送的中断都处于待处理状态。distributor 确定最高优先级的待处理的中断,可以传递
到 core 并将其转发到 CPU 接口,在 CPU 接口处,中断依次发送给 core,此时 core 接受 FIQ 或 IRQ 异常。
core 执行异常处理程序作为响应。处理程序从 CPU 接口寄存器中查询中断 ID 并开始执行中断服务程序。完
成后,处理程序必须写入 CPU 接口寄存器以报告处理结束。

Distributor 提供了报告不同中断 ID 的当前状态的寄存器。在多核/多处理器系统中,单个 GIC 可以由多个内
核共享(在 GICv2 中最多 8 个)。GIC 提供寄存器来控制 SPI 所对应的内核。这个机制使操作系统能够跨内
核共享和分发中断,并且协调工作。

配置

GIC 的寄存器实现都是外部 memory_map 形式。所有的核都可以访问公共的 Distributor,但是 CPU 接口是
banked 的,即每个核使用相同的地址访问自己的私有 CPU 接口。一个内核不可能访问另一个内核的 CPU 接
口。

Distributor 包含许多寄存器,您可以使用它们来配置各个中断的属性。这些可配置的属性有:

  • 中断优先级GICD_IPRIORITY),distributor 使用它来确定哪个中断接下来被转发到 CPU 接口。
  • 中断配置GICD_ICFGR)。这决定了中断是电平还是边缘敏感。不适用于 SGI。
  • 一个中断目标 coreGICD_ITARGETSR)。这确定了中断可以路由到哪些 core。仅适用于 SPI。
  • 中断启用或禁用状态GICD_ISENABLERGICD_ICENABLER)。只有那些在 distributor 中启用的中断。
    当它们处于待处理状态时有资格被路由到 cpu 接口。
  • 中断安全 (GICD_IGROUPR) 确定中断是否分配给安全或非安全。
  • 中断状态

Distributor 还提供了优先级屏蔽,通过它可以防止低于某个优先级的中断到达内核。distributor 在确定是否可
以将挂起的中断转发到特定内核时使用它每个内核上的 CPU 接口有助于微调中断控制和处理核

初始化

Distributor 和 CPU interface 在复位时都被禁用。GIC 必须在复位后初始化,然后才能向内核提供中断。

在 Distributor 中,软件必须配置优先级、目标 core、安全性并启用各个中断。随后必须通过其控制寄存器
(GICD_CTLR) 启用 distributor

对于每个 CPU interface,软件必须对优先级掩码和优先级抢占进行设置。每个 CPU interface 本身必须通过控制寄存器 (GICD_CTLR) 启用。

在 cpu 处理中断之前,软件通过设置使 cpu准备好接受中断向量表中的有效中断向量,并清除 PSTATE 中的中断屏蔽位,并设置路由控制。

可以通过禁用 Distributor 来禁用系统中的整个中断机制。也可以通过禁用其 CPU 接口来禁用对单个内核的中断传递。也可以在分配器中禁用(或启用)各个中断。

对于一个中断到达核心,单个中断、分配器和 CPU 接口必须全部启用。中断也需要有足够的优先级,即高于内核的优先掩码。

中断处理

当 core 接受了中断,就跳转到顶层中断向量表并且开始执行。顶层的中断处理程序从 CPU interface 读取寄存
器以获得中断 ID

读取寄存器除了返回中断 ID 外,还会导致中断在 distributor 中被标记为活 active。一旦知道中断 ID(识别中
断源),顶层处理程序就可以调度特定于设备的中断处理程序来服务中断。

当中断处理程序完成执行时,顶层处理程序将相同的中断 ID 写入 CPU 接口块中的中断结束 (EoI) 寄存器,指
示中断处理结束。

除了移除活动状态,使最终的中断状态为 Inactive 或 pending(如果状态为 active and pending),这使 CPU 接口能够将更多挂起的中断转发到 core。这结束了单个中断的处理。

在同一个内核上可能有多个中断等待服务,但是 CPU 接口一次只能发出一个中断信号。顶层中断处理程序
可以重复上述顺序,直到读取到特殊中断 ID 值 1023表示有在这个核心上没有更多的中断挂起。这个特殊
的中断 ID 被称为虚假的中断标识。虚假中断 ID 是一个保留值,不能分配给系统。当顶层处理程序读取了虚
假中断 ID 后,它可以完成它的执行,并准备内核在中断之前恢复它正在执行的任务。

通用中断控制器 (GIC)通常管理来自多个中断源的输入。并将它们分发给 IRQ 或 FIQ 。

中断状态时序图

中断状态时序图

M和N分别代表两个中断,Input表示中断信号,State表示中断状态机,nFIQCPU[n]表示连接到CPU核心的FIQ信号

假设M和N都是SPI中断,且N的优先级高于M

GICv2寄存器

GIC寄存器后缀

有些寄存器是按中断号来描述的,比如使用某几个比特位来描述一个中断号的相关属性,同一个寄存器可以n个,例如GICD_ISENABLERn寄存器,它是用来使能某个中断号的。“n”表示它有n个这样的寄存器

GICD_ISENABLERn

GICD_ISENABLERn的Set-enable bits

计算方法:根据中断号计算对应寄存器

计算方法

对于有些寄存器,会提示:

A register bit corresponding to an unimplemented interrupt is RAZ/WI.

RAZ = Read-As-Zero
如果你读这个寄存器位,返回值永远是 0,即使你写过别的值。

WI = Write-Ignored
如果你往这个位写入数据,硬件会忽略,不会改变,也不会报错。

GICD_CTLR

Distributor Control Register

image-20250830231416178

GICD_CTLR bit

GICD_ITARGETSRn

Interrupt Processor Targets Registers,见中断路由章节

GICD_TYPER (Interrupt Controller Type Register)

描述这个 GIC 的能力和配置参数,比如有多少中断、多少个 CPU 接口、是否支持某些特性等等。

GICD_TYPER

GICD_TYPER bit

GICD_ICFGRn

Interrupt Configuration Registers

GIC用来配置中断触发类型的寄存器

每个 GICD_ICFGRn32 位寄存器

GICD_ICFGRn

分布方式

  • 每个中断需要 2 bitInt_config)来描述,所以一个寄存器可以配置 16 个中断

  • n 个寄存器(ICFGRn)对应的中断范围是:

    1
    中断 ID = n*16  ~ (n*16 + 15)

Int_config字段编码

根据 ARM GICv2 TRM(Table 4-18):

GICD_ICFG Int_config.field

对于PPI(Private Peripheral Interrupt)和SPI(Private Peripheral Interrupt)来说

Bit[1] Bit[0] 含义
0 reversed Level-sensitive
1 reserved Edge-triggered

GICD_ISENABLERn

Interrupt Set-Enable Registers

负责打开中断转发

  • GICD_ISENABLERn32位寄存器

  • 每个 GICD_ISENABLERn 控制 32 个中断源

    GICD_ISENABLERn

  • 写 1 → 使能对应的中断(让 Distributor 可以把它转发给 CPU interface)

  • 写 0 → 没有作用

  • → 可以看到某个中断当前是否被使能(1=已使能,0=未使能)

根据中断号计算GICD_ISENABLERn的n

GICD_ISENABLERn计算

GICD_ICENABLERn

Interrupt Clear-Enable Registers

  • W1C (写 1 清除),禁用某个中断(禁止分发到 CPU)

GICD_ICEABLERn

GICD_ICEABLERn Clear-enable bit

GICD_ICENABLERn计算

GICD_ISACTIVERn

Interrupt Set-Active Registers

  • W1S(写1设置),软件可以把一个中断状态人为标记为 active

GICD_ISACTIVERn

GICD_ISACTIVERn Set-active bits

GICD_ISACTIVERn计算

GICD_ICACTIVERn

Interrupt Clear-Active Registers

  • W1C(写1清除),清除中断的 active 状态。

  • 和 EOI(End of Interrupt)有点类似,但 EOI 是 CPU interface 寄存器,只能由当前 CPU 对自己接收的中断做“中断服务完成”的动作;

  • ICACTIVERnDistributor,软件可以全局地强制清除某个中断的 active 位

GICD_ICACTIVERn

GICD_ICACTIVERn Clear-active bits

GICD_ICACTIVERn

GICD_IPRIORITYRn

Interrupt Priority Registers

用于设置每个中断的优先级

  • 每个中断占 8 bit

  • 一个 32 位寄存器管理 4 个中断

GICD_IPRIORITYRn

GICD_IPRIORITYRn bit assignments

地址计算:

GICD_IPRIORITYRn地址计算

GICC_CTLR

CPU Interface Control Register

GICC_CTLR

GICC_CTLR bit assignment

GICC_PMR

Iterrupt Priority Mask Register

CPU 接口的优先级屏蔽寄存器,它决定了 CPU 能够接收哪些优先级的中断

  • 只有 优先级数值 ≤ PMR 的中断 才能被 CPU 接收。
  • 注意:GIC 的优先级是 数值越小,优先级越高

GICC_PMR

GICC_PMR bit assignment

使用举例:假设你的 GIC 实现有 8 bit 优先级:

  1. 如果 GICC_PMR = 0xff
    • CPU 接收所有优先级的中断(最常见的初始化设置)。
  2. 如果 GICC_PMR = 0xa0
    • 只接收 优先级值 ≤ 0xa0 的中断,优先级更“低”的中断会被屏蔽。
  3. 如果 GICC_PMR = 0x0
    • 只接收最高优先级(0)的中断,其他都屏蔽

GICC_IAR

Interrupt Acknowledge Register

当 CPU 收到 IRQ 信号时,软件需要从这个寄存器读取当前 pending 中断的 ID,并确认它是哪一个 IRQ。

GICC_IAR

Bits 名称 含义
[9:0] INTID 中断 ID(0~1019 表示有效中断号)
[12:10] CPUID 标识触发该中断的 CPU(多核系统有用)
[31:13] Reserved 保留,读出为 0

GICC_EOIR

End of Interrupt Register

软件在完成中断处理后,必须写入这个寄存器,告诉 GIC:“这个 IRQ 已经处理完了,可以清除 active 状态,并允许后续相同中断再次触发”。

GICC_EOIR

Bits 名称 含义
[9:0] INTID 中断 ID(必须与 IAR 读出的中断号一致)
[12:10] CPUID 发出该中断的 CPU ID(多核系统使用)
[31:13] Reserved 保留,写 0

树莓派4B的GIC400

GIC-400

  • ARM Core的中断:
    • Core n HP tiemr IRQ
    • Core n V timer IRQ
    • Legacy FIQn
    • Core n PS timer IRQ
    • Core n PNS timer IRQ (PPI ID 30)
    • Legacy IRQn
  • ARM local的中断
    • ARM Mailbox IRQs
    • Core 0 PMU IRQ
    • Core 1 PMU IRQ
    • Core 2 PMU IRQ
    • Core 3 PMU IRQ
    • AXIERR IRQ
    • Local timer IRQ
  • 16个ARMC外设中断
  • 64个VC外设中断
  • 51个PCI相关的外设中断

访问GIC-400寄存器

  • 树莓派4b上的GIC-400的基地址

GIC-400地址

GIC-400 register map

GIC-400 memory map

GIC-400 memory map

访问:树莓派GIC400的基地址 + GIC-400 memory map偏移 + 寄存器偏移

GIC400的初始化流程

  1. 设置distributor和CPU interface寄存器组的基地址
  2. 读取GICD_TYPER寄存器,计算当前GIC最大支持多少个中断源
  3. 初始化distributor
    1. Disable distributor,设置GICD_CTLR(这一步可以不做,因为复位时本来就是关闭的)
    2. 设置SPI中断的路由
      1. 前32个中断怎么路由是GIC芯片固定的,因此先读GICD_ITARGETSRn前面的值,获取能路由的所有CPU
      2. SPI(Shared Peripheral Interrupt)中断 的路由设置为将中断分发到所有能路由的 CPU,因为SPI中断是Shared的中断,设置SPI的GICD_ITARGETSRn
    3. 设置SPI中断的触发类型,例如Level触发,设置GICD_ICFGRn
    4. Disactive和disable所有的SPI中断源
    5. 打开SGI中断(0-15),SMP会用到
    6. Enable distributor,设置GICD_CTLR
  4. 初始化CPU interface
    1. 为前32个中断源设置默认中断优先级,设置GICD_IPRIORITYRn
    2. 设置GICC_PRM,设置中断优先级mask level
    3. Enable CPU interface,设置GICC_PRM
注册中断
  1. 初始化外设
  2. 查找该外设的中断在GIC-400的中断号,例如PNS timer的中断号为30
  3. 设置GICD_ISENABLERn寄存器来enable这个中断号
  4. 打开设备相关的中断,例如树莓派上的generic timer,需要打开ARM_LOCAL寄存器中的TIMER_CNTRL0寄存器中相关的enable位
  5. 打开CPU的PSTATE中的I位
中断响应
  1. 中断发生
  2. 异常向量表
  3. 跳转到GIC中断函数里,gic_handle_irq()
  4. 读取GICC_IAR寄存器,获取中断号
  5. 根据中断号来进行相应的中断处理,例如读取的中断号为30,说明的是PNS的generic timer,然后跳转到generic timer的处理函数里。

GIC中断实验1:实现generic timer

GIC中断实验1

GIC-400初始化

  1. 设置distributorCPU interface寄存器组的基地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef __GIC_V2_H
#define __GIC_V2_H

#include "asm/base.h"

#define GIC_V2_DISTRIBUTOR_OFFSET 0x1000
#define GIC_V2_CPU_INTERFACE_OFFSET 0x2000
#define GIC_V2_DISTRIBUTOR_BASE (GIC_400_BASE + GIC_V2_DISTRIBUTOR_OFFSET)
#define GIC_V2_CPU_INTERFACE_BASE (GIC_400_BASE + GIC_V2_CPU_INTERFACE_OFFSET)

#define GICD_CTLR (GIC_V2_DISTRIBUTOR_BASE + 0x000)

#define GICD_TYPER (GIC_V2_DISTRIBUTOR_BASE + 0x004)
#define GICD_TYPER_ITLINESNUMBER 0x1f
// 直接用 n / 4 或 n / 16 会得到 寄存器索引
// 然后在地址计算上乘以 4(每个寄存器 4 字节)
#define GICD_ITARGETSRn(n) (GIC_V2_DISTRIBUTOR_BASE + 0x800 + (n / 4) * 4)
#define GICD_ICFGRn(n) (GIC_V2_DISTRIBUTOR_BASE + 0xc00 + (n / 16) * 4)

#define GICD_ICFG_LEVEL_SENSITIVE 0x0
#define GICD_ICFG_LEVEL_EDGE_TRIGGERED 0x2

#define GICD_ISENABLERn(n) (GIC_V2_DISTRIBUTOR_BASE + 0x100 + (n / 32) * 4)
#define GICD_ICENABLERn(n) (GIC_V2_DISTRIBUTOR_BASE + 0x180 + (n / 32) * 4)
#define GICD_ICACTIVERn(n) (GIC_V2_DISTRIBUTOR_BASE + 0x380 + (n / 32) * 4)

#define GICD_IPRIORITYRn(n) (GIC_V2_DISTRIBUTOR_BASE + 0x400 + (n / 4) * 4)
#define DAFAULT_IPRIORITY 0xa0a0a0a0

#define GICC_PMR (GIC_V2_CPU_INTERFACE_BASE + 0x4)
#define GICC_CTLR (GIC_V2_CPU_INTERFACE_BASE + 0x0)

#define GICC_IAR (GIC_V2_CPU_INTERFACE_BASE + 0xc)
#define GICC_IAR_CPU_ID_MASK 0x1c00
#define GICC_IAR_INT_ID_MASK 0x3ff
#define GICC_EOIR (GIC_V2_CPU_INTERFACE_BASE + 0x10)

int gic_init(void);
void gic_enable_irq(int irq);

#endif

  1. 读取GICD_TYPER寄存器,计算当前GIC最大支持多少个中断源

读取GICD_TYPER寄存器

  1. 初始化distributor

    初始化distributor

    gic_distributor_init

  2. 初始化CPU interface

    gic_cpu_init

  3. 打开PNS_TIMER_IRQ的路由

打开PNS_TIMER_IRQ路由

  1. 测试

gic_irq_handle

测试

GIC中断实验2:实现树莓派上的System Timer

实验2

GICv3中断控制器

GICv3比GICv2改进了哪些内容

  • GICv3兼容GICv2
  • 支持更多CPU数量,>8
  • 支持message-base中断(Message Signaled InterruptMSI
  • 支持ITSInterrupt Translation Service)服务
  • 支持更多的硬件中断号,>1020
  • 为了更好兼容ARMv8异常模型,支持中断组(Interrupt grouping)
  • 为了优化访问延时,提供系统寄存器的方式来访问CPU Interface

GICv3支持的中断类型

  • Private Peripheral Interrupt(PPI)
    • PPI是指本地CPU特有的中断,比如CPU内部的定时器等。不同的CPU可以使用相同的PPI中断号
    • PPI可以在group0和group1
    • 可以边沿触发(edge-triggered)或者水平触发(level-sensitve)
  • Shared Peripheral Interrupt(SPI)
    • SPI通常用于外设中断,它可以路由到任意一个CPU
    • SPI可以在group0和group1
    • 可以边沿触发或者水平触发
  • Software Generated Interrupt(SGI)
    • SGI通常软件触发的中断,用于核间通信,例如IPI(Inter-Processor Interrupts)
    • SGI只能边沿触发
  • 新增:Locality-specific Peripheral Interrupt(LPI)
    • 在非安全中断组1
    • 边沿触发
    • 使用ITS服务
    • 没有active状态
    • message-based中断

LPI

有线中断与基于消息的中断

有线中断

基于消息的中断

  • SPI和LPI都支持message-base中断
    • SPI的message-base中断不需要经过ITS,往GICD_SETSPI_NSR寄存器写入中断号触发中断
    • LPI的message-base需要ITS

中断号分配

中断号分配

中断状态机

  • Inactive 不活跃状态
  • Pending 中断触发了,但是还没有被CPU响应(acknowledge)
  • Active 中断被CPU响应和处理
  • Active & Pending 当前有一个中断正在被CPU响应和处理,这时有一个相同的中断触发了,这个新的中断被设置为active & pending
  • LPI没有active和active & pending状态

问题:如果GIC在响应中断的过程中,又有一个相同的中断触发了,那怎么办?

这里要分两种情况:

  • 如果GIC正在响应第一个中断时,即第一个中断的状态为active,此时第二个相同的中断触发,那么蝶儿个中断的状态会变成 active & pending,这样避免丢失了中断
  • 如果第一个中断还处于pending状态,此时又来了一个相同的中断,那么它们会merge成一个

中断状态机

中断亲和性路由层级(Affinity routing)

  • GICv3支持4级路由
  • Level 0面对的是redistributor

Distributor分发

core的唯一标识

GIC-500的中断亲和性路由

GIC-500的中断亲和性路由

  • GIC-500支持两级的亲和性路由
    • Level 0 是 core
    • Level 1 是 cluster
  • GIC-500最大支持128个cores以及32个cluster

0.0.0.1表示第0个cluster中的第1个core

0.0.1.1表示第1个cluster中的第1个core

  • 中断组与安全模式
    • ARMv8支持安全模式和非安全模式
    • GICv3支持EL0~EL3,所以每个中断源都需要设置对应的中断组和安全模式
      • Group0用于EL3
      • 安全模式的Group1:用于Trust OS on EL2
      • 非安全模式的Group1:用于VMM或者OS

中断组与安全模式

Group0使用FIQ,Group1根据情况使用IRQ or FIQ

Group0,Group1

Exception Level

例子

Non-secure/secure Group

注意:中断是否会路由到EL3要看SCR_EL3寄存器FIQ和IRQ字段

  • 在非安全模式下:
    • 非安全group1的中断直接在RichOS响应
    • 安全group1和Group0的FIQ中断路由到EL3
  • 在安全模式下
    • 安全group1的IRQ中断直接在Trusted OS响应
    • 非安全group1和group0的FIQ中断路由到EL3

特殊中断号

特殊中断号

1021号中断的使用1

例子: 陷入到EL3

  1. CPU运行在安全模式下的trusted os,此时来了一个非安全模式下的OS的中断,那么需要陷入到EL3的FIQ来处理
  2. CPU陷入到EL3的安全监视器,安全监视器会读取IAR寄存器,并且读到1021,说明这个中断是希望在非安全模式下处理的。切换到非安全模式的Rich OS
  3. CPU切换到非安全模式的Rich OS来处理这个中断

1021号中断的使用2

例子: 陷入到EL2的情况

  1. CPU运行在安全模式下的trusted os,此时来了一个非安全模式下OS的中断。那么需要陷入到EL3的FIQ来处理。但是由于SCR_EL3.FIQ=0,它只能在EL2中处理。
  2. EL2的Trusted OS执行SMC系统调用陷入到EL3
  3. 安全监视器会读取IAR寄存器,并且读到1021,说明这个中断是希望在非安全模式下处理的。切换到非安全模式的Rich OS
  4. CPU切换到非安全模式的Rich OS来处理这个中断

中断优先级

  • GICv3支持8位优先级,最多256级别
    • 支持2个安全模式时,最少支持32个中断优先级,最多支持256个
    • 支持1个安全模式时,最少支持16个中断优先级
  • 中断优先级
    • 数值越小,中断优先级越高,0表示最高优先级,255表示最低优先级,idle priority
    • GICR_IPRIORITYR\ 设置PPI和SGI中断优先级
    • GICD_IPRIORITYR\ 设置SPI中断优先级
    • LPI配置表保存了LPI的中断优先级

中断优先级分组寄存器(ICC_BPR0_EL1/ICC_BPR1_EL1)

  • Binary Point Register寄存器把中断优先级分成两个字段
    • group priority 需要抢占时,优先比较group priority
    • subpriority 当需要抢占时,只有当group priority相同时,才比较subpriority

中断优先级分组寄存器

Group0的中断优先级分组情况,见ICC_BPR0_EL1寄存器

注意:group1和group0的分组情况略有不同

中断优先级阈值与运行中断优先级

  • 中断阈值
    • 寄存器:ICC_PMR_EL1
    • PMR决定目标CPU的优先级阈值。GIC只有当pending中断的优先级高于这个中断优先级阈值,才会发送中断到CPU
    • PMR为0,表示屏蔽了所有中断发送给CPU
  • Running priority
    • 寄存器:ICC_PMR_EL1
    • 返回正在响应中的group priority

中断优先级的抢占

  • GICv3支持中断抢占,当一个中断优先级同时满足下面条件
    • 优先级高于CPU接口的优先级阈值PMR
    • Group priority高于正在处理的running prirority

GICv3内部架构

GICv3内部架构

  • Distributor:优先级排队,派发SPI和SGI到redistributor
  • Redistributor:连接CPU interface。每个CPU一个redistributor
  • CPU interface:发送中断给CPU,响应中断等
  • ITS: 中断转换服务,把LPIs中断请求转换到中断号以及发送到redistributor

ITS服务(Interrupt translation service)

  • ITS作用:把设备(device_id)的Event_ID转成:
    • 硬件中断号(INTID)
    • 目标redistributor
  • ITS转换过程
    • 使用Device_ID来查询device table
    • 使用Event_ID来查询Interrupt translation table
      • 物理中断号INTID
      • interrupt collection number
    • 由ICID来查询Collection table得到目标redistributor
  • ITS五张表
    • 中断配置表configure table
    • 中断pending表
    • device table
    • Interrupt translation table
    • Collection table

Interrupt translation service

Interrupt translation

配置表和pending表

  • 中断配置表用来存储每个LPI中断的优先级和enable位

    • 每个表项占8bit
    • 配置表基地址:GICR_PROPBASER.Physical_Address
    • 表项个数:2^(GICR_PROPBASER.IDbits)

    中断配置表

中断配置表表表项

  • 中断pending 用来表示每个LPI中断的pending状态
    • 每个表项占1个bit
    • 每个redistributor有一个中断pending表

Pending表

Device Table的创建

  • Device Table由OS软件创建
    • 分配内存,创建Table
    • 把表的基地址设置到GITS_BASER.Physical_Address
  • Device Table的重要参数在GITS_BASER\寄存器中

    • 表的类型:GITS_BASER.Type
    • 表项的大小:GITS_BASER.Entry_Size(8个字节)
    • Table中每个page的大小:GITS_BASER.Page_Size(eg, 64KB)
    • 是否需要二级页表:GITS_BASER_Indirect
    • 设置表基地址:GITS_BASER.Physical_Address
    • 需要多少个表项:2^(device_id位宽),device_id位宽在GITS_TYPER.devbits
  • Device Table支持1级或者2级表

  • Ddevice Table表项的内容由:
    • 软件通过command queue来发送命令给硬件,硬件来填充表项
    • 通过MAPD命令,把deviceID映射到ITT表中

Device table一级表

Device Table二级表

  • Device Table的表项叫做DTE,用来指向ITT表的

DDT表项DTE

  • ITT的表项叫做ITE,用来描述EventID和最终物理ID号的关系

ITT表项ITE

Interrupt translation table的创建

  • ITT表项用来映射 EventID -> 物理中断号INTID以及ICID
  • ITT重要参数
    • ITT表项大小:GITS_TYPER.ITT_entry_size(eg:16字节)
    • ITT表项个数:申请中断时请求的vector个数
    • ITT表的基地址:由OS来分配
  • ITT表项的内容由:
    • 软件通过command queue来发送命令给硬件,硬件来完成
    • 通过MAPD命令,把deviceID映射到ITT表中

Collection Table的创建

  • Collection Table表项用来映射:ICID -> redistributor
  • 这个表不需要在内存中分配内存
  • Collection Table表项的内容由:

    • 软件通过command queue来发送命令给硬件,硬件来完成
    • 软件告知:redistributor的物理地址或者GICR_TYPER.Processor_Number
    • 通过MAPC命令来映射ICID -> redistributor
    • 在GIC初始化的时候就遍历所有的redistributor,并且调用MAPC命令初始化

    gic_smp_init()->

    ​ 遍历所有present CPU()->

    ​ gic_starting_cpu()->

    ​ its_cpu_init()->

    ​ its_cpu_init_collections()->

    ​ its_cpu_init_collection()->

    ​ its_send_mapc()发送MAPC命令初始化collection table

  • Collection table的表项CTE

collection table表项CTE

ITS Command Queue

  • 三个与command queue相关的寄存器
    • GITS_CBASER:指定command queue的大小和基地,基地址必须64KB对齐,大小必须4KB整数倍
    • GITS_CREADR:ITS下一条要处理的command
    • GITS_CWRITER:下一条要写入的command
  • 常用的Command
    • MAPD:映射device ID到ITT table中
      • MAPD \, \, \
    • MAPI:映射eventid和deviceid以及硬件中断号到ITT中
      • MAPI \, \, \
    • MAPTI:映射eventid,deviceid以及硬件中断号到ITT中
      • MAPTI \, \, \, \
    • MAPC:映射collection id到目标redistributor
      • MAPC \, \

ITS Command Queue

例子

假设某个设备的Device ID为5,想把Event ID=0映射到物理中断号8192中,ITT表的基地址为0x850000。对应的Collection ID为3,对应的目标redistributor的物理地址为0x78400000

例子

linux内核中喜欢用vector表示eventid

ITS在Linux中的实现

qemu virtio device its

irq domain中断控制域
  • 系统存在多级中断控制器的可能性
    • 传统中断控制器-GIC
    • 可以抽象成中断控制器:GIC ITS,GPIO等
    • 虚拟中断控制器,platform irqdomain
  • IRQ Domain看作是IRQ Controller的软件抽象

irq domain

ITS驱动框架

ITS驱动代码

ITS驱动框架

OPS

创建MSI中断的API

MSI中断 platform device

MSI中断 PCI device

Platform device中使用MSI中断的例子-SMMUv3

SMMUv3

platform请求分配MSI中断流程图

platform请求分配MSI中断流程图

PCI设备分配MSI中断流程图

PCI设备分配MSI中断

pci_alloc_irq_vectors_affinity

IO设备如何触发中断?

对于GICv3中断控制来说,IO设备需要往GITS_TRANSLATER寄存器写入event ID,即可触发MSI中断

GITS_TRANSLATER

Linux内核封装了一个struct msi_msg数据结构,包括了这个寄存器的地址,以及将要写入的数据

1
2
3
4
5
struct msi_msg{
u32 address_lo;
u32 address_hi;
u32 data;
}

在irq_chip的ops有一个its_irq_compose_msi_msg回调函数,用来填充这个msi_msg,就是把GITS_TRANSLATER寄存器的物理地址写入到msi_msg->address字段里

设备驱动程序在使用platform_msi_domain_alloc_irqs()注册MSI的时候,就会从msi_msg数据结构中获取到GITS_TRANSLATER寄存器的物理地址和eventID,然后写入到IO设备自己的寄存器(例如SMMU中的SMMU_EVENTQ_IRQ_CFGO和CFG1)里。

设备要触发MSI,就会自动往自己寄存器里写入eventid,来触发中断。

以SMMU为例:

SMMU

ITS Debug Tips
  • 最新的qemu支持ITS。可以通过QEMU+linux kernel来单步调试ITS和MSI
  • 在kernel command line中添加”irq_gic_v3_its.dyndbg=+pflmt irqdomain.dyndbg=+pflmt”来打开相关的动态打印:

kenel启动日志

  • 可以在its_domain_ops回调中添加”dump_stack()”来打印调用函数关系calltrace