第一章:设备树基础——什么是设备树?为什么需要设备树?设备树在嵌入式Linux中的角色

大家好,我是你们的讲师。今天咱们聊聊设备树。说实话,我刚入行那会儿,设备树还没这么普及。那时候做嵌入式Linux,板级文件(board file)满天飞,每次换个芯片型号,都得改一堆C代码。后来设备树出现了,嗯,这玩意儿确实解决了不少痛点。

1.1 什么是设备树?

设备树,英文叫Device Tree,简称DT。说白了,它就是一个描述硬件信息的数据结构。你想想看,Linux内核要跑起来,总得知道CPU是什么、内存多大、外设挂在哪里吧?设备树就是干这个的。

设备树的文件格式有两种:

  • .dts:设备树源文件(Device Tree Source),人类可读的文本格式
  • .dtb:设备树二进制文件(Device Tree Blob),编译后的二进制格式

我习惯把.dts比作C语言的源文件,.dtb就是编译后的可执行文件。U-Boot启动时,会把.dtb加载到内存,然后传给内核。

核心概念:设备树本质上是一个树形结构,根节点是"/",下面挂载各种子节点,每个节点代表一个硬件设备或总线。

来看一个最简单的设备树示例:

/dts-v1/;

/ {
    compatible = "vendor,board-name";
    #address-cells = <1>;
    #size-cells = <1>;

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB内存
    };

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

    uart0: serial@10000000 {
        compatible = "ns16550";
        reg = <0x10000000 0x1000>;
        interrupts = <0 5 4>;
        clock-frequency = <24000000>;
    };
};

这段代码描述了什么?根节点下有三个子节点:memory(内存)、chosen(启动参数)、uart0(串口)。每个节点都有属性,比如compatible告诉内核这个设备兼容哪个驱动,reg描述地址和大小。

我的经验:刚开始写设备树时,最容易搞错的是reg属性的地址范围。我在项目中遇到过,因为reg写错了,导致内核访问外设时直接挂死。嗯,调试这种问题特别痛苦,建议你写完后先用dtc工具检查语法。

1.2 为什么需要设备树?

这个问题问得好。在设备树出现之前,嵌入式Linux是怎么处理硬件的?答案是——硬编码。

早期的内核里,每个板子都有一个对应的board文件,比如arch/arm/mach-xxx/board-yyy.c。这些文件里写满了硬件初始化代码、GPIO配置、中断号映射等等。你想想看,如果内核要支持100种板子,就得有100个board文件。维护起来简直是噩梦。

设备树解决了三个核心问题:

  1. 硬件描述与内核代码分离:硬件信息放在.dts里,内核驱动通过解析设备树来获取参数。换板子只需要换.dtb,不用改内核。
  2. 减少内核代码量:Linux主线内核现在支持上千种板子,如果没有设备树,代码量会膨胀到无法维护。
  3. 标准化接口:设备树规范是标准化的,ARM、RISC-V、MIPS等架构都在用。U-Boot也支持设备树,启动时直接传递.dtb给内核。

避坑指南:我曾经接手过一个项目,前任工程师把硬件配置写死在驱动里,完全没用设备树。结果客户要换一颗Flash芯片,得改三四个文件。后来我花了一周时间把所有硬件参数迁移到设备树里,再改驱动去解析设备树。嗯,从那以后,我再也不允许团队在驱动里硬编码硬件参数了。

1.3 设备树在嵌入式Linux中的角色

设备树在嵌入式Linux启动流程中扮演着"翻译官"的角色。咱们来看看整个启动链条:

阶段 角色 设备树的作用
U-Boot 引导加载程序 加载.dtb到内存,修改或添加节点(如内存大小、MAC地址)
Linux内核 操作系统内核 解析.dtb,创建设备模型,匹配驱动
设备驱动 硬件驱动模块 通过设备树API获取硬件资源(中断号、寄存器地址、DMA通道等)

说白了,设备树就是U-Boot和内核之间的"契约"。U-Boot负责把硬件信息整理成.dtb,内核负责读懂它。我见过不少新手,在U-Boot里改了设备树,但内核没重新编译,结果启动时内核报"无法匹配设备"。其实问题很简单——U-Boot传的.dtb和内核期望的不一致。

设备树在Linux内核中的角色可以概括为三点:

  • 硬件探测:内核启动时,通过设备树知道有哪些设备、在哪里、怎么配置。
  • 驱动匹配:每个设备节点都有compatible属性,驱动通过这个属性来匹配自己该管哪个设备。
  • 资源分配:中断、DMA、时钟、GPIO等资源,都通过设备树描述,驱动用of_xxx系列API来获取。

举个例子:你在设备树里写了一个I2C控制器节点,compatible = "snps,designware-i2c"。内核启动时,会遍历所有注册的I2C驱动,找到compatible匹配的那个驱动,然后调用它的probe函数。驱动在probe里通过设备树获取基地址、中断号、时钟频率,然后初始化硬件。整个过程不需要改一行内核代码。

你可能会问:设备树和U-Boot到底怎么协同?嗯,这个问题我们后面章节会详细讲。简单说,U-Boot在启动内核前,会做三件事:

  1. 从存储介质(Flash、SD卡、网络)加载.dtb文件到内存
  2. 根据实际硬件情况,修改.dtb中的某些节点(比如根据EEPROM读取的MAC地址,更新eth节点)
  3. 把.dtb的地址通过寄存器或参数传递给内核

我记得有一次调试,板子上的DDR容量是512MB,但设备树里写的是256MB。U-Boot启动时检测到实际内存大小,自动修改了memory节点的reg属性。嗯,这就是设备树的灵活性——它允许U-Boot在运行时动态调整硬件描述。

个人建议:刚开始学设备树,别急着写复杂的.dts。先拿一块开发板,看看它的设备树文件长什么样。用dtc工具把.dtb反编译成.dts,对照原理图一行一行看。我当年就是这么学的,虽然慢,但基础打得牢。

总结一下今天的内容:设备树是嵌入式Linux的硬件描述语言,它把硬件信息从内核代码中剥离出来,让U-Boot和内核通过.dtb这个"契约"来协同工作。没有设备树,嵌入式Linux不可能支持今天这么多五花八门的硬件平台。

下一章,咱们会深入设备树的语法和编译方法。到时候我会带大家手写一个完整的.dts文件,然后编译成.dtb,再放到U-Boot里测试。嗯,敬请期待。