第四节:根节点与compatible属性

好,咱们今天聊聊设备树里最基础、也最容易让人忽略的两个东西——根节点和compatible属性。

说实话,我刚接触设备树那会儿,觉得根节点不就是个壳子嘛,随便写写就行。后来踩了坑才明白,根节点和compatible属性,其实是整个设备树和内核匹配的“第一道关卡”。

根节点的定义

每个设备树文件,有且只有一个根节点。它用 / 表示,是所有其他节点的父节点。说白了,它就是整棵树的根。

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
    #address-cells = <1>;
    #size-cells = <1>;
    
    chosen {
        stdout-path = &uart0;
    };
    
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };
    
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            compatible = "arm,cortex-a8";
            device_type = "cpu";
            reg = <0>;
        };
    };
};

根节点里一般放什么?我总结了几样东西:

  • model:板子的名字,给人看的。比如 "TI AM335x BeagleBone Black"
  • compatible:板子的兼容性标识,给内核看的。这个最重要,后面细说
  • #address-cells 和 #size-cells:定义地址和大小字段的宽度,默认是32位
  • chosen:存放引导参数,比如控制台串口
  • memory:描述板子的内存布局
  • cpus:描述CPU信息

嗯,这里要注意:根节点里不要放外设节点。外设节点应该挂在对应的总线下面,比如 ocp、axi 或者 amba 总线节点里。我见过有人把所有东西都塞根节点里,那叫一个乱。

compatible属性的作用

compatible属性,说白了就是告诉内核:“我是谁,我能兼容谁”。

它的格式是一个字符串列表,按优先级从高到低排列:

compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";

内核在匹配时,会从左到右依次尝试。先看有没有专门针对 "ti,am335x-bone-black" 的驱动,没有就试 "ti,am335x-bone",再没有就试 "ti,am33xx"。

为什么会这样设计?你想想看,一个芯片厂商可能出很多款板子,但核心SoC是一样的。如果每块板子都要写一个驱动,那太累了。所以用compatible做兼容性匹配,驱动可以写一个,支持多个板子。

核心要点:compatible属性是设备树与内核驱动之间的“握手协议”。内核通过它找到对应的驱动,驱动通过它知道自己该干什么。

匹配规则详解

内核的匹配过程,我画了个图,方便你理解:

compatible属性匹配流程 设备树节点 compatible = "vendor,board-v2" compatible = "vendor,board-v1" compatible = "vendor,soc" 内核匹配过程(从左到右) ① 尝试 "vendor,board-v2" → 有驱动吗? ② 没有?尝试 "vendor,board-v1" → 有驱动吗? ③ 还没有?尝试 "vendor,soc" → 有驱动吗? ④ 都没有?返回 -ENODEV,驱动加载失败 匹配成功 驱动 probe() 函数被调用 设备与驱动绑定,开始初始化 驱动端 of_match_table = { "vendor,board-v2", "vendor,soc", { /* sentinel */ } }; 匹配规则:设备树提供兼容列表,驱动提供匹配列表,取交集

匹配规则其实就一句话:设备树提供兼容列表,驱动提供匹配列表,内核取交集。

具体到代码层面,驱动里是这样写的:

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "ti,am335x-bone-black", },
    { .compatible = "ti,am33xx", },
    { /* sentinel */ }
};

static struct platform_driver my_driver = {
    .driver = {
        .name = "my_driver",
        .of_match_table = my_driver_of_match,
    },
    .probe = my_driver_probe,
    .remove = my_driver_remove,
};

当内核发现设备树的compatible里有 "ti,am335x-bone-black",而驱动的of_match_table里也有它,那就匹配上了。驱动probe函数被调用,开始干活。

小技巧:我个人习惯在驱动里把最精确的compatible放前面,通用的放后面。这样如果将来出了新板子,只要compatible字符串不变,驱动就能继续用。

板级兼容性设计

这块是我觉得最有意思的。什么叫板级兼容性?就是一块板子,换了个型号,但硬件改动不大,能不能共用同一个设备树?

我举个例子。假设我们公司做了两款板子:

  • Board-A:有LED、按键、UART、I2C
  • Board-B:在Board-A基础上,多了个SPI Flash,少了两个按键

如果为每块板子都写一个完整的设备树,那太浪费了。更好的做法是:

