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

大家好,我是你们的老朋友。今天咱们聊聊设备树的起源。

说实话,我刚入行那会儿,Linux内核里还没有设备树这玩意儿。那时候写驱动,最头疼的就是处理那些板级信息。你想想看,一个SoC要适配几十种开发板,每种板子的GPIO、中断、时钟配置都不一样,这代码怎么写?

1.1 板级文件时代的痛点

在设备树出现之前,Linux内核用的是什么?板级文件(Board File)。

每个开发板都有一个对应的 .c 文件,比如 arch/arm/mach-xxx/board-yyy.c。这个文件里写满了硬件信息:

// 伪代码示例:板级文件中的硬件描述
static struct platform_device my_device = {
    .name = "my_driver",
    .id = -1,
    .dev = {
        .platform_data = &my_platform_data,
    },
};

static struct i2c_board_info __initdata my_i2c_devices[] = {
    {
        I2C_BOARD_INFO("temperature_sensor", 0x48),
        .irq = IRQ_GPIO_A_START + 17,
    },
};

static void __init my_board_init(void)
{
    platform_device_register(&my_device);
    i2c_register_board_info(0, my_i2c_devices, ARRAY_SIZE(my_i2c_devices));
}

这种写法有什么问题?我给大家列几条:

  • 代码冗余:每个板子都要写一套类似的初始化代码
  • 维护困难:换一个板子就要改内核源码,重新编译
  • 碎片化严重:Linux内核里充斥着各种板级文件,社区维护成本极高
  • 二进制不兼容:同一个内核镜像无法在不同板子上运行

我记得有一次,客户要求我们支持一款新的开发板。我花了整整两天时间,就为了改那几个板级文件里的GPIO号和中断号。改完之后还要重新编译整个内核,烧录到板子上测试。效率低得让人抓狂。

我曾经踩过的坑:有一次我改了一个板级文件里的I2C设备地址,结果忘了更新另一个文件里的对应配置。调试了整整一个下午,才发现两个文件里的信息不一致。这种问题在板级文件时代太常见了。

1.2 设备树的诞生

为什么会想到用设备树?说白了,就是要把硬件描述驱动代码彻底分开。

设备树(Device Tree)最早源于PowerPC架构。2005年左右,PowerPC社区开始使用一种叫Open Firmware的技术,用类似Forth的语言来描述硬件。后来ARM社区也遇到了同样的问题——板级文件太多了,多到让人崩溃。

2011年,Linux 3.0内核开始正式支持ARM架构的设备树。到了2012年,ARM社区明确表态:新板子必须使用设备树,不再接受新的板级文件

设备树的核心思想很简单:

  • 树形结构描述硬件
  • 每个硬件节点包含属性(reg、interrupts、clocks等)
  • 驱动通过匹配机制找到对应的设备节点
  • 硬件变化时,只需修改设备树文件,无需改动内核

设备树的本质:它是一份硬件描述文件,告诉内核"你的板子上有什么硬件,它们怎么连接"。驱动代码只关心"怎么操作这个硬件",不关心"这个硬件在哪个板子上"。

1.3 从板级文件到设备树的演进

咱们用一张图来看看这个演进过程:

Linux内核硬件描述演进历史 板级文件时代 2000-2011 每个板子一个.c文件 硬件信息硬编码 过渡期 2011-2012 板级文件+设备树并存 逐步迁移 设备树时代 2012-至今 统一的硬件描述 驱动与硬件解耦 关键节点: 2011年:Linux 3.0开始支持ARM设备树 2012年:ARM社区宣布不再接受新板级文件 2014年:大多数主流SoC完成设备树迁移

这张图很清楚地展示了三个阶段的演进。我个人觉得,过渡期是最痛苦的——既要维护旧的板级文件,又要学习新的设备树语法。那段时间我经常搞混,在板级文件里写设备树语法,或者在设备树里用板级文件的写法。

1.4 设备树解决了哪些问题?

咱们用表格对比一下,设备树到底带来了哪些改变:

对比项 板级文件时代 设备树时代
硬件描述方式 C语言代码硬编码 文本文件描述(.dts)
修改硬件 改内核源码,重新编译 改设备树文件,重新编译dtb
多板支持 每个板子一个内核镜像 一个内核镜像+多个dtb
驱动复用 驱动与板级信息耦合 驱动与硬件描述解耦
社区维护 碎片化严重,维护成本高 统一规范,维护成本低
学习曲线 需要了解内核架构 只需学习设备树语法
我的建议:如果你刚开始学习嵌入式Linux,直接从设备树入手就好。板级文件那套东西,了解一下历史就够了,不用深究。我见过不少新人花大量时间研究板级文件,结果发现现在根本用不上。

1.5 设备树的基本概念

说了这么多,设备树到底是什么?我给大家拆解一下:

  • .dts文件:设备树源文件,文本格式,人类可读
  • .dtsi文件:设备树头文件,通常包含SoC公共部分
  • .dtb文件:设备树二进制文件,由dts编译生成,内核使用
  • 节点(node):描述一个硬件设备,用 {} 包裹
  • 属性(property):描述设备的特性,如地址、中断号等

一个简单的设备树示例:

// my-board.dts
/dts-v1/;

#include "soc-common.dtsi"

/ {
    model = "My Custom Board";
    compatible = "mycompany,myboard", "generic-board";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

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

    i2c@1000 {
        compatible = "mycompany,i2c";
        reg = <0x1000 0x100>;
        interrupts = <17>;
        clock-frequency = <100000>;

        temperature@48 {
            compatible = "mycompany,temp-sensor";
            reg = <0x48>;
        };
    };
};

这段代码描述了一个简单的开发板:

  • 板子型号是"My Custom Board"
  • 内存起始地址0x80000000,大小256MB
  • 有一个I2C控制器,地址0x1000,中断号17
  • I2C总线上挂了一个温度传感器,地址0x48

关键点:设备树描述的是硬件长什么样,而不是驱动怎么操作硬件。驱动代码通过"compatible"属性来匹配设备树中的节点。这个匹配机制,咱们下一节会详细讲。

1.6 我的经验总结

做了这么多年嵌入式开发,我最大的感受是:设备树让硬件描述变得标准化了

以前写驱动,你得先搞清楚这个板子上的硬件是怎么连接的,然后去板级文件里找对应的初始化代码。现在呢?打开设备树文件,一目了然。哪个外设在哪个地址,用哪个中断,时钟怎么配,全都在一个文件里。

我记得有一次,客户要求把一块板子上的UART从2号改成1号。在板级文件时代,我得改好几个文件:板级初始化、时钟配置、中断映射...改完还要重新编译。用设备树之后,只需要改一行 reg = <0x10010000 0x1000>;,重新编译dtb,搞定。

嗯,这就是设备树的魅力。它把硬件描述从内核代码中剥离出来,让驱动开发变得更纯粹、更高效。

好了,这一节就到这里。下一节咱们聊聊设备树的语法细节,包括节点命名、属性定义、引用机制这些实战中经常用到的东西。


公众号:蓝海资料掘金营,微信deep3321