4. 设备树基础语法(三):中断控制器、GPIO控制器、时钟控制器的设备树绑定
好,咱们接着聊设备树。前两章我们把设备树的基本骨架搭起来了,也聊了 reg 和 compatible 这些核心属性。今天要讲的这三个家伙——中断控制器、GPIO 控制器、时钟控制器——可以说是嵌入式系统里的「三大件」。你想想看,一个 SoC 要正常工作,哪个能少得了它们?
我个人习惯把这三者称为「资源调度三兄弟」。它们负责把芯片内部的中断线、GPIO 引脚、时钟信号,有条理地分配给各个外设。设备树里怎么描述它们?说白了,就是一套标准化的「接线图」。
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 需要几个 cellgpio-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—— 指定描述一个时钟输出需要几个 cellclock-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 控制器)就会觉得似曾相识了。