3. 中断控制器初始化:GIC Distributor & Redistributor
中断控制器,说白了就是CPU和外部中断源之间的“总机接线员”。在Jacinto平台上,我们面对的是ARM GICv3或GICv2。我个人习惯把GIC初始化拆成两大部分:Distributor(分发器)和Redistributor(重分发器)。搞懂这两个家伙,中断管理就拿下了一半。
3.1 GIC Distributor初始化流程
Distributor负责全局中断的使能、优先级分组和路由。初始化它,我一般按下面几步走:
- 关闭Distributor:写GICD_CTLR寄存器,把Enable位清0。配置期间必须关掉,否则会出乱子。
- 设置中断分组:GICD_IGROUPRn寄存器决定每个中断是Group 0(安全)还是Group 1(非安全)。
- 配置优先级:GICD_IPRIORITYRn寄存器,每个中断8位优先级字段。值越小优先级越高。
- 设置触发方式:GICD_ICFGRn寄存器,配置边沿触发还是电平触发。
- 路由到目标PE:GICD_IROUTERn寄存器(GICv3特有),指定中断发往哪个CPU核心。
- 打开Distributor:再次写GICD_CTLR,置位Enable。
核心要点:Distributor初始化必须在所有Redistributor初始化之前完成。顺序错了,中断可能根本收不到。
来看一段实际的初始化代码片段:
void gic_distributor_init(void)
{
uint32_t i;
uint32_t gicd_base = GICD_BASE_ADDR;
// 1. 关闭Distributor
writel(0x0, gicd_base + GICD_CTLR);
// 2. 将所有中断设为Group 1(非安全)
for (i = 0; i < GICD_IGROUPR_NUM; i++) {
writel(0xFFFFFFFF, gicd_base + GICD_IGROUPR + i * 4);
}
// 3. 设置默认优先级(0xA0,中等优先级)
for (i = 0; i < GICD_IPRIORITYR_NUM; i++) {
writel(0xA0A0A0A0, gicd_base + GICD_IPRIORITYR + i * 4);
}
// 4. 所有中断设为电平触发
for (i = 0; i < GICD_ICFGR_NUM; i++) {
writel(0x0, gicd_base + GICD_ICFGR + i * 4);
}
// 5. 路由到CPU0(GICv3)
for (i = 32; i < 1020; i++) {
writel(0x0, gicd_base + GICD_IROUTER + i * 8);
}
// 6. 打开Distributor
writel(0x1, gicd_base + GICD_CTLR);
// 内存屏障,确保配置生效
dsb();
}
避坑指南:我曾经在GICv2上直接用了GICv3的IROUTER写法,结果中断全跑到CPU1上去了。记得先确认GIC版本,再决定用哪个寄存器族。
3.2 Redistributor初始化
Redistributor是每个CPU核心私有的。它负责处理本地中断(PPI、SGI)和来自Distributor的中断。初始化Redistributor,我建议按这个顺序:
- 使能Redistributor帧:写GICR_WAKER寄存器,清除ProcessorSleep位,等待ChildrenAsleep位清零。
- 配置PPI优先级:GICR_IPRIORITYR寄存器,每个PPI中断独立设置。
- 配置SGI优先级:GICR_IPRIORITYR中对应SGI的字段。
- 设置PPI触发方式:GICR_ICFGR0(SGI固定边沿触发,不用配),GICR_ICFGR1配PPI。
- 使能Redistributor:写GICR_CTLR,置位Enable。
注意:Redistributor初始化必须在每个CPU核心上分别执行。你想想看,如果只在CPU0上配了,CPU1上的PPI中断根本不会响应。
代码示例:
void gic_redistributor_init(void)
{
uint32_t gicr_base = get_redist_base(); // 获取当前CPU的Redistributor基址
// 1. 唤醒Redistributor
writel(readl(gicr_base + GICR_WAKER) & ~(1 << 1), gicr_base + GICR_WAKER);
while (readl(gicr_base + GICR_WAKER) & (1 << 2)); // 等待唤醒完成
// 2. 配置PPI优先级(ID 16-31)
for (int i = 0; i < 4; i++) {
writel(0xA0A0A0A0, gicr_base + GICR_IPRIORITYR + i * 4);
}
// 3. PPI设为电平触发
writel(0x0, gicr_base + GICR_ICFGR1);
// 4. 使能Redistributor
writel(0x1, gicr_base + GICR_CTLR);
dsb();
}
3.3 中断使能与屏蔽
中断使能分两级:全局使能和个体使能。全局使能在CPU接口(GICD_CTLR/GICR_CTLR),个体使能靠GICD_ISENABLER/GICD_ICENABLER。
使能一个中断的典型流程:
- 在Distributor端使能:写GICD_ISENABLERn对应位。
- 在Redistributor端使能:写GICR_ISENABLER0(PPI/SGI)或GICR_ISENABLER1(其他)。
- 在CPU接口使能:写ICC_IGRPEN1_EL1(非安全中断)。
屏蔽则反过来,用GICD_ICENABLERn和GICR_ICENABLER0/1。
| 操作 | Distributor寄存器 | Redistributor寄存器 | CPU接口寄存器 |
|---|---|---|---|
| 使能 | GICD_ISENABLERn | GICR_ISENABLER0/1 | ICC_IGRPEN1_EL1 |
| 屏蔽 | GICD_ICENABLERn | GICR_ICENABLER0/1 | ICC_IGRPEN0_EL1 |
| 状态查询 | GICD_ISENABLERn(读回) | GICR_ISENABLER0/1(读回) | 读ICC_IGRPEN1_EL1 |
个人经验:我习惯在使能中断前,先屏蔽所有中断,然后逐个使能。这样可以避免中断在配置过程中意外触发。曾经有个项目,因为没先屏蔽,中断在配置优先级时触发,导致优先级还没设好就进了ISR,系统直接挂掉。
3.4 初始化流程总览
下面这张图总结了GIC初始化的完整流程:
嗯,这里要注意:Distributor和Redistributor的初始化顺序不能颠倒。我曾经在一个多核项目中,先初始化了Redistributor再配Distributor,结果CPU1上的PPI中断一直不触发。查了两天才发现,Redistributor在Distributor没配好之前,根本收不到任何中断。
总结一下:GIC初始化就是三步走——Distributor全局配置、Redistributor本地配置、最后使能具体中断。每一步都有对应的寄存器族,搞混了就会出问题。我个人建议在写代码前,先画一张寄存器映射表,对照着写,不容易出错。