第四节:根节点与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属性是设备树与内核驱动之间的“握手协议”。内核通过它找到对应的驱动,驱动通过它知道自己该干什么。
匹配规则详解
内核的匹配过程,我画了个图,方便你理解:
匹配规则其实就一句话:设备树提供兼容列表,驱动提供匹配列表,内核取交集。
具体到代码层面,驱动里是这样写的:
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命名规范:厂商,型号 → 厂商,系列 → 厂商,架构
- 不要为了兼容而牺牲正确性,该分开就分开