2、设备树基础语法:节点、属性、标签、路径

好,咱们今天聊聊设备树的基础语法。说实话,我刚接触设备树那会儿,看着满屏的.dts文件,心里直犯嘀咕——这玩意儿跟C语言长得也太不一样了。但用久了你会发现,它其实挺简单的,就是一套描述硬件的“说明书”。

2.1 节点(Node)——设备树的“骨架”

节点是什么?说白了就是硬件设备在软件里的“化身”。每个节点代表一个硬件设备或总线,比如一个I2C控制器、一个GPIO引脚、一个串口。

节点的写法很简单:

/ {
    cpu {
        compatible = "arm,cortex-a7";
        reg = <0x00 0x100>;
    };

    i2c0 {
        compatible = "snps,designware-i2c";
        reg = <0x1000 0x100>;
    };
};

看到没?每个节点用花括号{}包起来,最后加分号。根节点是/,所有设备都挂在它下面。我刚开始写的时候,经常忘记加分号,编译报错半天找不到原因——嗯,这种低级错误我犯过不止一次。

节点命名规则:

  • 节点名一般用设备类型+序号,比如i2c0uart1
  • 可以包含字母、数字、连字符、下划线
  • 不能以数字开头,不能包含空格

2.2 属性(Property)——节点的“血肉”

节点只有骨架不行,还得有血肉。属性就是描述节点具体信息的键值对。我习惯把属性分成三类:

属性类型 示例 说明
字符串 compatible = "snps,designware-i2c"; 用于匹配驱动
整数 reg = <0x1000 0x100>; 地址和大小
布尔 status = "okay"; 启用或禁用设备

这里有个坑,我当年踩过:compatible属性的值,顺序很重要!内核匹配驱动时,会按顺序尝试。如果你把通用的写在前面,专用的写在后面,可能会匹配到错误的驱动。我曾经因为这个,让一个I2C设备在某个板子上死活不工作,查了两天才发现是compatible顺序搞反了。

我的小技巧:写compatible时,把最具体的型号放最前面,通用的放后面。比如:"ti,lm75", "national,lm75"。这样内核会优先匹配你的专用驱动。

2.3 标签(Label)——给节点起个“外号”

标签是什么?说白了就是给节点起个外号。你想想看,如果节点名很长,比如i2c@ff010000,每次引用都要写全名,多累啊。标签就是用来解决这个问题的。

i2c0: i2c@ff010000 {
    compatible = "snps,designware-i2c";
    reg = <0xff010000 0x100>;
};

&i2c0 {
    clock-frequency = <400000>;
};

看到i2c0:了吗?这就是标签。后面用&i2c0就能引用这个节点。我个人习惯给所有重要的节点都加上标签,方便后续扩展。比如你要在另一个文件里给这个I2C控制器加个时钟频率,直接&i2c0就搞定了。

注意:标签名不能重复!整个设备树里标签必须是唯一的。我见过有人把两个节点都叫i2c0,编译时直接报错。嗯,这种低级错误,新手期难免。

2.4 路径(Path)——设备树的“地址”

路径就是从根节点到某个节点的完整“地址”。比如:

/soc/i2c@ff010000/rtc@68

这个路径表示:根节点下的soc节点,里面的i2c@ff010000节点,再里面的rtc@68节点。路径用斜杠分隔,跟文件系统很像。

路径在哪儿用?最常见的是在chosen节点里指定内核启动参数:

/ {
    chosen {
        stdout-path = &uart0;
    };
};

这里&uart0其实就是路径的简写形式。内核会把控制台输出重定向到UART0。我记得有次调试一个板子,串口死活没输出,最后发现是stdout-path写错了路径——嗯,这种问题排查起来特别费时间。

2.5 综合示例——一个完整的设备树片段

来,咱们看一个实际项目中用到的例子:

/dts-v1/;

/ {
    model = "MyBoard V1.0";
    compatible = "mycompany,myboard", "arm,vexpress";

    chosen {
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;

        uart0: serial@ff010000 {
            compatible = "snps,dw-apb-uart";
            reg = <0xff010000 0x100>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
            status = "okay";
        };

        i2c0: i2c@ff020000 {
            compatible = "snps,designware-i2c";
            reg = <0xff020000 0x100>;
            interrupts = <0 34 4>;
            status = "okay";

            rtc@68 {
                compatible = "dallas,ds1307";
                reg = <0x68>;
            };
        };
    };
};

这个例子包含了咱们今天讲的所有概念:

  • 节点:/chosenmemorysocuart0i2c0rtc
  • 属性:compatiblereginterruptsstatus
  • 标签:uart0:i2c0:
  • 路径:&uart0就是/soc/serial@ff010000的简写

避坑指南:我曾经在一个项目里,把reg属性的地址写成了32位,但实际硬件地址是64位的。结果内核访问外设时,地址被截断,系统直接崩溃。所以写reg时,一定要确认#address-cells#size-cells的值对不对。

好了,这一章的内容就到这儿。设备树的基础语法其实不复杂,就是节点、属性、标签、路径这四个概念。你只要记住:节点是硬件,属性是参数,标签是外号,路径是地址。下一章咱们聊聊设备树如何与驱动代码交互,那才是真正有意思的地方。