第1章 设备树起源:为什么需要设备树?

做嵌入式Linux开发的朋友,一定都跟设备树打过交道。但说实话,我刚入行那会儿,设备树对我来说就是个黑盒子——照着模板改,能跑就行,出了问题全靠蒙。

直到有一次,我在一个项目里被板级文件折腾得够呛。那是个基于ARM Cortex-A9的老平台,每次换一个外设,就得改一堆C代码,重新编译整个内核。改一次编译半小时,调试一次烧录五分钟。嗯,那滋味,谁干谁知道。

后来我认真研究了设备树的来龙去脉,才明白这东西到底解决了什么问题。今天咱们就从根源上聊聊:设备树到底是怎么来的?它凭什么能取代板级文件?

1.1 板级文件的时代

在设备树出现之前,Linux内核是怎么描述硬件平台的?

答案是:硬编码。

每个开发板,都在内核源码里有一堆C文件和头文件。这些文件里写死了内存地址、中断号、GPIO引脚、时钟频率……所有硬件信息,全写在代码里。

板级文件的核心问题:

  • 硬件信息与内核代码深度耦合
  • 换一个板子就要改内核源码
  • 不同厂商的板级文件风格各异,维护成本极高
  • 同一个SoC的不同开发板,代码大量重复

举个例子。当年我维护一个项目,用的是某厂商的ARM芯片。厂商给了个BSP包,里面光是板级文件就有十几个。每个文件里都有一段类似的代码:

static struct resource uart_resources[] = {
    {
        .start = 0x10009000,
        .end   = 0x10009fff,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = IRQ_UART0,
        .end   = IRQ_UART0,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device uart_device = {
    .name = "my_uart",
    .id   = 0,
    .resource = uart_resources,
    .num_resources = ARRAY_SIZE(uart_resources),
};

你看,每个外设都要手动定义资源结构体,注册platform_device。一个板子几十个外设,代码量可想而知。更麻烦的是,如果两个板子用的同一个SoC,只是外设引脚不同,你得复制一份代码,改几个数字。内核里充斥着这种「复制-修改」的板级文件。

1.2 为什么ARM社区最先受不了?

你可能会问:x86平台怎么没有设备树?

原因很简单。x86平台有ACPI(高级配置与电源管理接口),BIOS/UEFI固件已经帮你把硬件信息描述好了,内核启动时直接读取就行。但嵌入式平台,尤其是ARM平台,没有统一的固件标准。

ARM架构的碎片化是出了名的。同一个ARMv7指令集,有高通、TI、Freescale、NXP、三星……每家都有自己的外设布局。Linux内核为了支持这些五花八门的板子,板级文件数量呈爆炸式增长。

Linux内核版本 ARM板级文件数量(估算) 主要问题
2.6.x 数百个 每个板子独立文件,代码重复严重
3.0 ~ 3.8 超过1000个 维护成本急剧上升,社区不堪重负
3.9+ 逐步淘汰 引入设备树,板级文件开始迁移

我记得在2011年左右,Linus Torvalds公开吐槽过ARM的板级文件乱象。他说:「每次看到ARM的代码提交,都是一堆新的板级文件,这简直是一场灾难。」

说白了,ARM社区需要一个统一的硬件描述方式。设备树,就是在这个背景下被引入的。

1.3 设备树的诞生

设备树(Device Tree)的概念其实不是Linux原创的。它最早来自Open Firmware(开放固件),用于SPARC和PowerPC平台。Linux内核在PowerPC上已经用了很多年,效果不错。

2011年,ARM Linux社区决定引入设备树。从Linux 3.9开始,设备树成为ARM平台的标准硬件描述方式。到2014年左右,大部分主流ARM SoC都完成了设备树迁移。

设备树的核心思想:把硬件信息从内核代码中剥离出来,放到一个独立的数据结构中。内核启动时解析这个结构,动态创建platform_device,而不是在代码里硬编码。

设备树文件通常以.dts为后缀(Device Tree Source),经过编译生成.dtb(Device Tree Blob),由bootloader传递给内核。

一个简单的设备树节点长这样:

/ {
    model = "MyBoard";
    compatible = "vendor,myboard", "vendor,soc";

    uart@10009000 {
        compatible = "vendor,uart";
        reg = <0x10009000 0x1000>;
        interrupts = <0 33 4>;
        clocks = <&uart_clk>;
    };
};

你看,同样的UART外设,现在用文本描述,而不是C代码。内核驱动通过compatible属性匹配设备,通过reg获取地址,通过interrupts获取中断号。硬件变了,改.dts文件就行,不用重新编译内核。

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

设备树到底在内核里扮演什么角色?我用一张图来说明:

设备树在Linux内核中的角色 .dts 设备树源文件 DTC 编译 .dtb 设备树二进制 Bootloader 传递 Linux 内核解析设备树 动态创建 platform_device 驱动匹配 of_xxx API 读取

这张图展示了设备树的完整生命周期:

  1. 编写:开发者编写.dts源文件,描述硬件平台
  2. 编译:DTC(Device Tree Compiler)将.dts编译为.dtb二进制
  3. 传递:Bootloader(如U-Boot)将.dtb加载到内存,传递给内核
  4. 解析:内核启动时解析.dtb,动态创建设备,驱动通过compatible匹配

注意:设备树不是万能的。它只描述硬件「有什么」,不描述「怎么用」。驱动的行为逻辑,仍然由C代码实现。设备树只是把硬件配置信息从代码里抽出来了。

1.5 设备树带来的改变

设备树引入后,嵌入式Linux开发流程发生了根本变化:

对比项 板级文件时代 设备树时代
硬件描述方式 C代码硬编码 文本文件描述
更换板子 修改内核源码,重新编译 修改.dts,重新编译dtb
内核镜像通用性 一个内核只支持一个板子 一个内核支持多个板子
驱动开发 需要了解板级注册细节 只需关注compatible匹配
社区维护 板级文件泛滥,维护困难 设备树统一管理,易于维护

我个人最直观的感受是:以前给客户做方案,换一个Flash芯片,得改内核代码、重新编译、验证。现在改一下.dts里的reg属性,重新编译.dtb,重启就行。效率提升不是一星半点。

1.6 避坑指南

最后,分享几个我踩过的坑:

我曾经犯过的错:

  • 修改了.dts但忘记重新编译.dtb,调试了半天发现内核用的还是旧设备树
  • 设备树里reg属性写错了地址,导致驱动访问了错误的内存区域,系统随机崩溃
  • interrupts属性里的中断类型写错,导致中断无法触发

嗯,这些坑其实都很基础,但越基础的东西越容易忽略。后面章节我会详细讲每个属性的含义和用法,帮你从一开始就避开这些雷区。

设备树的出现,本质上是为了解决硬件描述与内核代码解耦的问题。它不是什么高深的技术,但它的设计思想——把配置从代码中分离出来——值得每个嵌入式开发者深入理解。