从源码分析arm64中断与GIC

本文以树莓派4b(armv8)来实现,4b支持两种

  • 传统的中断控制器
  • gic-400
    但是使用的qemu和实际的板子都是默认支持gic-400的,所以主要是借助gic-400实现中断的功能

异常处理

相关寄存器

  • PSTATE 就是cpu状态
    • DAIF 调试异常 SError(系统异常) IRQ(中断) FIQ(快速中断)
  • esr_elx 用来保存返回地址
  • spsr_elx 用来保存对应级别的PSTATE
  • elr_elx 用来保存异常的原因

处理异常时自动发生的事

CPU捕获到异常时

  1. 将PSTATE保存到对应的SPSR_ELx中
  2. 返回地址保存到ELR_ELx中
  3. PSTATE DAIF关闭
  4. 如果是同步异常把原因写入ESR_ELx,如果是中断,原因保存在GIC-400的寄存器中
  5. 切换SP到对应的SP_ELx中
  6. 跳转到中断向量表里
  7. 执行对应的处理函数

CPU处理完异常执行eret,会

  1. ELR中恢复PC
  2. SPSR中恢复PSTATE (DAIF也会变)

中断向量表

可见通过保存到对应异常的vbar中,CPU就可以在对应级别是发生异常进入中断向量表中
由于内核目前一直在EL1阶段,所以在el1的初始化函数el1_entry

el1_entry:
    // 加入向量表
    adr x0, vectors
    msr vbar_el1, x0

其中vector的实现是参考linux的实现

    .macro kernel_ventry, el, label, regsize=64
    .align 7
    sub sp, sp, #S_FRAME_SIZE
    b el\()\el\()_\label
    .endm

    .pushsection ".entry.text", "ax"
    .align 11
ENTRY(vectors)
	kernel_ventry	1, sync_invalid			// Synchronous EL1t
	kernel_ventry	1, irq_invalid			// IRQ EL1t
	kernel_ventry	1, fiq_invalid			// FIQ EL1t
	kernel_ventry	1, error_invalid		// Error EL1t

	kernel_ventry	1, sync_invalid				// Synchronous EL1h
	kernel_ventry	1, irq                      // IRQ EL1h
	kernel_ventry	1, fiq_invalid			// FIQ EL1h
	kernel_ventry	1, error_invalid			// Error EL1h

	kernel_ventry	0, sync_invalid				// Synchronous 64-bit EL0
	kernel_ventry	0, irq_invalid				// IRQ 64-bit EL0
	kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0
	kernel_ventry	0, error_invalid			// Error 64-bit EL0

	kernel_ventry	0, sync_invalid, 32		// Synchronous 32-bit EL0
	kernel_ventry	0, irq_invalid, 32		// IRQ 32-bit EL0
	kernel_ventry	0, fiq_invalid, 32		// FIQ 32-bit EL0
	kernel_ventry	0, error_invalid, 32		// Error 32-bit EL0
END(vectors)

kernel_ventry 值得说一下 el\()\el\()_\label\()表示一个符号的结尾,el 然后是结尾\() ,接着\el 这个就是传入宏第一个的参数,然后是结尾\(),然后是_,在跟着\label也就是第二个参数

vectors 就是个位置,依次放着各个异常的入口,只要依次实现这些入口函数,CPU发生异常的时候就会进入对应的处理函
数。

el0_sync_invalid为例,现在先忽略kernel_entry,这个是用来现场保护的,最后会到bad_mode中进行异常的处理

/*
 * Invalid mode handlers
 */
	.macro	inv_entry, el, reason, regsize = 64
	bl kernel_entry
	mov	x0, sp
	mov	x1, #\reason
	mrs	x2, esr_el1
	b	bad_mode
	.endm
	
el0_sync_invalid:
	inv_entry 0, BAD_SYNC
ENDPROC(el0_sync_invalid)

中断现场保护和恢复

这时候就不得不说中断发生时的现场保护了
与异常不同最后需要恢复现场

el1_irq:
    bl kernel_entry
    bl irq_handle
    bl kernel_exit
ENDPROC(el1_irq)

一个线框的定义是

struct pt_regs {
	struct {
		u64 regs[31];
		u64 sp;
		u64 pc;
		u64 pstate;
	};
	u64 orig_x0;
#ifdef __AARCH64EB__
	u32 unused2;
	s32 syscallno;
#else
	s32 syscallno;
	u32 unused2;
#endif

