1. 设备树基础:设备树(DTS)的起源与作用

1.1 为什么需要设备树?

先说说我早年做嵌入式开发时的经历吧。那时候,每次换一块新板子,都得重新编译内核。为什么?因为硬件信息是硬编码在C语言里的。你想想看,换个GPIO引脚、改个I2C地址,就得改arch/arm/mach-xxx下的板级文件。这活儿,说白了就是个体力活。

我记得有一次,客户临时换了个Flash芯片,只是容量不同、分区表变了。我愣是花了半天改代码、重新编译、烧录。当时我就想:要是能把硬件描述和内核代码分开,该多好?

嗯,设备树就是来解决这个问题的。

设备树的核心思想: 把硬件信息从内核源码中剥离出来,用一套独立的数据结构来描述。内核启动时,解析这个描述,动态获取硬件配置。

1.2 设备树的起源

设备树(Device Tree)最早源于PowerPC架构。2005年左右,PowerPC社区受够了板级文件泛滥的苦。他们借鉴了Open Firmware的FDT(Flattened Device Tree)概念,把硬件描述做成二进制格式,交给内核解析。

后来,ARM社区也遇到了同样的问题。2011年到2012年间,ARM Linux社区开始大规模采用设备树。到了2013年,新加入的ARM SoC基本都要求必须支持设备树。我那时候正好在做一个基于TI AM335x的项目,算是赶上了这波转型。

为什么会这样?因为ARM生态太碎片化了。每家芯片厂商都有自己的板级文件,内核维护者疲于应付。设备树一统江湖后,内核代码干净多了。

1.3 设备树在Linux内核中的位置

设备树在内核启动流程中,扮演着「硬件信息传递者」的角色。它的位置可以用下面这张图来说明:

设备树在Linux内核启动流程中的位置 Bootloader(U-Boot等) 加载设备树二进制文件(.dtb)到内存,传递地址给内核 内核早期启动(head.S / start_kernel) 解析设备树,获取内存大小、CPU数量、中断控制器等基础信息 设备树核心(drivers/of/) unflatten_device_tree() → 将FDT展开为树状结构 驱动匹配与设备注册 of_match_table → platform_driver_register() → probe() 设备树文件路径:arch/arm/boot/dts/ 或 arch/arm64/boot/dts/

从这张图可以看出,设备树贯穿了从Bootloader到驱动匹配的整个流程。我个人习惯把设备树比作「硬件说明书」——内核拿到它,就知道板上有什么、怎么用。

1.4 设备树相关的文件类型

实际工作中,你会接触到三种文件:

文件类型 扩展名 说明
设备树源文件 .dts 文本格式,开发者编写,类似C语言的语法
设备树头文件 .dtsi 可被多个.dts包含,用于SoC公共部分或板级公共部分
设备树二进制文件 .dtb 编译后的二进制,Bootloader加载并传递给内核

避坑指南: 我曾经犯过一个低级错误——直接修改了.dtb文件。记住,.dtb是二进制,不能直接编辑。要改就改.dts,然后用dtc编译器重新生成。命令是:dtc -I dts -O dtb -o xxx.dtb xxx.dts

1.5 一个简单的设备树示例

光说不练假把式。我们来看一个最简单的设备树片段:

/dts-v1/;

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

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

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

    uart0: serial@10000000 {
        compatible = "vendor,uart-ip";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
        clock-frequency = <24000000>;
        status = "okay";
    };
};

这段代码描述了什么?

  • 根节点:板子型号和兼容性标识
  • chosen节点:内核启动参数,相当于给内核传参
  • memory节点:内存起始地址0x80000000,大小512MB
  • uart0节点:串口控制器,地址0x10000000,中断号33

你想想看,如果没有设备树,这些信息就得写在arch/arm/mach-xxx/board-xxx.c里。换块板子,就得改C代码、重新编译。有了设备树,只需要换一个.dtb文件就行。

1.6 设备树与内核驱动匹配的关系

设备树最终要服务于驱动匹配。内核怎么知道哪个驱动对应哪个设备?靠的就是compatible属性。

驱动代码里会声明一个of_match_table

static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,uart-ip", },
    { /* sentinel */ }
};

static struct platform_driver my_uart_driver = {
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
    .probe = my_uart_probe,
    .remove = my_uart_remove,
};

内核启动时,遍历设备树中的所有节点。如果某个节点的compatible值与驱动的of_match_table匹配,就调用该驱动的probe函数。这就是设备树驱动匹配的核心机制。

注意: compatible属性的值有约定俗成的命名规则——「厂商,型号」。比如"ti,am335x-uart"、"fsl,imx6q-uart"。不要随便起名字,否则驱动匹配不上,板子就起不来了。

1.7 小结

设备树不是什么高深莫测的东西。说白了,它就是一份硬件配置清单。内核启动时照着清单干活,不用再硬编码在C代码里。

我个人觉得,理解设备树的关键就三点:

  1. 为什么需要:解耦硬件描述与内核代码
  2. 怎么工作:.dts → .dtb → 内核解析 → 驱动匹配
  3. 核心属性:compatible是匹配的关键,reg描述地址空间,interrupts描述中断

嗯,这一章就到这里。下一章我们会深入设备树的语法细节,包括节点、属性、标签、引用这些具体写法。到时候我会拿一个实际项目中的设备树文件来拆解,看看那些坑是怎么踩出来的。


专注资料整理