一、设备树是什么:从一块开发板说起

我记得刚入行那会儿,做嵌入式Linux开发最头疼的事是什么?

不是写驱动,不是调中断,而是——换一块开发板,内核就要重新编译一遍。

为什么?因为那时候硬件信息是硬编码在代码里的。板子上的GPIO接了个LED,你得在arch/arm/mach-xxx/board-xxx.c里写死。换一颗Flash芯片?改代码。换一个网卡?改代码。甚至只是把某个外设的地址挪了挪位置,还是得改代码。

这合理吗?不合理。但当时大家都这么干。

1.1 设备树的起源

设备树(Device Tree)这个概念,最早来自PowerPC架构。2005年左右,PowerPC的开发者们受够了这种「改板子=改内核」的玩法。他们想:能不能把硬件描述从内核代码里抽出来,变成一份独立的数据文件?

这个想法后来被ARM架构看上了。ARM生态有多乱,做过的人都知道——每家芯片厂商都有自己的板子,每个板子都有自己的外设布局。如果每个板子都要维护一份board-xxx.c,那内核维护者得疯掉。

所以从Linux 3.x开始,ARM架构全面转向设备树。到后来,RISC-V、MIPS、X86(某些场景)也都跟进了。

核心思想:设备树就是把硬件描述从内核源码中剥离出来,变成一份独立的数据结构。内核启动时解析这份数据,动态构建设备模型。

1.2 为什么需要设备树

你想想看,如果没有设备树,会是什么场景?

  • 代码膨胀:每个板子都要在arch/arm/mach-xxx/下加一堆board文件。Linux 3.0之前,ARM架构下有上百个board文件,很多都是重复的。
  • 维护噩梦:换一个板子就要重新编译内核。用户拿到的内核镜像,跟硬件是绑死的。
  • 碎片化严重:同样的驱动,因为板级信息不同,要写多个版本。我见过一个GPIO驱动,因为board文件里中断号写的位置不一样,硬生生搞出三个ifdef分支。

设备树解决了这些问题。它把硬件描述变成了一份文本文件(.dts),编译成二进制(.dtb),跟内核镜像分开存放。启动时,bootloader把dtb传给内核,内核解析后就知道:哦,这个板子有512MB内存,UART在0x10000000,网卡是RTL8169,中断号是32。

我个人习惯:把设备树想象成硬件的「身份证」。内核拿到这张身份证,就知道该怎么跟硬件打交道。换板子?换身份证就行,内核不用动。

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

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

设备树在Linux内核中的角色 硬件层 CPU、内存、外设 设备树层 .dts / .dtb Linux内核 设备模型、驱动框架 设备驱动 关键流程说明 ① Bootloader加载dtb到内存 ② 内核启动时解析dtb ③ 根据dtb创建设备节点 ④ 驱动匹配设备节点 ⑤ 驱动访问硬件资源 设备树提供的信息: • 内存布局(起始地址、大小) • 外设寄存器地址 • 中断号、DMA通道 • 时钟频率、GPIO引脚 • 设备间的依赖关系

这张图展示了设备树的核心位置。它夹在硬件和内核之间,充当翻译官的角色。内核不直接跟硬件对话,而是通过设备树来了解硬件。

1.4 设备树到底长什么样

说了这么多,设备树文件到底长什么样?我贴一个最简单的例子:

// 一个简单的设备树源文件
/dts-v1/;

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

    #address-cells = <1>;
    #size-cells = <1>;

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

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

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

这段代码描述了一个简单的开发板:256MB内存,一个UART,一个GPIO控制器。每个设备都有它的地址、中断号、时钟频率等信息。

我曾经踩过一个坑:设备树里reg属性的地址写错了,导致内核访问外设时直接挂死。调试了整整两天才发现是地址偏移了一位。所以写设备树时,地址和大小一定要跟硬件手册再三核对。

1.5 设备树的核心价值

总结一下,设备树给嵌入式Linux带来了什么:

维度 没有设备树 有设备树
硬件描述 硬编码在C代码中 独立的数据文件
板级支持 每个板子一个board文件 每个板子一个.dts文件
内核编译 换板子必须重新编译 只需更换dtb文件
驱动开发 驱动与板级信息耦合 驱动与板级信息解耦
维护成本 高(代码冗余严重) 低(描述与实现分离)

说白了,设备树就是让「硬件描述」和「驱动代码」各司其职。硬件描述归硬件描述,驱动代码归驱动代码。两者通过设备树这个接口对接,互不干扰。

我的理解:设备树是嵌入式Linux走向工程化、标准化的重要一步。它让内核不再跟具体的板子绑定,让驱动可以跨平台复用。如果你做过裸机开发,就知道这有多重要——裸机程序里,硬件信息散落在各个头文件和宏定义里,改一个引脚定义要翻遍整个工程。设备树把这种混乱变成了有序。

1.6 小结

这一章我们聊了设备树的起源、为什么需要它、以及它在内核中的角色。核心就三句话:

  • 设备树是硬件的「身份证」
  • 它把硬件描述从内核代码中剥离出来
  • 它让驱动开发与板级支持解耦

下一章,我们会深入设备树的语法和结构,看看.dts文件到底怎么写、怎么编译、怎么调试。嗯,到时候我会分享一些实战中遇到的坑,保证让你少走弯路。


可可精品资料库