4. 设备树基础语法(三):中断控制器、GPIO控制器、时钟控制器的设备树绑定

好,咱们接着聊设备树。前两章我们把设备树的基本骨架搭起来了,也聊了 reg 和 compatible 这些核心属性。今天要讲的这三个家伙——中断控制器、GPIO 控制器、时钟控制器——可以说是嵌入式系统里的「三大件」。你想想看,一个 SoC 要正常工作,哪个能少得了它们?

我个人习惯把这三者称为「资源调度三兄弟」。它们负责把芯片内部的中断线、GPIO 引脚、时钟信号,有条理地分配给各个外设。设备树里怎么描述它们?说白了,就是一套标准化的「接线图」。

设备树资源绑定 中断控制器 interrupt-controller GPIO 控制器 gpio-controller 时钟控制器 clock-controller #interrupt-cells #gpio-cells #clock-cells 每个控制器都需要声明自己「有多少个 cell 来描述资源」 外设节点通过 interrupts / gpios / clocks 属性引用

4.1 中断控制器绑定

先说说中断控制器。这东西在设备树里怎么描述?核心就两样东西:一个是声明自己「我是中断控制器」,另一个是告诉别人「我每个中断需要几个 cell 来描述」。

中断控制器的标准属性:

  • interrupt-controller —— 空属性,声明节点是中断控制器
  • #interrupt-cells —— 指定描述一个中断需要几个 32 位整数
  • interrupt-parent —— 指定该节点连接到哪个上一级中断控制器

举个例子,一个典型的 ARM GIC(通用中断控制器)在设备树里长这样:

gic: interrupt-controller@f0001000 {
    compatible = "arm,cortex-a9-gic";
    reg = <0xf0001000 0x1000>,
          <0xf0000100 0x0100>;
    interrupt-controller;
    #interrupt-cells = <3>;
};

这里 #interrupt-cells = <3> 是什么意思?GIC 用三个 cell 来描述一个中断:第一个 cell 表示中断类型(0 是 SPI,1 是 PPI),第二个 cell 是中断号,第三个 cell 是触发方式(电平触发、边沿触发等)。

我的经验: 我在项目中遇到过好几次因为 #interrupt-cells 写错导致中断不工作的情况。比如某款 SoC 的 GPIO 控制器只支持两个 cell,但我在 dts 里写了三个,结果内核直接报错。嗯,这种问题排查起来还挺费时间的。

外设节点怎么用中断?通过 interrupts 属性:

uart0: serial@f0002000 {
    compatible = "ns16550a";
    reg = <0xf0002000 0x1000>;
    interrupt-parent = <&gic>;
    interrupts = <0 33 4>;
};

这里 interrupt-parent 指向 GIC 节点,interrupts 的三个值分别对应 SPI 类型、中断号 33、高电平触发。

注意: 如果系统里只有一个中断控制器,可以不用在每个节点里写 interrupt-parent,在根节点定义 interrupt-parent 即可。但如果有多个中断控制器(比如 GIC + GPIO 中断),每个外设必须明确指定自己的父中断控制器。

4.2 GPIO 控制器绑定

GPIO 控制器的绑定思路和中断控制器非常像。也是先声明身份,再定义 cell 数量。

GPIO 控制器的标准属性:

  • gpio-controller —— 空属性,声明节点是 GPIO 控制器
  • #gpio-cells —— 指定描述一个 GPIO 需要几个 cell
  • gpio-ranges —— (可选)描述 GPIO 引脚和 pin controller 的映射关系

一个典型的 GPIO 控制器节点:

gpio0: gpio@f0003000 {
    compatible = "snps,dw-apb-gpio";
    reg = <0xf0003000 0x1000>;
    gpio-controller;
    #gpio-cells = <2>;
    ngpios = <32>;
};

这里 #gpio-cells = <2> 表示用两个 cell 来描述一个 GPIO:第一个 cell 是引脚号(0~31),第二个 cell 是标志位(比如 GPIO_ACTIVE_HIGH 或 GPIO_ACTIVE_LOW)。

外设怎么引用 GPIO?通过 gpios 或 [name]-gpios 属性:

leds {
    compatible = "gpio-leds";
    led-red {
        label = "red";
        gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
    };
};

这里 GPIO_ACTIVE_HIGH 是一个宏定义,展开后就是 0。如果是低电平有效,就用 GPIO_ACTIVE_LOW(值为 1)。

避坑指南: 我曾经在一个项目里把 gpios 写成了 gpio,少了那个 's'。结果内核编译不报错,但驱动就是拿不到 GPIO 资源。查了两天才发现是属性名写错了。记住,标准属性名是 gpios,带 s 的。

4.3 时钟控制器绑定

时钟控制器稍微复杂一点,因为时钟树往往有层级关系。一个 SoC 里可能有多个时钟源(晶振、PLL),然后通过时钟门控、分频器生成各种频率的时钟。

时钟控制器的标准属性:

  • clock-controller —— 空属性,声明节点是时钟控制器
  • #clock-cells —— 指定描述一个时钟输出需要几个 cell
  • clock-output-names —— (可选)给每个时钟输出起个名字

一个简单的固定频率时钟:

osc24M: oscillator {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;
};

这里 #clock-cells = <0> 表示这个时钟源只有一个输出,不需要额外参数。外设引用时直接写 &osc24M 就行。

复杂一点的时钟控制器,比如全志 SoC 的 CCU(时钟控制单元):

ccu: clock@f0004000 {
    compatible = "allwinner,sun8i-h3-ccu";
    reg = <0xf0004000 0x400>;
    clocks = <&osc24M>;
    clock-names = "hosc";
    #clock-cells = <1>;
    #reset-cells = <1>;
};

这里 #clock-cells = <1> 表示需要用一个 cell 来指定具体的时钟 ID。比如 UART 模块要引用时钟:

uart0: serial@f0002000 {
    compatible = "snps,dw-apb-uart";
    reg = <0xf0002000 0x1000>;
    interrupts = <0 33 4>;
    clocks = <&ccu 6>;
    clock-names = "baudclk";
};

这里的 &ccu 6 表示引用 CCU 的 6 号时钟输出。具体 6 对应哪个时钟,要看芯片手册或者 CCU 驱动里的定义。

注意: 时钟的引用经常和复位控制器一起出现。很多 SoC 的 CCU 同时管理时钟和复位,所以你会看到 #reset-cells 属性。外设节点里用 resets = <&ccu 12> 来引用复位信号。

4.4 三者的共同模式

看到这里,你应该发现规律了。中断、GPIO、时钟这三种控制器的绑定模式几乎一模一样:

控制器类型 声明属性 cell 数量属性 外设引用属性
中断控制器 interrupt-controller #interrupt-cells interrupts
GPIO 控制器 gpio-controller #gpio-cells gpios
时钟控制器 clock-controller #clock-cells clocks

说白了,这就是一套「生产者-消费者」模型。控制器节点是生产者,声明自己能提供什么资源、每个资源需要几个参数来描述。外设节点是消费者,通过对应的属性来引用这些资源。

我的建议: 刚开始写设备树时,最容易搞混的就是 cell 的数量和含义。我个人的做法是:先找到芯片厂商提供的 .dtsi 文件,看看里面控制器节点是怎么定义的。然后对照芯片手册,确认每个 cell 的位域含义。千万别凭感觉写,否则调试起来很痛苦。

嗯,今天的内容就到这里。这三种控制器的绑定是设备树里最常用的模式之一,理解了它们,后面看其他类型的控制器(比如 DMA 控制器、PWM 控制器)就会觉得似曾相识了。

可可精品资料库