本文以树莓派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[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角度来看,一个中断发生过程
- 当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- [0]: 全局中断转发使能
- [1]: Group1 中断使能
- [2]: Group0 中断使能0x004GICD_TYPER分发器类型寄存器RO- [4:0]: 支持的中断数(ITLinesNumber = N/32 -1)
- [7:5]: CPU 数量 -1
- [10:8]: 共享中断数(LSPI)0x008GICD_IIDR分发器标识寄存器RO- [31:0]: 厂商和版本信息0x080GICD_IGROUPRn中断组寄存器RW每 bit 对应一个中断:
- 0: Group0(安全)
- 1: Group1(非安全)0x100GICD_ISENABLERn中断使能寄存器RW每 bit 对应一个中断:
- 1: 使能中断0x180GICD_ICENABLERn中断禁用寄存器RW每 bit 对应一个中断:
- 1: 禁用中断0x400GICD_IPRIORITYRn中断优先级寄存器RW每中断占 8 位:
- [7:0]: 优先级(值越低优先级越高)0x800GICD_ITARGETSRn中断目标 CPU 寄存器RW每中断占 8 位:
- [7:0]: 目标 CPU 掩码(每 bit 对应一个 CPU)0xC00GICD_ICFGRn中断配置寄存器RW每中断占 2 位:
- 00: 电平触发
- 01: 边沿触发CPU 接口寄存器 (CPU Interface Registers)如下
偏移地址 (Hex)寄存器名中文名类型位域描述 (位宽: 32-bit)0x0000GICC_CTLRCPU 控制寄存器RW- [0]: CPU 接口使能
- [1]: Group0 FIQ 旁路
- [2]: Group1 IRQ 旁路0x0004GICC_PMR优先级屏蔽寄存器RW- [7:0]: 优先级阈值(仅高 4 位有效)0x0008GICC_BPR二进制点寄存器RW- [2:0]: 优先级分组值0x000CGICC_IAR中断应答寄存器RO- [9:0]: 中断 ID
- [12:10]: 源 CPU ID0x0010GICC_EOIR中断结束寄存器WO- [9:0]: 结束中断的 ID0x0014GICC_RPR运行优先级寄存器RO- [7:0]: 当前中断优先级0x0018GICC_HPPIR最高挂起中断寄存器RO- [9:0]: 最高优先级挂起中断 ID0x001CGICC_ABPR别名二进制点寄存器RW- [2:0]: Group0 二进制点值0x00D0GICC_DIR停用中断寄存器WO- [9:0]: 停用中断 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[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;
- }
复制代码 [code]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 |