3. 设备树中断控制器:中断控制器节点、interrupt-parent、interrupts 属性与中断号映射

在嵌入式 Linux 设备树中,中断资源的描述是驱动正确响应硬件事件的基础。本章深入解析设备树中中断控制器的节点定义、interrupt-parent 与 interrupts 属性的使用规范,以及中断号从设备树到内核的映射机制。掌握这些内容,是调试 V4L2 驱动中断流程的前提。

3.1 中断控制器节点 (interrupt-controller)

中断控制器节点是设备树中描述能够接收并转发中断信号的硬件单元。一个典型的中断控制器节点必须包含以下属性:

属性 说明 是否必需
interrupt-controller 空属性,标识该节点是一个中断控制器 是
#interrupt-cells 指定该控制器使用多少个 cell 来描述一个中断 是
compatible 驱动匹配字符串 是
reg 控制器寄存器地址范围 是
interrupt-parent 指向上一级中断控制器(级联时使用) 级联时必需

以下是一个 GIC (Generic Interrupt Controller) 中断控制器节点的典型定义:

gic: interrupt-controller@f1000000 {
    compatible = "arm,gic-400";
    interrupt-controller;
    #interrupt-cells = <3>;
    reg = <0x0 0xf1000000 0x0 0x10000>,
          <0x0 0xf1001000 0x0 0x10000>;
    interrupts = <GIC_PPI 9 0xf04>;
};
关键点: #interrupt-cells 的值决定了子节点在描述中断时需要提供多少个 32 位整数。对于 GIC-400,通常使用 3 个 cell:中断类型(SPI/PPI)、中断号、触发类型与优先级标志。
中断控制器节点结构 中断控制器节点 interrupt-controller; #interrupt-cells = <3>; compatible = "arm,gic-400"; reg = <0xf1000000 ...>; interrupt-parent = <...>; 子设备节点 interrupt-parent = <&gic>; interrupts = <0 42 4>; /* SPI, IRQ 42, 高电平触发 */ 中断映射关系 子设备通过 interrupt-parent 指向中断控制器 interrupts 描述具体中断

3.2 interrupt-parent 属性

interrupt-parent 属性用于指定当前设备节点连接到哪个中断控制器。它采用 phandle 引用方式指向中断控制器节点。中断控制器可以级联形成树状结构,每个节点只需指定其直接上级中断控制器。

使用规则:

  • 如果设备节点没有显式指定 interrupt-parent,则继承其父节点的 interrupt-parent。
  • 根节点可以设置 interrupt-parent 作为整个系统的默认中断控制器。
  • 级联场景下,中断控制器本身也可以是一个中断消费者,通过自身的 interrupts 属性连接到上一级控制器。
/* 根节点设置默认中断控制器 */
/ {
    interrupt-parent = <&gic>;

    soc {
        /* 继承根节点的 interrupt-parent,连接到 gic */
        uart0: serial@10000000 {
            interrupts = <0 33 4>;
        };

        /* 显式指定不同的中断控制器 */
        gpio0: gpio@20000000 {
            interrupt-parent = <&gpio_controller>;
            interrupts = <5 2>;
        };
    };
};
常见错误: 忘记在子节点中设置 interrupt-parent,且根节点也未定义默认中断控制器,导致内核无法解析中断资源,驱动 probe 失败。调试时可通过 cat /proc/interrupts 检查中断是否注册。

3.3 interrupts 属性

interrupts 属性描述设备使用的中断资源。其格式由中断控制器的 #interrupt-cells 决定。常见的编码格式如下:

中断控制器类型 #interrupt-cells interrupts 格式 示例
GIC (ARM) 3 <中断类型 中断号 触发标志> <0 42 4>
GPIO 控制器 2 <GPIO 引脚 触发类型> <5 2>
简单中断控制器 1 <中断号> <14>

GIC 中断类型编码:

  • 0 — SPI (Shared Peripheral Interrupt),共享外设中断,中断号范围 32~1019。
  • 1 — PPI (Private Peripheral Interrupt),私有外设中断,中断号范围 16~31。
  • 2 — 扩展中断(较少使用)。

触发标志(GIC 第三个 cell):

  • 0 — 未指定。
  • 1 — 上升沿触发。
  • 2 — 下降沿触发。
  • 4 — 高电平触发。
  • 8 — 低电平触发。
/* 实际设备树中的中断描述示例 */
camera_sensor: camera@3c {
    compatible = "sony,imx219";
    reg = <0x3c>;
    interrupt-parent = <&gpio0>;
    interrupts = <10 1>;   /* GPIO0 的 pin 10,上升沿触发 */
};
interrupts 属性编码与解析流程 设备树节点 interrupts = <0 42 4>; #interrupt-cells = 3 步骤1:解析 cell cell[0] = 0 → SPI cell[1] = 42 → IRQ# cell[2] = 4 → 高电平 步骤2:映射中断号 SPI 基偏移 = 32 hwirq = 32 + 42 = 74 触发类型 = IRQF_TRIGGER_HIGH 中断号映射说明 设备树中的中断号是相对于中断控制器的硬件中断号 (hwirq),内核通过 irq_domain 将其映射为 Linux 虚拟中断号 (virq)

