4. 设备树实战:GPIO控制器绑定、时钟控制器绑定、pinctrl子系统绑定、中断控制器绑定
设备树这东西,说白了就是硬件的“户口本”。内核启动时靠它来认设备、配资源。今天咱们就把它最核心的四个绑定讲透——GPIO、时钟、pinctrl、中断。这四个绑定了,你基本就能搞定90%的嵌入式设备树。
核心观点:设备树绑定的本质,就是告诉内核“这个设备用哪个引脚、吃多少时钟、怎么配置IO、中断挂在哪”。四个绑定缺一不可,顺序错了也不行。
4.1 GPIO控制器绑定——管脚资源的“身份证”
GPIO绑定,我习惯把它分成两层:控制器节点和使用者节点。控制器节点描述硬件本身,使用者节点描述谁在用哪个引脚。
4.1.1 控制器节点怎么写
先看一个典型的GPIO控制器节点:
gpio0: gpio@ff730000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xff730000 0x0 0x100>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_gpio0>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
这里有几个关键点:
- gpio-controller:声明这是个GPIO控制器,没有这个属性,内核不认。
- #gpio-cells = <2>:表示每个GPIO引用需要2个参数——引脚号和标志位。
- interrupt-controller:GPIO还能当中断控制器用,这个后面细说。
我的经验:#gpio-cells的值一定要和驱动匹配。我见过有人写3,驱动却只解析2,结果引脚号全错位,灯死活不亮。查了两天才发现是cells数对不上。
4.1.2 使用者节点怎么引用
设备要使用GPIO,通过gpios或xxx-gpios属性引用:
&led {
compatible = "gpio-leds";
led-red {
gpios = <&gpio0 RK_PA5 GPIO_ACTIVE_LOW>;
label = "red-led";
};
};
这里RK_PA5是引脚号,GPIO_ACTIVE_LOW是标志位。标志位常用值:
| 宏定义 | 值 | 含义 |
|---|---|---|
| GPIO_ACTIVE_HIGH | 0 | 高电平有效 |
| GPIO_ACTIVE_LOW | 1 | 低电平有效 |
| GPIO_OPEN_DRAIN | 2 | 开漏输出 |
| GPIO_OPEN_SOURCE | 3 | 开源输出 |
注意:GPIO_ACTIVE_LOW不是把电平拉低,而是告诉驱动“这个引脚低电平时表示有效状态”。我曾经在项目里搞反了,按键按下是低电平,我配成ACTIVE_HIGH,结果中断一直触发,CPU根本睡不了觉。
4.2 时钟控制器绑定——设备的“心跳”
时钟绑定比GPIO复杂一些,因为时钟有父子关系、频率计算、使能控制。我个人习惯先把时钟树理清楚,再写设备树。
4.2.1 时钟提供者
时钟控制器节点示例:
clk_core: clock-controller@ff760000 {
compatible = "rockchip,rk3399-cru";
reg = <0x0 0xff760000 0x0 0x1000>;
#clock-cells = <1>;
#reset-cells = <1>;
};
#clock-cells = <1>表示每个时钟引用需要1个参数,通常是时钟ID。这个ID定义在驱动头文件里,比如clk-rk3399.h。
4.2.2 时钟消费者
设备怎么要时钟?看这个:
&uart0 {
compatible = "rockchip,rk3399-uart";
reg = <0x0 0xff180000 0x0 0x100>;
clocks = <&clk_core SCLK_UART0>, <&clk_core PCLK_UART0>;
clock-names = "baudclk", "apb_pclk";
};
clock-names很重要,驱动通过名字找时钟,而不是靠顺序。我建议你养成写名字的习惯,不然驱动升级时顺序一变,代码就崩了。
避坑指南:我曾经在项目里只配了一个时钟,结果UART波特率死活不对。后来发现UART需要两个时钟:一个给波特率发生器,一个给APB总线。少一个,数据就乱码。所以配时钟前,一定先看datasheet里设备需要几个时钟域。
4.3 pinctrl子系统绑定——引脚的“多面手”
pinctrl是设备树里最灵活也最容易出错的部分。它负责把引脚配置成GPIO、UART、I2C等功能。说白了,就是引脚的“角色切换器”。
4.3.1 控制器节点
&pinctrl {
compatible = "rockchip,rk3399-pinctrl";
rockchip,grf = <&grf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
uart0 {
uart0_xfer: uart0-xfer {
rockchip,pins =
<2 RK_PB4 RK_FUNC_1 &pcfg_pull_up>,
<2 RK_PB5 RK_FUNC_1 &pcfg_pull_up>;
};
};
};
每个引脚配置包含4个要素:bank号、引脚号、功能号、上下拉配置。功能号RK_FUNC_1表示复用功能1,具体对应什么功能要看芯片手册。
4.3.2 设备引用pinctrl
&uart0 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&uart0_xfer>;
pinctrl-1 = <&uart0_sleep>;
};
pinctrl-names定义状态名,pinctrl-0对应default状态。系统在设备初始化时自动应用default状态,休眠时切换到sleep状态。这个机制对低功耗设计特别有用。
低功耗技巧:在sleep状态里把不用的引脚配成模拟输入或下拉,能省不少电。我做过一个项目,光靠优化pinctrl的sleep配置,待机电流从5mA降到了0.8mA。
4.4 中断控制器绑定——事件的“快递员”
中断绑定是设备树里最讲究“层级关系”的部分。一个设备的中断可能经过多个控制器才到达CPU。
4.4.1 中断控制器节点
&gic {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0xfe000000 0x0 0x10000>;
};
#interrupt-cells = <3>表示每个中断描述需要3个参数:中断类型、中断号、触发方式。
4.4.2 中断消费者
&i2c0 {
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 57 IRQ_TYPE_LEVEL_HIGH>;
touch@38 {
interrupt-parent = <&gpio0>;
interrupts = <RK_PA5 IRQ_TYPE_EDGE_FALLING>;
};
};
这里有个关键点:中断可以级联。I2C控制器挂在GIC上,触摸屏挂在GPIO0上。GPIO0本身也是中断控制器,它收到触摸屏的中断后,再通过自己的中断线报告给GIC。
我曾经踩过的坑:写触摸屏设备树时,忘了指定interrupt-parent,结果中断一直往GIC送。GIC收到的是GPIO的bank中断号,不是触摸屏的引脚中断号。驱动根本不知道是谁触发了中断。所以,子设备的中断父节点一定要指向它的直接中断控制器,不能偷懒。
4.5 四个绑定的协同关系
这四个绑定不是孤立的。一个典型的设备节点可能同时用到它们:
&spi1 {
compatible = "rockchip,rk3399-spi";
reg = <0x0 0xff1c0000 0x0 0x1000>;
interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_core SCLK_SPI1>, <&clk_core PCLK_SPI1>;
clock-names = "spiclk", "apb_pclk";
pinctrl-names = "default";
pinctrl-0 = <&spi1_clk &spi1_cs0 &spi1_mosi &spi1_miso>;
cs-gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;
};
这个节点同时绑定了中断、时钟、pinctrl和GPIO。内核初始化时会按顺序解析:先配时钟,再设pinctrl,然后注册中断,最后才调用驱动的probe函数。
4.6 调试技巧与常见错误
写设备树难免出错,我总结几个高频问题:
- GPIO号对不上:检查bank号和引脚号是否从0开始计数,不同厂商的编码方式可能不同。
- 时钟找不到:确认
clock-names和驱动里devm_clk_get用的名字一致。 - pinctrl不生效:检查
rockchip,pins里的bank号、引脚号、功能号是否和datasheet一致。 - 中断不触发:先确认
interrupt-parent指向正确,再看触发类型是否匹配硬件。
我的调试习惯:在驱动probe函数里加dev_info打印,把获取到的GPIO号、时钟频率、中断号都打出来。对比设备树里的值,一眼就能看出哪里配错了。这招帮我省了无数查datasheet的时间。
嗯,四个绑定讲完了。记住一个原则:设备树是给内核看的“说明书”,不是给编译器看的“代码”。写的时候多想想“内核看到这个属性会怎么解析”,很多问题就能避免。