1. 设备树基础回顾:设备树起源、语法结构、节点与属性、dts/dtsi/dtb文件关系

1.1 设备树的起源——为什么要有它?

说实话,我刚入行那会儿,Linux内核里还没有设备树这东西。那时候板级文件满天飞,每个ARM平台都有自己的board-xxx.c,里面密密麻麻全是硬件寄存器地址、中断号、GPIO映射。你想想看,换一个Flash型号,就得改C代码、重新编译内核。这哪是写驱动,简直是给硬件做硬编码。

设备树(Device Tree)的出现,说白了就是要把硬件描述从内核代码里剥离出来。它最早源于PowerPC架构,后来被ARM社区大力推广。我记得2011年左右,ARM Linux主线开始强制要求使用设备树,那会儿不少老工程师都在抱怨——但回过头看,这步棋走得太对了。

核心思想:用一套独立的数据结构来描述硬件,内核在启动时解析它,动态生成平台设备。硬件变了,改dts文件就行,不用动内核代码。

1.2 语法结构——树是怎么长出来的?

设备树的语法,其实特别直观。它就是一个倒挂的树,根节点是/,下面挂各种总线、外设。每个节点代表一个硬件设备或总线,节点里放属性来描述它的特征。

我习惯把设备树想象成一张「硬件清单」:

  • 节点(node):就是清单上的一个条目,比如「UART0」、「I2C控制器」
  • 属性(property):条目的具体内容,比如地址0x10000000、中断号33
  • 值(value):属性的具体数值,可以是32位整数、字符串、字节数组

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "MyBoard V1.0";
    compatible = "vendor,myboard";

    cpu@0 {
        device_type = "cpu";
        compatible = "arm,cortex-a9";
        reg = <0>;
    };

    uart@10000000 {
        compatible = "ns16550";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
    };
};

嗯,这里要注意:@后面的数字是地址,用来区分同类型设备。比如两个UART,可以写成uart@10000000uart@20000000。这个地址不一定是物理地址,但通常我们会用它来对应。

1.3 节点与属性——核心要素详解

节点和属性,是设备树里最基础的两个概念。我在项目中遇到过不少新手,把节点名和属性名搞混,结果内核死活认不出设备。

节点命名规则:

  • 格式:node-name@unit-address
  • node-name:描述设备类型,比如uart、i2c、spi
  • unit-address:设备的地址,通常是寄存器基址

常用属性一览:

属性名 含义 示例
compatible 设备兼容性字符串,驱动匹配的关键 "vendor,device"
reg 寄存器地址和长度 <0x10000000 0x1000>
interrupts 中断号、触发类型 <0 33 4>
status 设备状态,okaydisabled "okay"
clocks 时钟源引用 <&clk_uart0>

我的小技巧:compatible属性时,建议遵循「厂商,设备型号」的格式。比如"ti,am335x-uart"。这样驱动匹配时,可以做到精确匹配,避免误认。

1.4 dts、dtsi、dtb——三兄弟的关系

这三个文件,我经常被问到有什么区别。其实特别简单:

  • .dts:设备树源文件,你手写的硬件描述
  • .dtsi:设备树头文件,公共部分放这里,用#include引用
  • .dtb:编译后的二进制文件,内核启动时直接加载

为什么要有.dtsi?你想想看,一个SoC可能有几十种开发板,但CPU核心、内存控制器、中断控制器这些是共用的。如果每个板子都写一遍,那跟以前的板级文件有什么区别?

所以,我们通常这样组织:

// am335x.dtsi —— SoC公共部分
#include "skeleton.dtsi"

/ {
    compatible = "ti,am335x";
    
    ocp {
        uart0: serial@44e09000 {
            compatible = "ti,am335x-uart";
            reg = <0x44e09000 0x2000>;
            interrupts = <72>;
            status = "disabled";
        };
    };
};

// myboard.dts —— 具体板级配置
#include "am335x.dtsi"

/ {
    model = "MyBoard AM335x";
    
    &uart0 {
        status = "okay";
        pinctrl-0 = <&uart0_pins>;
    };
};

编译命令也很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

曾经踩过的坑:我曾经在项目中,把dtsi里的status = "disabled"忘了,结果所有板子都默认开启了某个外设,导致功耗超标。排查了两天才发现。所以,SoC公共部分的外设,我建议默认都disabled,板级文件里再逐个okay

1.5 设备树在内核中的生命周期

设备树从源文件到被内核使用,大致走这么几步:

  1. 编译阶段dtc.dts编译成.dtb
  2. 启动阶段:Bootloader(如U-Boot)把.dtb加载到内存,把地址传给内核
  3. 内核解析:内核在启动早期调用unflatten_device_tree(),把二进制数据解析成树状结构
  4. 设备创建:内核遍历设备树,为每个compatible匹配的节点创建平台设备
  5. 驱动绑定:驱动通过of_match_table匹配节点,获取资源并初始化

我个人觉得,理解这个生命周期特别重要。很多驱动问题,其实不是代码写错了,而是设备树没被正确加载。比如我曾经遇到一个案子,U-Boot传的dtb地址不对,内核解析出来全是乱码,设备一个都没认到。

1.6 小结

设备树不是什么高深的技术,它就是一个硬件描述语言。你只要记住三点:

  • 节点描述设备,属性描述特征
  • dts是源文件,dtsi是公共头文件,dtb是编译产物
  • compatible是驱动匹配的钥匙,写错了设备就认不出来

嗯,基础就这些。后面的章节,我们会深入到性能优化和资源管理,到时候你会发现——设备树写得好不好,直接影响到系统的启动速度和内存占用。准备好了吗?我们继续往下走。