	u64 orig_addr_limit;
	u64 unused; // maintain 16 byte alignment
	u64 stackframe[2];
};

主要就是32个寄存器和SP、PC、PSTATE,所以只要按照这个顺序依次保存就可以

// 保护 pt_regs
kernel_entry:
	//开辟空间
    sub sp, sp, #S_FRAME_SIZE

    // 保存普通寄存器
    stp	x0, x1, [sp, #16 * 0]
	stp	x2, x3, [sp, #16 * 1]
	stp	x4, x5, [sp, #16 * 2]
	stp	x6, x7, [sp, #16 * 3]
	stp	x8, x9, [sp, #16 * 4]
	stp	x10, x11, [sp, #16 * 5]
	stp	x12, x13, [sp, #16 * 6]
	stp	x14, x15, [sp, #16 * 7]
	stp	x16, x17, [sp, #16 * 8]
	stp	x18, x19, [sp, #16 * 9]
	stp	x20, x21, [sp, #16 * 10]
	stp	x22, x23, [sp, #16 * 11]
	stp	x24, x25, [sp, #16 * 12]
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]

	//保存最开始sp的位置到x21 
    add x21, sp, #S_FRAME_SIZE

    mrs x22, elr_el1
    mrs x23, spsr_el1

    stp lr, x21, [sp, #S_LR]
    stp x22, x23, [sp, #S_PC]
    ret

stp 会依次保存两个寄存器到 sp+第二个数的位置

接下来就是恢复时候

// 恢复 pt_regs
kernel_exit:
    // 先把pc 和 pstate恢复
    ldp x22, x23, [sp, #S_PC]
    msr	elr_el1, x22			// set up the return data
	msr	spsr_el1, x23

    ldp	x0, x1, [sp, #16 * 0]
	ldp	x2, x3, [sp, #16 * 1]
	ldp	x4, x5, [sp, #16 * 2]
	ldp	x6, x7, [sp, #16 * 3]
	ldp	x8, x9, [sp, #16 * 4]
	ldp	x10, x11, [sp, #16 * 5]
	ldp	x12, x13, [sp, #16 * 6]
	ldp	x14, x15, [sp, #16 * 7]
	ldp	x16, x17, [sp, #16 * 8]
	ldp	x18, x19, [sp, #16 * 9]
	ldp	x20, x21, [sp, #16 * 10]
	ldp	x22, x23, [sp, #16 * 11]
	ldp	x24, x25, [sp, #16 * 12]
	ldp	x26, x27, [sp, #16 * 13]
	ldp	x28, x29, [sp, #16 * 14]

    //最后再恢复lr 和 之前的sp
	ldr	lr, [sp, #S_LR]
	add	sp, sp, #S_FRAME_SIZE		// restore sp   
    eret

就是保存的逆操作,最后通过 eret 返回到elr_el1指向的位置

GIC-400

基本介绍

上面就已经把异常发生时候的软件部分部分说完了,接下来就是控制中断的GIC-400驱动的实现了。

GIC支持的中断有:

  • SGI 软件中断
  • PPI 私有外设中断
  • SPI 共享外设中断,只有SPI可以设置分发的CPU

GIC是中断控制器,分为分发器(dist) 和 CPU接口

从GIC角度来看,一个中断发生过程

  1. 当GIC 检测到一个中断发生时,会将该中断状态从inactive状态标记为pending状态。
  2. 对于处在penging状态的中断,分发器会确定目标 CPU, 将中断请求发给这个CPU。
  3. 对于每个 CPU, 分发器会从众多处于等待状态的中断中选择一个优先级最高的中断,发送到目标CPU 的 CPU 接口。
  4. CPU 接口会决定这个中断是否可以发送给CPU, 如果这个中断的优先级满足要求,GIC 会发送一个中断请求信号给 CPU.
  5. CPU 进入中断异常, 读取 GICC_IAR 来响应该中断(一般是由 Linux 内核的中断处理程序来读寄存器)。寄存器会返回硬件中断号(hardware interrupt ID)。对于 SGI 来说,返回源CPU 的ID (source processor ID) 。当GIC感知到软件读取了该寄存器后,根据如下情况处理。
    1. 如果该中断源处于pending 状态,则将该中断状态切换到 active 状态
    2. 如果该中断又重新产生,那么该中断状态则变成 active and pending 状态
  6. 如果该中断正在忙,正在处理其他中断, 则该中断状态其切换为 active and pending 状态,等待CPU将当前当前的中断处理结束之后,再将该中断切换到 active 状态
  7. 处理器完成中断服务,发送一个完成信号结束中断(End of Interupt,EOI) 给 GIC。该中断状态再切换到 inactive 状态。

从CPU来看

  1. 接受到一个中断信号,进入中断向量表
  2. 执行gic driver中的中断handle函数
  3. 读取GICC_IAR得到原因,执行对应的处理函数

寄存器描述

GIC-400 分发器寄存器 (Distributor Registers)

偏移地址 (Hex) 寄存器名 中文名 类型 位域描述 (位宽: 32-bit)
0x000 GICD_CTLR 分发器控制寄存器 RW - [0]: 全局中断转发使能
- [1]: Group1 中断使能
- [2]: Group0 中断使能
0x004 GICD_TYPER 分发器类型寄存器 RO - [4:0]: 支持的中断数(ITLinesNumber = N/32 -1)
- [7:5]: CPU 数量 -1
- [10:8]: 共享中断数(LSPI)
0x008 GICD_IIDR 分发器标识寄存器 RO - [31:0]: 厂商和版本信息
0x080 GICD_IGROUPRn 中断组寄存器 RW 每 bit 对应一个中断:
- 0: Group0(安全)
- 1: Group1(非安全)
0x100 GICD_ISENABLERn 中断使能寄存器 RW 每 bit 对应一个中断:
- 1: 使能中断
0x180 GICD_ICENABLERn 中断禁用寄存器 RW 每 bit 对应一个中断:
- 1: 禁用中断
0x400 GICD_IPRIORITYRn 中断优先级寄存器 RW 每中断占 8 位:
- [7:0]: 优先级(值越低优先级越高)
0x800 GICD_ITARGETSRn 中断目标 CPU 寄存器 RW 每中断占 8 位:
- [7:0]: 目标 CPU 掩码(每 bit 对应一个 CPU)
0xC00 GICD_ICFGRn 中断配置寄存器 RW 每中断占 2 位:
- 00: 电平触发
- 01: 边沿触发

CPU 接口寄存器 (CPU Interface Registers)如下

偏移地址 (Hex) 寄存器名 中文名 类型 位域描述 (位宽: 32-bit)
0x0000 GICC_CTLR CPU 控制寄存器 RW - [0]: CPU 接口使能
- [1]: Group0 FIQ 旁路
- [2]: Group1 IRQ 旁路
0x0004 GICC_PMR 优先级屏蔽寄存器 RW - [7:0]: 优先级阈值(仅高 4 位有效)
0x0008 GICC_BPR 二进制点寄存器 RW - [2:0]: 优先级分组值
0x000C GICC_IAR 中断应答寄存器 RO - [9:0]: 中断 ID
- [12:10]: 源 CPU ID
0x0010 GICC_EOIR 中断结束寄存器 WO - [9:0]: 结束中断的 ID
0x0014 GICC_RPR 运行优先级寄存器 RO - [7:0]: 当前中断优先级
0x0018 GICC_HPPIR 最高挂起中断寄存器 RO - [9:0]: 最高优先级挂起中断 ID
0x001C GICC_ABPR 别名二进制点寄存器 RW - [2:0]: Group0 二进制点值
0x00D0 GICC_DIR 停用中断寄存器 WO - [9:0]: 停用中断 ID(虚拟化扩展)

说明

  1. 偏移地址:相对基地址(如 GICD_BASEGICC_BASE)。
  2. 寄存器数组(如 GICD_IGROUPRn):每个寄存器管理 32 个中断(例如 n=0 对应中断 0-31)。
  3. 优先级:实际有效位数由实现决定(例如 4 位或 8 位)。
  4. 目标 CPU 掩码:例如 0x01 表示 CPU0,0x03 表示 CPU0 和 CPU1。
  5. GICD_ITARGETSRn用来控制中断号的目标CPU,每八位描述一个中断号,前32个中断号(GICD_ITARGETSR0-7)是只读的,只有SPI可以配置到哪个CPU

GIC初始化

gic的结构体如下

struct gic_chip_data {
	u64 raw_dist_base;
	u64 raw_cpu_base;
	struct irq_domain *domain;
	struct irq_chip *chip;
	u32 gic_irqs;
};

#define gic_dist_base(d) ((d)->raw_dist_base)
#define gic_cpu_base(d) ((d)->raw_cpu_base)

初始化函数,可以对照上面的寄存器看一下所需要的寄存器

int gic_init(int chip, u32 dist_base, u32 cpu_base)
{
	printk("gic init ...\n");
	struct gic_chip_data *gic;

	gic = &gic_data[chip];
	gic->raw_dist_base = dist_base;
	gic->raw_cpu_base = cpu_base;

	u32 irq_num = (readl(gic_dist_base(gic) + GIC_DIST_TYPER) & 0x1f);
	irq_num = (irq_num + 1) * 32;
	gic->gic_irqs = irq_num;
	printk("cpu_base:0x%x, dist_base:0x%x, gic_irqs:%d\n",
	       gic_dist_base(gic), gic_cpu_base(gic), gic->gic_irqs);

	gic_dist_init(gic);
	gic_cpu_init(gic);

	return 0;
}
static void gic_dist_init(struct gic_chip_data *gic)
{
	u64 base = gic_dist_base(gic);
	writel(GICD_ENABLE, base + GIC_DIST_CTRL);

	u32 cpu_mask = gic_get_cpumask(gic);
	cpu_mask |= cpu_mask << 8;
	cpu_mask |= cpu_mask << 16;

	u32 gic_irqs = gic->gic_irqs;
	s32 i = 0;
	/* 将SPI都配置成路由到和前32个中断一样的CPU */
	for (i = 32; i < gic_irqs; i += 4)
		writel(cpu_mask, base + GIC_DIST_TARGET + i * 4 / 4);

	/* 将所以的SPI都设置成电平触发,低电平有效 */
	for (i = 32; i < gic_irqs; i += 16)
		writel(GICD_INT_ACTLOW_LVLTRIG, base + GIC_DIST_CONFIG + i / 4);

	/* Deactivate and disable all 中断(SGI, PPI, SPI).
	 *
	 * 当注册中断的时候才 enable某个一个SPI中断,例如调用gic_unmask_irq()*/
	for (i = 0; i < gic_irqs; i += 32) {
		writel(GICD_INT_EN_CLR_X32,
		       base + GIC_DIST_ACTIVE_CLEAR + i / 8);
		writel(GICD_INT_EN_CLR_X32,
		       base + GIC_DIST_ENABLE_CLEAR + i / 8);
	}

	/*打开SGI中断(0~15),可能SMP会用到*/
	writel(GICD_INT_EN_SET_SGI, base + GIC_DIST_ENABLE_SET);

	/* 打开中断:Enable group0 interrupt forwarding.*/
	writel(GICD_ENABLE, base + GIC_DIST_CTRL);
}

static void gic_cpu_init(struct gic_chip_data *gic)
{
	int i;
	unsigned long base = gic_cpu_base(gic);
	unsigned long dist_base = gic_dist_base(gic);

	/*
	 * Set priority on PPI and SGI interrupts
	 */
	for (i = 0; i < 32; i += 4)
		writel(0xa0a0a0a0, dist_base + GIC_DIST_PRI + i * 4 / 4);

	writel(GICC_INT_PRI_THRESHOLD, base + GIC_CPU_PRIMASK);

	writel(GICC_ENABLE, base + GIC_CPU_CTRL);
}

注册一个中断

void timer_init(void)
{
// 初始化所需要的寄存器
	generic_timer_init();
	generic_timer_reset(val);

// 打开GIC对应的irq
	gic_set_irq(GENERIC_TIMER_IRQ);

// 使能
	enable_timer_interrupt();
}

只需要看如何打开就好,其他的是这个timer的寄存器,现在聚焦在gic的配置

void gic_set_irq(u32 irq)
{
	u32 n = irq / 32;
	u32 mask = 1 << (irq % 32);

	writel(mask, get_gic_dist_base() + GIC_DIST_ENABLE_SET + 4 * n);
}

GICD_ISENABLERn是一位对应一个中断号

响应中断

中断向量表:

// entry.S
el1_irq:
    bl kernel_entry
    bl irq_handle
    bl kernel_exit
ENDPROC(el1_irq)
// irq.c
void irq_handle(void)
{
	gic_handle_irq();
}
// gic_v2.c
void gic_handle_irq(void)
{
	u64 cpu_base = get_gic_cpu_base();
	u32 irqstat, irqnr;

	do {
		irqstat = readl(cpu_base + GIC_CPU_INTACK);
		irqnr = irqstat & GICC_IAR_INT_ID_MASK;

		if (irqnr == GENERIC_TIMER_IRQ)
			handle_timer_irq();

		gicv2_eoi_irq(irqnr);

	} while (0);
}
posted @ 2025-02-06 01:35  LIalan  阅读(12)  评论(0编辑  收藏  举报