一、设备树基础概念

什么是设备树?

设备树,英文叫 Device Tree,是一套描述硬件的数据结构。说白了,它就是一个文本文件,用树形结构把板子上的所有硬件信息都列出来。

我刚开始接触嵌入式Linux时,觉得这东西挺玄乎的。后来做项目多了才明白——它就是一份硬件清单,告诉内核:你有哪些外设、它们挂在哪条总线上、地址是多少、中断号是什么。

举个例子,一个简单的设备树节点长这样:

uart0: serial@101f0000 {
    compatible = "arm,pl011";
    reg = <0x101f0000 0x1000>;
    interrupts = <0 45 4>;
    clock-names = "uartclk", "apb_pclk";
    clocks = <&uart_clk &apb_pclk>;
    status = "okay";
};

这段代码描述了一个串口控制器。compatible 告诉内核用哪个驱动,reg 指定了寄存器地址范围,interrupts 是中断信息,clocks 是时钟配置。

核心要点:设备树是硬件描述语言,不是程序代码。它不执行任何逻辑,只是提供数据。

为什么需要设备树?

这个问题我经常被问到。在设备树出现之前,Linux内核是怎么处理硬件的?

答案是——硬编码。每个板子都在内核源码里写一堆 platform_device 结构体,填上地址、中断、时钟等信息。换一个板子就要改代码,重新编译内核。

你想想看,这有多痛苦?

  • 维护成本高——每个板子都要维护一份内核源码
  • 代码膨胀——arch/arm 目录下塞满了各种板级文件
  • 升级困难——换一颗Flash芯片都要改内核

我记得2011年左右,我在做一个基于TI OMAP的平板项目。当时为了支持一个新版本的开发板,我得在 arch/arm/mach-omap2 里加几百行板级代码。改完还要重新编译整个内核,烧写验证,一个周期下来半天就没了。

设备树解决了这个问题。它把硬件描述从内核源码中剥离出来,变成独立的数据文件。内核启动时解析这个文件,动态构建设备模型。

我的经验:设备树让板级支持变得像配菜一样简单。换一个板子,只需要换一个 .dts 文件,内核不用动。这在产品快速迭代时特别有用。

设备树在Linux内核中的角色

设备树在内核里扮演什么角色?我习惯把它比作「硬件数据库」。

内核启动流程中,设备树的工作大致是这样的:

  1. Bootloader(如U-Boot)把设备树二进制文件(.dtb)加载到内存
  2. 内核启动时,从指定地址读取设备树
  3. 内核解析设备树,创建 platform_device、i2c_client、spi_device 等设备对象
  4. 驱动根据 compatible 匹配设备,完成初始化

下面这张图展示了设备树在内核中的核心位置:

设备树在Linux内核中的角色 硬件设备 设备树(.dts / .dtb) Linux内核(解析设备树,创建设备) platform_driver i2c_driver spi_driver 设备树解耦了硬件描述与内核代码,驱动通过 compatible 匹配设备

从这张图可以看出,设备树是硬件和驱动之间的桥梁。它不参与具体的数据处理,但决定了驱动能看到什么硬件。

设备树文件体系

实际项目中,设备树文件通常分三层:

文件类型 后缀 作用
设备树源文件 .dts 描述具体板子的硬件,如树莓派3B、某款开发板
设备树头文件 .dtsi 描述SoC公共部分,如CPU核心、内存控制器、时钟等
设备树二进制 .dtb 编译后的二进制文件,Bootloader加载给内核

我习惯把 .dtsi 比作「芯片手册」,.dts 比作「板级原理图」。芯片厂商提供 .dtsi,板级开发者写 .dts 来引用和定制。

注意:设备树不是万能的。它只描述「静态」硬件信息。动态硬件(如USB热插拔)还是靠内核枚举机制。我曾经见过有人试图用设备树描述USB设备,结果搞得一团糟。

设备树与时钟、Reset的关系

这门课的核心是时钟与Reset控制。为什么要把它们放在设备树里讲?

因为时钟和Reset是SoC里最基础、最关键的资源。一个外设要正常工作,必须先拿到时钟,然后释放Reset。设备树就是描述这些依赖关系的载体。

举个例子,一个I2C控制器需要两个时钟:

i2c1: i2c@40012000 {
    compatible = "st,stm32f4-i2c";
    reg = <0x40012000 0x400>;
    interrupts = <31>;
    clocks = <&rcc 0 STM32F4_APB1_CLOCK(I2C1)>;
    resets = <&rcc STM32F4_APB1_RESET(I2C1)>;
    status = "disabled";
};

这里的 clocks 和 resets 属性,就是告诉内核:这个I2C控制器依赖哪个时钟源、哪个Reset信号。驱动初始化时,内核会按顺序处理这些依赖。

嗯,这里要注意:时钟和Reset的编号不是随便写的。它们必须和时钟控制器(如RCC)驱动里的定义一致。我在项目中就踩过这个坑——时钟号写错了,外设怎么都唤不醒,查了两天才发现是设备树里少了一个偏移量。

避坑指南:写设备树时钟属性时,一定要对照芯片参考手册的时钟树图。我曾经因为看错时钟分频比,导致UART波特率差了一倍。从那以后,我每次写时钟属性都会先画一遍时钟路径。

小结

设备树不是什么高深的技术。它就是一份硬件描述文件,让内核知道板子上有什么、怎么访问。它的出现,让嵌入式Linux开发从「改内核源码」变成了「改配置文件」。

后面几章,我们会深入设备树的语法、时钟框架、Reset框架。但不管讲得多深,记住一点:设备树的核心价值是解耦——把硬件描述和内核代码分开。

这个理念,贯穿了整个嵌入式Linux生态。


专注资料整理