1. 设备树基础:什么是设备树?为什么需要设备树?设备树在Linux内核中的角色。

大家好,我是你们的老朋友。今天咱们开始聊设备树。说实话,我刚入行那会儿,设备树这个概念让我头疼了好一阵子。但搞明白之后,你会发现它其实没那么玄乎。

1.1 什么是设备树?

设备树,英文叫 Device Tree,简称 DT。说白了,它就是一个描述硬件的数据结构。嗯,你可以把它想象成一张硬件的「身份证」。

这张身份证上写了什么?写了你的板子上有哪些硬件设备,它们怎么连接的,地址是多少,中断号是多少,等等。内核启动的时候,就靠读这个文件来认识硬件。

核心定义:设备树是一种用于描述硬件资源的数据结构,它以树形结构组织,每个节点代表一个硬件设备或总线。

举个例子,一个简单的设备树节点长这样:

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

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

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

你看,每个节点都有名字、地址,还有 compatible 属性。这个 compatible 特别重要,它告诉内核:这个设备用哪个驱动来管。

1.2 为什么需要设备树?

这个问题问得好。我刚开始做嵌入式的时候,还没有设备树。那时候怎么搞?硬编码!

什么意思呢?就是每个板子都要在 Linux 内核源码里写一堆 board-xxx.c 文件。里面全是硬件的地址、中断号、时钟配置。你想想看,一个芯片支持几十种板子,就得写几十个这样的文件。维护起来简直是噩梦。

我记得有一次,客户换了一个内存颗粒,只是改了容量和时序参数。结果呢?我得重新编译整个内核,就为了改一个 board 文件里的几个数字。你说崩溃不崩溃?

设备树解决了这个问题。它把硬件描述从内核源码里剥离出来,变成了一个独立的数据文件。这样,换硬件只需要改设备树文件,内核源码不用动。

我的经验:设备树最大的好处是「硬件与驱动解耦」。驱动写一次,设备树配不同板子。这在项目迭代快的公司里,能省下大量时间。

具体来说,设备树带来了几个关键好处:

  • 硬件描述标准化:用统一的语法描述硬件,不再依赖特定架构的板级文件
  • 驱动通用化:驱动只认 compatible 字符串,不关心具体板子
  • 启动参数灵活:内存大小、时钟频率、引脚复用,全在设备树里配
  • 多平台支持:同一份内核镜像,换设备树就能跑不同板子

1.3 设备树在Linux内核中的角色

设备树在内核里扮演什么角色?我打个比方你就明白了。

内核就像一个操作系统,设备树就是它的「硬件地图」。没有这张地图,内核就是个瞎子,不知道内存在哪,不知道串口在哪,更不知道怎么跟外设通信。

具体流程是这样的:

  1. Bootloader(比如 U-Boot)把设备树二进制文件(DTB)加载到内存
  2. 内核启动时,从指定地址读取 DTB
  3. 内核解析 DTB,构建出设备树结构体(device_node 树)
  4. 驱动通过设备树 API 查找节点、读取属性
  5. 驱动根据设备树信息初始化硬件

你看,设备树贯穿了整个硬件初始化过程。没有它,内核连内存大小都不知道,更别提挂载根文件系统了。

注意:设备树不是万能的。它只描述「硬件有什么」,不描述「硬件怎么用」。怎么用是驱动的事。我曾经见过有人把驱动逻辑写进设备树,这是大忌!

1.4 设备树的文件格式

设备树有三种文件格式,我简单列一下:

格式 扩展名 说明
DTS .dts 设备树源文件,文本格式,人类可读
DTSI .dtsi 设备树头文件,用于包含公共定义
DTB .dtb 设备树二进制文件,内核实际使用的格式

DTS 编译成 DTB 的工具叫 dtc(Device Tree Compiler)。这个工具在 Linux 内核源码的 scripts/dtc 目录下。我习惯用内核自带的 dtc,版本匹配,不容易出问题。

1.5 一个完整的例子

咱们来看一个实际项目中用到的设备树片段。这是我之前做的一个 i.MX6ULL 板子的配置:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "MyCustomBoard";
    compatible = "mycompany,imx6ull-board", "fsl,imx6ull";

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

    chosen {
        stdout-path = &uart1;
    };
};

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

&i2c1 {
    clock-frequency = <100000>;
    status = "okay";

    touch@38 {
        compatible = "edt,edt-ft5x06";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 2>;
    };
};

这里有几个关键点:

  • /dts-v1/; 表示使用设备树 v1 语法
  • #include 引入芯片级公共定义
  • modelcompatible 标识板子型号
  • chosen 节点指定控制台输出设备
  • &uart1 这种写法叫「引用」,用于覆盖或追加节点属性

避坑指南:我曾经犯过一个低级错误——忘了在 DTS 里加 /dts-v1/; 声明。结果 dtc 编译出来的 DTB 内核不认,启动直接挂掉。排查了半天才发现。所以,第一行一定要写!

1.6 设备树与驱动的绑定

设备树怎么跟驱动对上号?靠的就是 compatible 属性。

驱动里会定义一个 of_device_id 表:

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "mycompany,imx6ull-board", },
    { .compatible = "edt,edt-ft5x06", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);

内核启动时,会遍历设备树的所有节点。每个节点都带着 compatible 字符串。内核拿这个字符串去匹配驱动的 of_device_id 表。匹配上了,就调用驱动的 probe 函数。

说白了,这就是一个「对暗号」的过程。设备树说「我是 edt,edt-ft5x06」,驱动说「我认识 edt,edt-ft5x06」,好,你们俩配对成功。

1.7 小结

好了,这一章的内容就这么多。咱们回顾一下:

  • 设备树是硬件的「身份证」,用树形结构描述硬件资源
  • 它解决了板级文件泛滥的问题,实现了硬件与驱动的解耦
  • 在内核中,设备树是硬件初始化的「地图」,驱动靠它找到设备
  • DTS 编译成 DTB,内核解析 DTB 构建设备树结构

下一章,咱们会深入设备树的语法细节。到时候我会手把手教你写一个完整的 DTS 文件。嗯,敬请期待。