1. 设备树基础概念

大家好,我是你们的嵌入式系统讲师。今天咱们来聊聊设备树——这个在嵌入式Linux开发中绕不开的话题。

说实话,我刚开始接触设备树的时候,也是一头雾水。那时候还在用老旧的板级文件,突然说要换成设备树,心里还挺抵触的。但用了一段时间后,我不得不承认——这玩意儿确实是个好东西。

什么是设备树?

设备树,英文叫Device Tree,简称DT。说白了,它就是一个描述硬件信息的文本文件。

你想想看,一个嵌入式板子上有CPU、内存、GPIO、I2C、SPI、UART……这么多硬件,操作系统怎么知道它们长什么样?怎么知道它们怎么用?

设备树就是干这个的。它用树形结构,把板子上的所有硬件信息都描述清楚。嗯,就像一棵倒着长的树——根节点是CPU,下面挂着内存控制器、总线、外设等等。

核心要点:设备树是一种数据结构,用于描述硬件资源,让操作系统无需修改内核代码就能支持不同的硬件平台。

我习惯把设备树比作一张硬件“身份证”。每个外设都有自己专属的节点,节点里写着它的地址、中断号、时钟频率等等。内核启动时,就读这张“身份证”,然后按图索骥去初始化硬件。

为什么需要设备树?

这个问题问得好。在设备树出现之前,嵌入式Linux是怎么处理硬件的?

答案是——板级文件(Board File)。

那时候,每出一个新板子,就得在内核源码里加一个arch/arm/mach-xxx/board-xxx.c文件。里面密密麻麻地写着各种硬件的初始化代码。我当年参与过一个项目,光板级文件就有十几个,每个都上千行。

这有什么问题呢?问题大了去了。

  • 代码冗余:同样的硬件,换个板子就得重新写一遍初始化代码
  • 维护困难:内核升级时,板级文件经常要跟着改,工作量巨大
  • 碎片化严重:每个厂商都有自己的板级文件,Linux主线内核根本收不完

我记得有一次,客户要求把一块成熟的板子移植到新内核上。我花了整整两周,就为了把那个板级文件适配过去。改完以后,我就在想——这肯定不是长久之计。

设备树就是来解决这个痛点的。它把硬件描述和内核代码彻底分开了。

我的经验:设备树让硬件描述变成了“数据”而不是“代码”。数据可以独立于内核存在,换板子只需要换一个.dts文件,内核代码完全不用动。这在项目迭代中太香了。

设备树在嵌入式Linux中的角色

设备树在嵌入式Linux里到底扮演什么角色?我用一个图来说明。

设备树在嵌入式Linux中的角色 硬件层(Hardware) CPU · 内存 · GPIO · I2C · SPI · UART · 时钟 · 中断控制器 硬件描述 设备树(Device Tree) .dts / .dtsi → .dtb(二进制) 解析加载 Linux内核(Kernel) 设备驱动 · 内存管理 · 中断子系统 · 时钟框架

从上图你能看到,设备树夹在硬件和内核之间,扮演着“翻译官”的角色。

具体来说,它的工作流程是这样的:

  1. 硬件厂商提供板子的设备树源文件(.dts)
  2. 编译工具把.dts编译成二进制文件(.dtb)
  3. Bootloader(比如U-Boot)把.dtb加载到内存
  4. 内核启动时解析.dtb,获取硬件信息
  5. 设备驱动通过设备树API读取硬件参数,完成初始化

注意:我曾经遇到过一个坑——设备树里中断号写错了,结果某个外设死活不工作。查了两天才发现是.dts里一个数字写反了。所以写设备树一定要仔细,一个字节都不能错。

设备树的核心文件类型

搞设备树开发,你得认识这几种文件:

文件类型 扩展名 说明
源文件 .dts 设备树源文件,描述具体板子的硬件
头文件 .dtsi 设备树包含文件,通常放SoC公共部分
二进制文件 .dtb 编译后的设备树,内核直接使用
编译脚本 .dts → .dtb 使用dtc工具编译

我个人习惯把.dtsi当作“公共库”。比如STM32MP1系列,所有板子都共用同一个stm32mp151.dtsi,里面定义了CPU、内存控制器、总线等。然后每个板子再写自己的.dts,只描述板级特有的外设。

小技巧:写设备树时,多用#include引入.dtsi文件。这样既能复用代码,又能保持清晰。我见过有人把所有东西都写在一个.dts里,结果文件上万行,维护起来想哭。

一个简单的设备树示例

光说不练假把式。咱们看一个最简单的设备树长什么样:

/dts-v1/;

/ {
    model = "STM32MP157C-DK2";
    compatible = "st,stm32mp157c-dk2", "st,stm32mp157";

    memory@c0000000 {
        device_type = "memory";
        reg = <0xc0000000 0x20000000>;
    };

    chosen {
        stdout-path = &uart4;
    };

    uart4: serial@40010000 {
        compatible = "st,stm32-uart";
        reg = <0x40010000 0x400>;
        interrupts = <0 52 4>;
        status = "okay";
    };
};

这段代码描述了什么?

  • 板子型号是STM32MP157C-DK2
  • 内存从0xc0000000开始,大小512MB
  • 控制台输出用UART4
  • UART4的基地址是0x40010000,中断号是52

你看,是不是很直观?内核拿到这个文件,就知道怎么初始化硬件了。

设备树的优势总结

说了这么多,咱们总结一下设备树的几个核心优势:

  • 解耦硬件与内核:换板子不用改内核代码
  • 标准化描述:统一的语法,所有厂商都遵守
  • 易于维护:硬件信息集中管理,一目了然
  • 支持多板型:一个内核镜像可以适配多个板子
  • 社区生态好:Linux主线内核大量使用设备树

嗯,这就是设备树的基础概念。说白了,它就是嵌入式Linux世界里,硬件和软件之间的一座桥梁。没有它,我们的开发工作会痛苦得多。

下一节,咱们会深入设备树的语法和节点结构。到时候我会拿一个真实的STM32MP1项目来拆解,看看设备树到底怎么写、怎么用。

一句话总结:设备树 = 硬件的“身份证” + 内核的“导航图”。搞懂它,嵌入式Linux开发就成功了一半。