GICv2中断控制器配置:从硬件到设备树的实战解析

大家好,我是你们的嵌入式Linux讲师。今天咱们来聊聊GICv2中断控制器在设备树中的配置。说实话,中断控制器这块儿,很多工程师都觉得头疼。我刚开始接触时也踩过不少坑,尤其是地址映射搞错,系统直接起不来。嗯,今天咱们就把这事儿彻底讲明白。

GICv2的硬件架构:先搞清楚它长什么样

GICv2,全称Generic Interrupt Controller version 2,是ARM架构里最经典的中断控制器。你想想看,几乎所有的Cortex-A系列处理器都在用。它的核心部件就两个:DistributorCPU Interface

Distributor负责中断的分配、优先级管理、使能控制。CPU Interface则负责把中断信号送给具体的CPU核。说白了,Distributor是调度中心,CPU Interface是最后一公里的快递员。

我在项目中遇到过一个问题:某款芯片的GICv2地址映射文档写错了,Distributor和CPU Interface的基地址差了0x1000。结果呢?中断能注册,但从来不触发。查了三天才发现是地址问题。所以啊,地址映射一定要对着芯片手册反复核对。

设备树中的GICv2节点结构

咱们直接看一个标准的GICv2设备树节点。这是我个人习惯的写法,清晰明了:

gic: interrupt-controller@f1000000 {
    compatible = "arm,cortex-a15-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x0 0xf1000000 0x0 0x10000>,
          <0x0 0xf1001000 0x0 0x10000>;
    interrupts = <1 9 0xf04>;
};

我来拆解一下每个字段的含义:

属性 含义 我的经验
compatible 兼容性字符串,驱动匹配用 不同内核版本可能有差异,建议用"arm,cortex-a15-gic"最通用
#interrupt-cells 中断描述符的单元数,GICv2固定为3 这个值千万别改,改了内核解析会乱
interrupt-controller 标记这是一个中断控制器 空属性,但必须有
reg 地址映射,两段:Distributor和CPU Interface 地址范围要跟芯片手册完全一致
interrupts GIC自身的中断,用于级联 主GIC通常不需要,从GIC才配

重点提醒:reg属性有两段地址,第一段是Distributor,第二段是CPU Interface。地址偏移量通常是0x1000,但不同芯片可能不同。我见过某厂商把CPU Interface放在0x2000偏移处,结果内核panic了。

Distributor和CPU Interface的地址映射详解

咱们深入看看这两个部件的地址空间。Distributor的基地址通常是0x1000对齐,CPU Interface紧跟着偏移0x1000。但这不是绝对的,我建议你养成查手册的习惯。

Distributor的寄存器空间主要包含:

  • GICD_CTLR:控制器使能寄存器
  • GICD_TYPER:类型寄存器,告诉你支持多少中断
  • GICD_ISENABLERn:中断使能寄存器组
  • GICD_ICPENDRn:中断挂起清除寄存器组
  • GICD_IPRIORITYRn:中断优先级寄存器组

CPU Interface的寄存器空间则包括:

  • GICC_CTLR:CPU接口控制寄存器
  • GICC_PMR:优先级掩码寄存器
  • GICC_IAR:中断应答寄存器
  • GICC_EOIR:中断结束寄存器

我曾经调试过一个奇怪的问题:中断能触发,但ISR执行完后系统卡死。后来发现是GICC_EOIR没写对地址。CPU Interface的基地址我配错了,导致EOIR写到了Distributor的地址上。嗯,这种低级错误,犯过一次就再也不会犯了。

中断类型:SPI、PPI、SGI的配置

GICv2支持三种中断类型,咱们一个一个说。

SPI(Shared Peripheral Interrupt)

SPI是共享外设中断,中断号范围是32到1019。这是最常用的中断类型,所有CPU核都能收到。设备树中配置SPI的格式是:

interrupts = <0 42 4>;

第一个字段0表示SPI类型,第二个字段42是中断号(注意:设备树中的中断号要减去32),第三个字段是触发类型和优先级标志。

我的小技巧:SPI的中断号在设备树里写的是硬件中断号减去32。比如硬件中断号74,设备树里就写42。这个转换我刚开始也搞混过,后来干脆写了个脚本自动转换。

PPI(Private Peripheral Interrupt)

PPI是私有外设中断,中断号范围是16到31。每个CPU核独享,常用于本地定时器、性能监测等。配置格式:

interrupts = <1 13 0xf04>;

第一个字段1表示PPI类型,第二个字段13是中断号(对应硬件中断号29),第三个字段是触发类型。PPI的触发类型我建议用0xf04,这是内核推荐的默认值。

SGI(Software Generated Interrupt)

SGI是软件触发的中断,中断号范围是0到15。用于核间通信,比如IPI。SGI在设备树中通常不直接配置,而是由内核在运行时动态使用。但如果你要配置,格式是:

interrupts = <1 8 0xf04>;

第一个字段1表示PPI类型(SGI在设备树中被归类为PPI),第二个字段8是中断号(对应硬件中断号8),第三个字段是触发类型。

注意:SGI的触发类型必须是边缘触发,不能是电平触发。我曾经看到有人配成了电平触发,结果SGI发出去后CPU一直处于中断状态,根本停不下来。这个坑我替你们踩过了。

实战:配置一个完整的GICv2节点

好了,理论说完了,咱们来个完整的例子。假设你有一款四核Cortex-A15芯片,GICv2的Distributor基地址是0xf1000000,CPU Interface基地址是0xf1001000。设备树节点这样写:

gic: interrupt-controller@f1000000 {
    compatible = "arm,cortex-a15-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x0 0xf1000000 0x0 0x10000>,
          <0x0 0xf1001000 0x0 0x2000>;
    interrupts = <1 9 0xf04>;
};

然后,一个UART外设使用SPI中断,硬件中断号是74:

uart0: serial@f1010000 {
    compatible = "ns16550a";
    reg = <0x0 0xf1010000 0x0 0x1000>;
    interrupts = <0 42 4>;
    interrupt-parent = <&gic>;
};

这里interrupt-parent指向gic节点,告诉内核这个外设的中断由GIC管理。中断号42对应硬件中断号74(42+32=74),触发类型4表示高电平触发。

我个人习惯在设备树中把interrupt-parent显式写出来,虽然有些情况下可以省略,但显式写更安全,也方便别人阅读你的代码。

避坑指南:我踩过的那些坑

最后,分享几个我实际项目中遇到的坑:

  • 地址范围别写小了:Distributor的地址范围至少0x1000,CPU Interface至少0x1000。写小了内核访问寄存器时会触发异常。
  • #interrupt-cells必须是3:GICv2固定是3,别自作聪明改成2或4。内核的GIC驱动写死了这个值。
  • 中断号别重复:SPI的中断号范围是32-1019,但实际芯片可能只支持到几百。重复的中断号会导致驱动混乱。
  • 触发类型要匹配硬件:电平触发还是边缘触发,一定要看外设的数据手册。配错了中断要么不触发,要么一直触发。

我曾经在一个项目中,把某个GPIO的中断触发类型从边缘触发改成了电平触发,结果按键按一下,中断触发了十几次。嗯,从那以后我每次配中断都会先看硬件手册。

好了,GICv2的设备树配置就讲到这里。下一章咱们聊聊GICv3,那个更复杂,但原理是相通的。有什么问题欢迎在评论区交流,我看到会回复。