本文以树莓派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, vectorsmsr vbar_el1, x0
其中vector的实现是参考linux的实现
.macro kernel_ventry, el, label, regsize=64.align 7sub sp, sp, #S_FRAME_SIZEb el\()\el\()_\label.endm.pushsection ".entry.text", "ax".align 11
ENTRY(vectors)kernel_ventry 1, sync_invalid // Synchronous EL1tkernel_ventry 1, irq_invalid // IRQ EL1tkernel_ventry 1, fiq_invalid // FIQ EL1tkernel_ventry 1, error_invalid // Error EL1tkernel_ventry 1, sync_invalid // Synchronous EL1hkernel_ventry 1, irq // IRQ EL1hkernel_ventry 1, fiq_invalid // FIQ EL1hkernel_ventry 1, error_invalid // Error EL1hkernel_ventry 0, sync_invalid // Synchronous 64-bit EL0kernel_ventry 0, irq_invalid // IRQ 64-bit EL0kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0kernel_ventry 0, error_invalid // Error 64-bit EL0kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0kernel_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 = 64bl kernel_entrymov x0, spmov x1, #\reasonmrs x2, esr_el1b bad_mode.endmel0_sync_invalid:inv_entry 0, BAD_SYNC
ENDPROC(el0_sync_invalid)
中断现场保护和恢复
这时候就不得不说中断发生时的现场保护了
与异常不同最后需要恢复现场
el1_irq:bl kernel_entrybl irq_handlebl 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;
#elses32 syscallno;u32 unused2;
#endifu64 orig_addr_limit;u64 unused; // maintain 16 byte alignmentu64 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_SIZEmrs x22, elr_el1mrs x23, spsr_el1stp 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 datamsr spsr_el1, x23ldp 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 和 之前的spldr 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) |
---|---|---|---|---|
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(虚拟化扩展) |
说明
- 偏移地址:相对基地址(如
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;
}
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对应的irqgic_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_entrybl irq_handlebl 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);
}