3. 中断控制器初始化:GIC Distributor & Redistributor

中断控制器,说白了就是CPU和外部中断源之间的“总机接线员”。在Jacinto平台上,我们面对的是ARM GICv3或GICv2。我个人习惯把GIC初始化拆成两大部分:Distributor(分发器)和Redistributor(重分发器)。搞懂这两个家伙,中断管理就拿下了一半。

3.1 GIC Distributor初始化流程

Distributor负责全局中断的使能、优先级分组和路由。初始化它,我一般按下面几步走:

  1. 关闭Distributor:写GICD_CTLR寄存器,把Enable位清0。配置期间必须关掉,否则会出乱子。
  2. 设置中断分组:GICD_IGROUPRn寄存器决定每个中断是Group 0(安全)还是Group 1(非安全)。
  3. 配置优先级:GICD_IPRIORITYRn寄存器,每个中断8位优先级字段。值越小优先级越高。
  4. 设置触发方式:GICD_ICFGRn寄存器,配置边沿触发还是电平触发。
  5. 路由到目标PE:GICD_IROUTERn寄存器(GICv3特有),指定中断发往哪个CPU核心。
  6. 打开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。

使能一个中断的典型流程:

  1. 在Distributor端使能:写GICD_ISENABLERn对应位。
  2. 在Redistributor端使能:写GICR_ISENABLER0(PPI/SGI)或GICR_ISENABLER1(其他)。
  3. 在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初始化的完整流程:

GIC初始化流程总览 GIC初始化流程总览 第一阶段:Distributor初始化 关闭Distributor 配置中断分组 配置优先级 设置触发 路由到PE 打开Distributor 第二阶段:Redistributor初始化(每个CPU核心) 唤醒Redistributor 配置PPI优先级 配置SGI优先级 设置PPI触发 使能Redistributor 第三阶段:使能具体中断(ISENABLER + CPU接口)

嗯,这里要注意:Distributor和Redistributor的初始化顺序不能颠倒。我曾经在一个多核项目中,先初始化了Redistributor再配Distributor,结果CPU1上的PPI中断一直不触发。查了两天才发现,Redistributor在Distributor没配好之前,根本收不到任何中断。

总结一下:GIC初始化就是三步走——Distributor全局配置、Redistributor本地配置、最后使能具体中断。每一步都有对应的寄存器族,搞混了就会出问题。我个人建议在写代码前,先画一张寄存器映射表,对照着写,不容易出错。