1. 设备树起源:为什么需要设备树?从板级文件到设备树的演进历史

1.1 那个让人头疼的板级文件时代

做嵌入式Linux开发的老司机们,应该都记得那个被 arch/arm/ 目录支配的恐惧。

大概在2010年以前,每当我们拿到一块新的ARM开发板,第一件事就是往内核里塞一堆 board-xxx.c 文件。这些文件里密密麻麻地写着:

  • GPIO几号接了LED
  • I2C总线上挂了哪个传感器
  • SPI设备的片选信号是哪个引脚
  • 内存映射的起始地址是多少

说白了,每个板子都有自己的“身份证”——一个几百行的C文件。内核启动时,这些代码会一股脑地执行,把硬件信息硬编码进去。

核心问题:硬件描述和内核代码完全耦合。换一个板子,就得改内核代码,重新编译。

我记得有一次,客户临时换了一颗Flash芯片,只是引脚重映射了一下。结果呢?我得从头翻那个 board-xxx.c,找到对应的GPIO初始化代码,改完再重新编译整个内核。折腾了大半天,就为了改几个数字。你说烦不烦?

1.2 为什么板级文件模式走不下去了?

你想想看,ARM架构不像x86那样有固定的硬件枚举机制。x86有ACPI、有PCIe的配置空间,操作系统可以自动发现硬件。但ARM呢?每个SoC厂商、每个板子厂商,硬件连接方式千奇百怪。

这就导致了一个现象:内核代码里,ARM架构的板级文件数量呈指数级增长。到了Linux 3.x时代,arch/arm/ 目录下已经有上千个板级文件了。每次内核发布,光维护这些文件就够受的。

更糟糕的是,这些文件里充斥着大量重复代码。比如两个板子用了同一颗SoC,只是外设引脚不同,但它们的板级文件可能有80%的内容是重复的。内核社区的大佬们终于坐不住了——这哪是写驱动,这分明是在做体力活。

1.3 设备树:把硬件描述从内核代码中剥离出来

所以,设备树(Device Tree)的核心理念就一句话:把硬件描述和内核代码解耦

设备树本质上是一个描述硬件的数据结构。它用树形结构来描述:

  • CPU有几个核心
  • 内存的地址范围
  • 总线上挂了哪些设备
  • 每个设备的中断号、寄存器地址、时钟频率
  • 引脚复用关系

这些信息不再写在C代码里,而是写在一个 .dts 文件中。内核启动时,会解析这个文件,动态构建硬件信息。换板子?换一个 .dts 文件就行,内核代码不用动。

我的个人习惯:拿到一块新板子,我第一件事就是看它的 .dts 文件。这比看原理图还快,因为设备树已经把硬件拓扑结构清晰地表达出来了。

1.4 设备树的演进时间线

时间 事件 说明
2005年 PowerPC架构率先引入设备树 IBM和Freescale为了解决PowerPC板级文件泛滥问题,从Open Firmware中借鉴了设备树概念
2011年 ARM架构开始支持设备树 Linux 3.0开始,ARM社区正式推动设备树化,但初期支持有限
2012年 设备树成为ARM架构的标准配置 Linux 3.7之后,新加入的ARM平台必须使用设备树,板级文件被标记为“deprecated”
2014年 设备树覆盖大部分ARM平台 Linux 3.16时,ARM架构下设备树已经全面取代板级文件
至今 RISC-V、MIPS等架构也采用设备树 设备树已经成为嵌入式Linux的事实标准

1.5 设备树到底解决了哪些痛点?

我总结了一下,设备树主要解决了三个核心问题:

  1. 硬件描述与内核代码分离:改硬件配置不用重新编译内核,只需要更新 .dtb 文件(编译后的设备树二进制文件)。
  2. 减少内核代码膨胀:ARM架构的板级文件从上千个减少到几十个核心文件,大部分硬件差异由设备树处理。
  3. 统一硬件描述语言:不管你是ARM、RISC-V还是MIPS,都用同一套语法描述硬件,驱动代码可以跨架构复用。

我曾经踩过的坑:刚开始用设备树时,我犯过一个低级错误——在 .dts 里把GPIO的极性写反了。结果LED灯怎么点都不亮。查了两天才发现,原来设备树里 GPIO_ACTIVE_LOWGPIO_ACTIVE_HIGH 的定义和我想的正好相反。嗯,从那以后,我每次写GPIO节点都会再三确认极性。

1.6 设备树的工作流程

设备树在系统中的流转过程,其实很简单:

编写 .dts 文件
    ↓
dtc 编译器编译
    ↓
生成 .dtb 二进制文件
    ↓
Bootloader(如U-Boot)加载 .dtb 到内存
    ↓
内核启动时解析 .dtb,构建 device_node 树
    ↓
驱动通过 of_xxx 系列API读取硬件信息
    ↓
驱动完成设备初始化

这里有个细节要注意:.dts 是给人看的文本文件,.dtb 是给机器读的二进制文件。中间那个 dtc 编译器,就是设备树的“翻译官”。

1.7 一个简单的设备树长什么样?

为了让你有个直观感受,我贴一个最简单的设备树示例:

/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard";

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

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

    gpio0: gpio@20000000 {
        compatible = "mycompany,gpio";
        reg = <0x20000000 0x1000>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

你看,这个文件清晰地描述了:

  • 板子型号是 MyBoard
  • 内存从 0x80000000 开始,大小512MB
  • 有一个串口,地址 0x10000000,中断号5
  • 有一个GPIO控制器,地址 0x20000000

这些信息,以前都得写在C代码里。现在?一个文本文件搞定。

1.8 小结

设备树的出现,说白了就是嵌入式Linux开发的一次“工业化革命”。它把硬件描述从内核代码中解放出来,让驱动开发变得更灵活、更可维护。

我个人觉得,理解设备树的起源,比学会它的语法更重要。因为只有知道了它要解决什么问题,你才能理解为什么设备树要这样设计——为什么要有 compatible 属性?为什么要有 reg 属性?为什么要有中断映射?

这些问题的答案,都藏在板级文件时代的痛苦里。下一章,我会带你深入设备树的语法细节,看看那些 .dts 文件里的每一行到底在说什么。