1. 设备树的前世今生:为什么需要设备树?

做嵌入式Linux开发这么多年,我经常被问到同一个问题:「设备树到底是个啥?为啥以前没有它,现在却非用不可?」

说实话,我刚入行那会儿也没有设备树。那时候写驱动,最头疼的就是改板级文件。每次换一块新板子,都得把内核源码翻个底朝天。嗯,今天我们就聊聊这段历史。

1.1 板级文件时代的噩梦

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

答案很简单——硬编码。每个开发板都在内核源码里有一个对应的 board-xxx.c 文件。比如你的板子叫 imx6q-sabresd,那内核里就有一个 arch/arm/mach-imx/mach-imx6q.c。

这个文件里写什么呢?我给大家看个简化版:

// 伪代码:板级文件
static struct resource uart1_resources[] = {
    {
        .start = 0x02020000,
        .end   = 0x02020fff,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = 58,
        .end   = 58,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device uart1_device = {
    .name = "imx-uart",
    .id   = 0,
    .num_resources = ARRAY_SIZE(uart1_resources),
    .resource = uart1_resources,
};

static void __init my_board_init(void)
{
    platform_device_register(&uart1_device);
    // 注册GPIO、I2C、SPI... 几百行代码
}

你想想看,每个板子都得写这么一套。我当时维护过三个不同厂商的板子,每个板子的 mach-xxx.c 文件都有上千行。最崩溃的是什么?

  • 代码冗余:同一款SoC,换个板子就得复制粘贴改地址
  • 维护地狱:Linux内核版本升级,板级文件接口变了,全部重写
  • 碎片化严重:厂商各自为政,内核社区根本管不过来
我曾经踩过的坑:有一次给客户移植内核,发现他们的板子把UART2的基地址写成了0x021E8000,但实际应该是0x021EC000。查了三天,最后发现是板级文件里一个宏定义写错了。这种低级错误,在设备树时代根本不会发生。

1.2 设备树的诞生:从PowerPC到ARM

设备树(Device Tree)其实不是Linux原创的。它最早来自Open Firmware,是PowerPC架构上用来描述硬件的一种方式。

2011年左右,ARM架构的碎片化问题已经到了不可收拾的地步。Linus Torvalds本人都在邮件列表里发飙了——「ARM的板级文件就是一堆垃圾!」

于是,ARM社区决定引入设备树。说白了,就是把硬件的描述信息从C代码里剥离出来,放到一个独立的文本文件里。这个文件就是 .dts(Device Tree Source)。

它的样子是这样的:

// imx6qdl-sabresd.dtsi
/ {
    soc {
        aips1: aips-bus@02000000 {
            uart1: serial@02020000 {
                compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
                reg = <0x02020000 0x4000>;
                interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
                clocks = <&clks IMX6QDL_CLK_UART_IPG>,
                         <&clks IMX6QDL_CLK_UART_SERIAL>;
                clock-names = "ipg", "per";
                status = "disabled";
            };
        };
    };
};

看到区别了吗?原来写在C代码里的寄存器地址、中断号、时钟信息,现在全写在一个树形结构的文本里。驱动代码只需要通过API去解析这个树,就能拿到所有硬件信息。

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

设备树到底扮演什么角色?我用一句话概括:它是硬件描述与驱动代码之间的契约。

具体来说,设备树承担了三个核心职责:

职责 说明 我的理解
硬件描述 告诉内核:CPU是什么、内存多大、外设挂在哪里 相当于硬件的「身份证」
配置参数 提供驱动需要的运行时参数,比如时钟频率、引脚复用 相当于驱动的「配置文件」
板级差异隔离 同一款SoC的不同板子,只需改dts文件,不用改驱动 这才是最爽的地方

核心要点:设备树不是内核的一部分,它是在内核启动时由Bootloader加载到内存中的二进制数据(DTB)。内核启动后,会解析这个DTB,动态构建出平台设备(platform_device)和设备节点(device_node)。

1.4 从板级文件到设备树的演进路线

这个演进过程不是一蹴而就的。我把它分成三个阶段:

  1. 混沌期(2000-2010):全是板级文件,每个厂商自己玩自己的。内核里充斥着 #ifdef CONFIG_BOARD_XXX 这样的条件编译。
  2. 过渡期(2011-2014):ARM社区强制要求新板子必须使用设备树。但老板子还在用板级文件,两套方案并存。那段时间我经常要同时维护两种风格的代码。
  3. 成熟期(2015至今):板级文件基本被淘汰。所有主流ARM SoC都使用设备树。RISC-V架构也沿用了这套方案。

我个人觉得,这个演进最核心的驱动力就两个字——解耦。把硬件描述和驱动逻辑解耦,把板级差异和SoC共性解耦。你想想看,没有设备树的时候,换一块板子就得重新编译整个内核。现在呢?只需要换一个 .dtb 文件就行。

1.5 设备树的核心知识体系

为了让大家有个整体认知,我画了一张图。这张图涵盖了设备树从编写到使用的完整链路:

设备树全链路知识体系 DTS 源文件 .dts / .dtsi DTC 编译器 Device Tree Compiler DTB 二进制 Device Tree Blob Bootloader 加载 DTB U-Boot / Barebox 等 Linux 内核启动阶段 解析 DTB → 创建 device_node → 注册 platform_device of_platform_populate() / of_scan_flat_dt() 驱动匹配与资源获取 of_match_table → of_iomap / of_get_property / of_irq_get 编写阶段 加载阶段 运行阶段

一个小技巧:刚开始学设备树的时候,别急着看语法。先理解这个链路——DTS怎么变成DTB,DTB怎么被内核解析,驱动怎么从设备树拿资源。把流程走通了,语法自然就记住了。

1.6 为什么设备树能活下来?

其实在设备树之前,也有过其他方案,比如ACPI(高级配置与电源管理接口)。但ACPI太复杂了,适合x86这种标准化平台。嵌入式平台千奇百怪,需要的是轻量、灵活、可读性强的方案。

设备树恰好满足了这些需求:

  • 文本格式:用文本编辑器就能改,不用重新编译内核
  • 树形结构:硬件本身就是树形的(SoC→总线→外设),天然匹配
  • 可覆盖:.dtsi 文件可以包含,.dts 文件可以覆盖,复用性极强
  • 社区支持:内核社区强制推行,厂商不配合都不行

我记得2012年第一次用设备树的时候,心里还犯嘀咕:「这玩意儿靠谱吗?」结果用了一个月就真香了。以前改个GPIO引脚要翻半天代码,现在改一行dts就行。

说白了,设备树解决的不是技术问题,而是工程管理问题。它让硬件描述变得可维护、可复用、可版本控制。这才是它能在嵌入式Linux领域站稳脚跟的根本原因。