第四节:设备树中的pinctrl描述

好,咱们接着往下聊。上一节我们把pinctrl子系统的内部机制梳理了一遍,这一节就聚焦到设备树里,看看pinctrl到底是怎么描述的。

说实话,我刚接触设备树那会儿,看到pinctrl相关的节点也是一头雾水。什么pinctrl-0、pinctrl-names,还有那一堆pinmux、pinconf的配置,感觉就像在看天书。后来做项目踩了几个坑,才慢慢摸清楚门道。

4.1 pinctrl节点定义

先说说pinctrl节点本身。在设备树里,pinctrl节点通常放在soc节点下面,或者单独作为一个子节点。它的作用就是描述SoC内部那些pin controller的硬件信息。

我习惯这么写:

pinctrl: pinctrl@10000000 {
    compatible = "vendor,soc-pinctrl";
    reg = <0x10000000 0x1000>;
    #pinctrl-cells = <2>;
    
    /* 子节点:定义具体的引脚功能组 */
    uart0_pins: uart0-pins {
        function = "uart0";
        groups = "uart0_tx", "uart0_rx";
    };
    
    i2c0_pins: i2c0-pins {
        function = "i2c0";
        pins = "GPIO0", "GPIO1";
        bias-pull-up;
        drive-strength = <2>;
    };
};

这里有几个关键点:

  • compatible:匹配驱动用的,必须和驱动里写的名字一致
  • reg:pin controller的寄存器基地址和大小
  • #pinctrl-cells:这个很重要,表示每个引脚描述需要几个cell。常见的是2个,一个表示引脚号,一个表示功能选择

核心要点:pinctrl节点本身不直接配置引脚,它只是提供一个"模板仓库"。真正的配置是在使用这些引脚的外设节点里引用的。

4.2 pinctrl状态

为什么要有pinctrl状态?说白了,一个外设在不同的工作场景下,引脚配置可能不一样。

举个例子,我做过一个项目,SD卡接口在正常工作时需要高速模式,引脚驱动能力要强;但在休眠时,为了省电,引脚要改成弱驱动甚至关闭。这就涉及到不同状态的切换。

设备树里用pinctrl-names来定义状态名称,用pinctrl-0、pinctrl-1来对应具体的配置:

&mmc0 {
    pinctrl-names = "default", "sleep", "idle";
    pinctrl-0 = <&mmc0_default_pins>;
    pinctrl-1 = <&mmc0_sleep_pins>;
    pinctrl-2 = <&mmc0_idle_pins>;
};

内核里,驱动可以通过pinctrl_select_state()来切换状态。比如在系统休眠时,内核会自动调用pinctrl_select_state(dev, "sleep")。

我的经验:状态名称最好用"default"、"sleep"、"active"这种语义明确的词。我曾经见过有人用"state0"、"state1",结果三个月后自己都忘了哪个是干嘛的。

4.3 pinctrl绑定

绑定,就是把pinctrl配置"挂"到具体的外设节点上。这个过程其实很简单,就是在你的外设节点里加上pinctrl-names和pinctrl-X属性。

来看一个完整的例子:

&uart0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
};

这里&uart0_pins就是我们在pinctrl节点里定义的那个子节点。内核在初始化uart0驱动时,会去解析这个绑定,然后调用pinctrl子系统来配置引脚。

嗯,这里要注意一点:pinctrl-0可以引用多个配置节点,用空格隔开就行:

pinctrl-0 = <&pinmux_uart0 &pinconf_uart0>;

这样做的目的是把mux功能和conf功能分开定义,方便复用。我个人比较推荐这种写法,清晰明了。

4.4 pinctrl配置示例

光说不练假把式,咱们来看一个完整的实战配置。假设我们要配置一个I2C控制器,引脚是GPIO0和GPIO1,需要上拉、驱动强度为4mA。

第一步,在pinctrl节点里定义引脚组:

pinctrl: pinctrl@10000000 {
    compatible = "vendor,soc-pinctrl";
    reg = <0x10000000 0x1000>;
    #pinctrl-cells = <2>;
    
    i2c0_pins_a: i2c0-pins-a {
        pins = "GPIO0", "GPIO1";
        function = "i2c0";
        bias-pull-up;
        drive-strength = <4>;
        slew-rate = <1>;
    };
    
    i2c0_pins_b: i2c0-pins-b {
        pins = "GPIO2", "GPIO3";
        function = "i2c0";
        bias-pull-up;
        drive-strength = <2>;
    };
};

第二步,在I2C控制器节点里引用:

&i2c0 {
    status = "okay";
    pinctrl-names = "default", "alt";
    pinctrl-0 = <&i2c0_pins_a>;
    pinctrl-1 = <&i2c0_pins_b>;
    clock-frequency = <100000>;
};

你看,这里我定义了两套引脚配置。默认用GPIO0/1,备用方案用GPIO2/3。这在做硬件兼容设计时特别有用。

避坑指南:我曾经在一个项目里,把两个外设的pinctrl配置写成了同一个引脚组,结果两个设备同时工作时,引脚功能冲突,系统直接死机。排查了整整两天才发现问题。所以一定要检查引脚是否被复用。

4.5 知识体系总览

为了让你更直观地理解pinctrl在设备树中的整体结构,我画了一张图:

设备树pinctrl配置体系 设备树根节点 pinctrl@10000000 (pin controller) compatible, reg, #pinctrl-cells uart0_pins function, groups, bias i2c0_pins_a pins, function, drive-strength mmc0_default_pins pins, function, slew-rate &uart0 pinctrl-0 = <&uart0_pins> &i2c0 pinctrl-0 = <&i2c0_pins_a> &mmc0 pinctrl-0/1/2 (多状态) 绑定关系:外设节点 → pinctrl子节点 → 引脚配置 内核解析设备树 → 调用pinctrl驱动 → 配置寄存器

从这张图可以看得很清楚:pinctrl节点是"配置仓库",外设节点通过pinctrl-0等属性去"取货"。内核在启动时,会按照这个绑定关系,一步步把引脚配置到寄存器里。

好了,关于设备树中的pinctrl描述,核心内容就是这些。记住三个关键词:定义、状态、绑定。搞懂了这三个,设备树这块你就拿下了大半。

总结一下:pinctrl节点定义引脚功能组,pinctrl状态管理不同场景的配置,pinctrl绑定把配置挂到外设上。三者缺一不可。


可可精品资料库