3.4 中断号映射机制

设备树中的中断号是硬件中断号 (hwirq),Linux 内核通过 irq_domain 机制将其映射为系统全局的虚拟中断号 (virq)。映射过程涉及以下关键步骤:

  1. 设备树解析阶段: 内核遍历设备树,遇到带有 interrupts 属性的节点时,调用 of_irq_parse_one() 解析中断描述。
  2. irq_domain 查找: 根据 interrupt-parent 找到对应的中断控制器,获取其 irq_domain。
  3. 硬件中断号转换: 调用 irq_domain_ops->xlate() 回调函数,将设备树 cell 转换为 hwirq 和触发类型。
  4. 虚拟中断号分配: 调用 irq_domain_alloc_irqs() 或 irq_create_of_mapping() 分配 virq。
  5. 中断描述符初始化: 设置中断触发类型、芯片操作函数等。
/* 内核 irq_domain 映射核心流程(伪代码) */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
    struct of_phandle_args oirq;
    int ret;

    /* 1. 解析设备树中断描述 */
    ret = of_irq_parse_one(dev, index, &oirq);
    if (ret)
        return 0;

    /* 2. 创建或查找中断映射 */
    return irq_create_of_mapping(&oirq);
}

/* GIC 的 xlate 回调示例 */
static int gic_irq_domain_xlate(struct irq_domain *d,
                                struct device_node *ctrlr,
                                const u32 *intspec, unsigned int intsize,
                                unsigned long *out_hwirq,
                                unsigned int *out_type)
{
    /* intspec[0]: 中断类型 (SPI=0, PPI=1) */
    /* intspec[1]: 中断号 */
    /* intspec[2]: 触发标志 */
    if (intspec[0] == 0) {
        /* SPI: hwirq = intspec[1] + 32 */
        *out_hwirq = intspec[1] + 32;
    } else {
        /* PPI: hwirq = intspec[1] + 16 */
        *out_hwirq = intspec[1] + 16;
    }
    *out_type = intspec[2] & IRQ_TYPE_SENSE_MASK;
    return 0;
}
调试技巧: 在驱动中使用 irq_of_parse_and_map() 获取中断号后,可以通过 cat /proc/interrupts 查看中断注册情况。如果中断号显示为 0 或负数,说明设备树中断描述解析失败,应检查 interrupt-parent 引用是否正确。

3.5 中断级联与多级映射

在实际 SoC 中,中断控制器往往形成级联结构。例如 GPIO 控制器作为中断控制器,其输出连接到 GIC 的某个 SPI 中断。此时 GPIO 控制器节点既是中断控制器,又是中断消费者。

/* 中断级联示例:GPIO 控制器连接到 GIC */
gpio0: gpio@30000000 {
    compatible = "vendor,gpio-controller";
    reg = <0x30000000 0x1000>;
    interrupt-controller;
    #interrupt-cells = <2>;

    /* GPIO 控制器本身连接到 GIC */
    interrupt-parent = <&gic>;
    interrupts = <0 100 4>;  /* SPI 100, 高电平触发 */
};

/* 外设连接到 GPIO 控制器 */
button {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;
    interrupts = <15 1>;  /* GPIO0 pin 15, 上升沿触发 */
};

上述级联场景的中断映射流程:

  • 当 GPIO pin 15 产生中断时,GPIO 控制器内部先处理,然后通过其连接到 GIC 的 SPI 100 向 CPU 上报。
  • 内核需要为 GPIO 控制器创建独立的 irq_domain,并注册 irq_chip 来处理 GPIO 子中断。
  • 驱动中调用 gpio_to_irq() 或 of_irq_get() 获取最终的虚拟中断号。
中断级联映射架构 CPU / GIC GIC (irq_domain) GPIO 控制器 irq_domain (子域) SPI 100 外设 (button) GPIO pin 15 级联映射流程 1. 外设中断 → GPIO 控制器 2. GPIO 控制器 → GIC (SPI 100) 3. GIC → CPU 4. 内核通过 irq_domain 链查找

3.6 中断映射调试方法

当设备树中断配置出现问题时,可以使用以下方法进行调试:

调试手段 命令/方法 排查目标
查看中断注册表 cat /proc/interrupts 确认中断号是否注册,中断次数是否增长
设备树验证 dtc -I dtb -O dts xxx.dtb 反编译 DTB,检查 interrupt-parent 和 interrupts 语法
内核中断调试 开启 CONFIG_DEBUG_IRQ 打印中断映射和分配日志
驱动 probe 日志 devm_request_irq() 返回值检查 确认中断申请是否成功
irq_domain 导出 ls /sys/kernel/debug/irq_domain/ 查看 irq_domain 映射关系
实战建议: 在编写 V4L2 驱动时,建议在 probe 函数中打印获取到的中断号:dev_info(dev, "irq %d mapped\n", irq);。如果中断号为 0,立即检查设备树中 interrupt-parent 的 phandle 是否正确指向中断控制器节点。