2、设备树基础:设备树语法入门、节点与属性、设备树编译与反编译、设备树在Linux启动中的作用

2.1 设备树到底是个啥?

说实话,我刚接触嵌入式Linux那会儿,对设备树也是一头雾水。那时候还在用老式的板级文件(board-xxx.c),每次换个外设都得改内核源码,重新编译,麻烦得要命。

设备树(Device Tree)说白了,就是一套描述硬件信息的“说明书”。它用文本文件的形式,告诉内核:你的CPU是什么、内存多大、I2C总线上挂了哪些设备、GPIO怎么配置……等等。内核启动时读这份说明书,就知道该怎么初始化硬件了。

我个人习惯把设备树比作“硬件的身份证”。你想想看,没有身份证,别人怎么知道你是谁?内核也一样,没有设备树,它连你板子上有啥外设都不知道。

2.2 设备树语法入门

设备树的语法其实挺简单的。它用的是树形结构,根节点是“/”,下面挂各种子节点。每个节点代表一个硬件设备或总线。

来看个最基本的例子:

/dts-v1/;

/ {
    compatible = "my_company,my_board";
    #address-cells = <1>;
    #size-cells = <1>;

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

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
    };
};

这里有几个关键点:

  • /dts-v1/:版本声明,告诉编译器这是DTS v1格式
  • compatible:兼容性字符串,内核用它来匹配驱动
  • reg:地址和大小信息,格式是 <地址 长度>
  • chosen:特殊节点,用来传启动参数
我的小技巧:写compatible字符串时,建议用“厂商,型号”的格式。比如“ti,am335x”或“fsl,imx6q”。这样内核驱动匹配时更精准,不容易出乱子。

2.3 节点与属性详解

节点是设备树的基本单元。每个节点可以有多个属性,属性就是键值对。常见的属性类型有:

属性类型 示例 说明
字符串 compatible = "ti,am335x"; 单个字符串
字符串列表 compatible = "ti,am335x", "ti,omap"; 多个字符串,用逗号分隔
32位整数 reg = <0x48000000 0x1000>; 尖括号括起来
64位整数 reg = <0x00000000 0x80000000>; 两个32位拼一个64位
布尔值 status = "disabled"; 用字符串表示,常见"okay"和"disabled"

节点命名也有讲究。我建议用“设备名@地址”的格式。比如:

i2c@48000000 {
    compatible = "ti,omap4-i2c";
    reg = <0x48000000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

这里i2c@48000000表示I2C控制器,地址是0x48000000。它下面挂了一个eeprom,地址是0x50。这种层级关系,内核解析起来非常直观。

注意:我曾经在项目里犯过一个低级错误——把reg的地址写成了十进制。结果内核死活认不出设备。后来查了半天才发现,设备树里地址默认是十六进制,但如果你写成十进制,编译器不会报错,只是地址对不上。嗯,这种坑踩过一次就记住了。

2.4 设备树编译与反编译

设备树源文件(.dts)不能直接被内核使用,得编译成二进制格式(.dtb)。反过来,有时候拿到一个dtb文件,想看看里面是啥,就得反编译。

编译和反编译的工具是dtc(Device Tree Compiler)。用法很简单:

# 编译:dts -> dtb
dtc -I dts -O dtb -o my_board.dtb my_board.dts

# 反编译:dtb -> dts
dtc -I dtb -O dts -o my_board.dts my_board.dtb

参数说明:

  • -I:输入格式(dts或dtb)
  • -O:输出格式
  • -o:输出文件名

还有个常用选项是-@,用来生成符号表。如果你要做设备树插件(DT Overlay),这个选项必须加。

避坑指南:我曾经在调试一个I2C设备时,反复修改dts文件,每次都要手动编译、复制到板子上、重启。后来发现可以用fdtdump命令直接查看dtb内容,省了不少事。命令是:fdtdump my_board.dtb | less

2.5 设备树在Linux启动中的作用

Linux启动时,设备树扮演了“硬件向导”的角色。整个过程大致是这样的:

  1. Bootloader加载dtb:U-Boot或别的bootloader从存储介质(Flash、SD卡等)读取dtb文件,放到内存里。
  2. 传递dtb地址给内核:Bootloader通过寄存器或参数传递dtb的内存地址。
  3. 内核解析dtb:内核启动早期,会解析dtb,构建出设备树结构体(device_node树)。
  4. 驱动匹配:内核遍历设备树,根据compatible属性找到对应的驱动,然后调用驱动的probe函数。
  5. 设备初始化:驱动读取设备树中的reg、interrupts等属性,完成硬件初始化。

你想想看,如果没有设备树,内核就得靠板级文件里硬编码的地址和配置来初始化硬件。换一块板子就得改内核源码,那得多麻烦?

我记得有一次调试一个音频驱动,死活不出声。后来用ls /proc/device-tree/一看,发现设备树里音频节点的reg地址写错了。改过来重新编译,声音就出来了。所以说,设备树是调试硬件问题的第一站。

调试小技巧:在Linux启动时,加上earlycondtb=...参数,可以提前看到设备树解析的日志。如果设备树有问题,这里会直接报错,省得你后面猜来猜去。

2.6 小结

设备树是嵌入式Linux开发的必修课。语法不难,但细节很多。我建议你从简单的板子开始,自己写一个dts文件,编译、加载、调试,走一遍完整的流程。遇到问题别怕,多用dtcfdtdump工具看看,慢慢就熟练了。

下一章我们会深入讲时钟和复位节点的设计,那是设备树里最让人头疼的部分之一。做好准备,咱们接着干!