从源码分析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捕获到异常时
[*]将PSTATE保存到对应的SPSR_ELx中
[*]返回地址保存到ELR_ELx中
[*]PSTATE DAIF关闭
[*]如果是同步异常把原因写入ESR_ELx,如果是中断,原因保存在GIC-400的寄存器中
[*]切换SP到对应的SP_ELx中
[*]跳转到中断向量表里
[*]执行对应的处理函数
CPU处理完异常执行eret,会
[*]ELR中恢复PC
[*]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;
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;
};主要就是32个寄存器和SP、PC、PSTATE,所以只要按照这个顺序依次保存就可以
// 保护 pt_regs
kernel_entry:
//开辟空间
sub sp, sp, #S_FRAME_SIZE
// 保存普通寄存器
stp x0, x1,
stp x2, x3,
stp x4, x5,
stp x6, x7,
stp x8, x9,
stp x10, x11,
stp x12, x13,
stp x14, x15,
stp x16, x17,
stp x18, x19,
stp x20, x21,
stp x22, x23,
stp x24, x25,
stp x26, x27,
stp x28, x29,
//保存最开始sp的位置到x21
add x21, sp, #S_FRAME_SIZE
mrs x22, elr_el1
mrs x23, spsr_el1
stp lr, x21,
stp x22, x23,
retstp 会依次保存两个寄存器到 sp+第二个数的位置
接下来就是恢复时候
// 恢复 pt_regs
kernel_exit:
// 先把pc 和 pstate恢复
ldp x22, x23,
msr elr_el1, x22 // set up the return data
msr spsr_el1, x23
ldp x0, x1,
ldp x2, x3,
ldp x4, x5,
ldp x6, x7,
ldp x8, x9,
ldp x10, x11,
ldp x12, x13,
ldp x14, x15,
ldp x16, x17,
ldp x18, x19,
ldp x20, x21,
ldp x22, x23,
ldp x24, x25,
ldp x26, x27,
ldp x28, x29,
//最后再恢复lr 和 之前的sp
ldr 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接口
https://img2024.cnblogs.com/blog/3038414/202502/3038414-20250206013238083-1457678318.png
从GIC角度来看,一个中断发生过程
[*]当GIC 检测到一个中断发生时,会将该中断状态从inactive状态标记为pending状态。
[*]对于处在penging状态的中断,分发器会确定目标 CPU, 将中断请求发给这个CPU。
[*]对于每个 CPU, 分发器会从众多处于等待状态的中断中选择一个优先级最高的中断,发送到目标CPU 的 CPU 接口。
[*]CPU 接口会决定这个中断是否可以发送给CPU, 如果这个中断的优先级满足要求,GIC 会发送一个中断请求信号给 CPU.
[*]CPU 进入中断异常, 读取 GICC_IAR 来响应该中断(一般是由 Linux 内核的中断处理程序来读寄存器)。寄存器会返回硬件中断号(hardware interrupt ID)。对于 SGI 来说,返回源CPU 的ID (source processor ID) 。当GIC感知到软件读取了该寄存器后,根据如下情况处理。
[*]如果该中断源处于pending 状态,则将该中断状态切换到 active 状态
[*]如果该中断又重新产生,那么该中断状态则变成 active and pending 状态
[*]如果该中断正在忙,正在处理其他中断, 则该中断状态其切换为 active and pending 状态,等待CPU将当前当前的中断处理结束之后,再将该中断切换到 active 状态
[*]处理器完成中断服务,发送一个完成信号结束中断(End of Interupt,EOI) 给 GIC。该中断状态再切换到 inactive 状态。
从CPU来看
[*]接受到一个中断信号,进入中断向量表
[*]执行gic driver中的中断handle函数
[*]读取GICC_IAR得到原因,执行对应的处理函数
寄存器描述
GIC-400 分发器寄存器 (Distributor Registers)
偏移地址 (Hex)寄存器名中文名类型位域描述 (位宽: 32-bit)0x000GICD_CTLR分发器控制寄存器RW- : 全局中断转发使能
- : Group1 中断使能
- : Group0 中断使能0x004GICD_TYPER分发器类型寄存器RO- : 支持的中断数(ITLinesNumber = N/32 -1)
- : CPU 数量 -1
- : 共享中断数(LSPI)0x008GICD_IIDR分发器标识寄存器RO- : 厂商和版本信息0x080GICD_IGROUPRn中断组寄存器RW每 bit 对应一个中断:
- 0: Group0(安全)
- 1: Group1(非安全)0x100GICD_ISENABLERn中断使能寄存器RW每 bit 对应一个中断:
- 1: 使能中断0x180GICD_ICENABLERn中断禁用寄存器RW每 bit 对应一个中断:
- 1: 禁用中断0x400GICD_IPRIORITYRn中断优先级寄存器RW每中断占 8 位:
- : 优先级(值越低优先级越高)0x800GICD_ITARGETSRn中断目标 CPU 寄存器RW每中断占 8 位:
- : 目标 CPU 掩码(每 bit 对应一个 CPU)0xC00GICD_ICFGRn中断配置寄存器RW每中断占 2 位:
- 00: 电平触发
- 01: 边沿触发CPU 接口寄存器 (CPU Interface Registers)如下
偏移地址 (Hex)寄存器名中文名类型位域描述 (位宽: 32-bit)0x0000GICC_CTLRCPU 控制寄存器RW- : CPU 接口使能
- : Group0 FIQ 旁路
- : Group1 IRQ 旁路0x0004GICC_PMR优先级屏蔽寄存器RW- : 优先级阈值(仅高 4 位有效)0x0008GICC_BPR二进制点寄存器RW- : 优先级分组值0x000CGICC_IAR中断应答寄存器RO- : 中断 ID
- : 源 CPU ID0x0010GICC_EOIR中断结束寄存器WO- : 结束中断的 ID0x0014GICC_RPR运行优先级寄存器RO- : 当前中断优先级0x0018GICC_HPPIR最高挂起中断寄存器RO- : 最高优先级挂起中断 ID0x001CGICC_ABPR别名二进制点寄存器RW- : Group0 二进制点值0x00D0GICC_DIR停用中断寄存器WO- : 停用中断 ID(虚拟化扩展)说明
[*]偏移地址:相对基地址(如 GICD_BASE 或 GICC_BASE)。
[*]寄存器数组(如 GICD_IGROUPRn):每个寄存器管理 32 个中断(例如 n=0 对应中断 0-31)。
[*]优先级:实际有效位数由实现决定(例如 4 位或 8 位)。
[*]目标 CPU 掩码:例如 0x01 表示 CPU0,0x03 表示 CPU0 和 CPU1。
[*]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;
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
页:
[1]