// am335x-board-common.dtsi(公共部分)
/ {
    model = "Common AM335x Board";
    compatible = "ti,am33xx";
    
    chosen {
        stdout-path = &uart0;
    };
    
    leds {
        compatible = "gpio-leds";
        led0 {
            label = "heartbeat";
            gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>;
        };
    };
    
    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
    };
};
// am335x-board-a.dts(Board-A专用)
#include "am335x-board-common.dtsi"

/ {
    model = "Board-A";
    compatible = "vendor,board-a", "ti,am33xx";
    
    keys {
        compatible = "gpio-keys";
        key0 {
            label = "SW1";
            gpios = <&gpio2 1 GPIO_ACTIVE_LOW>;
        };
        key1 {
            label = "SW2";
            gpios = <&gpio2 2 GPIO_ACTIVE_LOW>;
        };
    };
};
// am335x-board-b.dts(Board-B专用)
#include "am335x-board-common.dtsi"

/ {
    model = "Board-B";
    compatible = "vendor,board-b", "ti,am33xx";
    
    keys {
        compatible = "gpio-keys";
        key0 {
            label = "SW1";
            gpios = <&gpio2 1 GPIO_ACTIVE_LOW>;
        };
    };
    
    spi0: spi@48030000 {
        compatible = "ti,am3352-spi", "ti,omap4-spi";
        reg = <0x48030000 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;
        
        flash@0 {
            compatible = "jedec,spi-nor";
            reg = <0>;
            spi-max-frequency = <50000000>;
        };
    };
};

你看,公共部分抽到 .dtsi 文件里,不同板子只写差异部分。这样维护起来轻松多了。

避坑指南:我曾经在项目里犯过一个错误——把两个板子的compatible写反了。结果Board-A的板子加载了Board-B的设备树,SPI Flash初始化失败,系统直接挂掉。后来排查了半天才发现是compatible字符串写错了。

所以我的建议是:compatible字符串一定要和板子一一对应,不要偷懒。特别是量产阶段,搞错了就是批量返工。

compatible的命名规范

这个其实没有硬性规定,但业界有个约定俗成的格式:

compatible = "厂商,型号", "厂商,系列", "厂商,架构";

比如:

级别 示例 说明
最精确 "ti,am335x-bone-black" 具体到某个板子型号
较通用 "ti,am335x-bone" 同一系列板子
最通用 "ti,am33xx" 同一SoC系列

厂商名一般用小写,型号用连字符连接。不要用下划线,不要用大写。这是Linux内核社区的惯例。

实际项目中的经验

最后分享一个我实际项目中的案例。当时做一款工业控制板,用了NXP的i.MX6ULL芯片。板子有两个版本:

  • V1.0:DDR3 256MB,NAND Flash 256MB
  • V2.0:DDR3 512MB,NAND Flash 512MB,多了个以太网口

我一开始想偷懒,共用同一个设备树,只在uboot里改内存大小。结果发现V2.0的以太网口初始化有问题,因为设备树里没配第二个网口的引脚复用。

后来我老老实实做了两个设备树:

// imx6ull-board-v1.dts
/ {
    compatible = "vendor,imx6ull-board-v1", "fsl,imx6ull";
    memory@80000000 {
        reg = <0x80000000 0x10000000>;  // 256MB
    };
};

// imx6ull-board-v2.dts
/ {
    compatible = "vendor,imx6ull-board-v2", "fsl,imx6ull";
    memory@80000000 {
        reg = <0x80000000 0x20000000>;  // 512MB
    };
    
    &fec2 {
        pinctrl-0 = <&pinctrl_enet2>;
        phy-mode = "rmii";
        status = "okay";
    };
};

嗯,这件事给我的教训是:兼容性设计要适度,不要为了省事而牺牲正确性。该分开的就分开,该共用的就共用。

好了,根节点和compatible属性就讲这么多。记住一句话:根节点是设备树的入口,compatible是内核匹配的钥匙。这两个搞明白了,后面学其他节点就顺了。

本章核心总结:

  • 根节点用 / 表示,是设备树的唯一入口
  • compatible属性是设备树与驱动的匹配依据,按优先级从左到右
  • 板级兼容性设计用 .dtsi 抽公共部分,.dts 写差异部分
  • compatible命名规范:厂商,型号 → 厂商,系列 → 厂商,架构
  • 不要为了兼容而牺牲正确性,该分开就分开

可可精品资料库