1. 设备树基础回顾:什么是设备树、为什么需要设备树、设备树文件结构

各位同学,咱们今天聊聊设备树。说实话,我刚入行那会儿,设备树还是个挺新鲜的东西。那时候做嵌入式,板级文件里全是硬编码的寄存器地址、中断号,改个引脚配置得翻半天代码。后来设备树一普及,整个世界都清爽了。

1.1 什么是设备树?

设备树,英文叫 Device Tree,简称 DT。说白了,它就是一个描述硬件信息的“数据文件”。你想想看,一个嵌入式板子上有 CPU、内存、I2C 控制器、SPI 控制器、GPIO 引脚……这些东西怎么告诉内核?

传统做法是在内核源码里写一堆 C 结构体,把硬件信息硬编码进去。但这样有个问题——换一块板子,就得重新编译内核。设备树把硬件描述从内核代码里剥离出来,变成一份独立的数据文件。内核启动时解析这个文件,就知道自己跑在什么硬件上了。

我在项目中遇到过最典型的场景:同一款 SoC,用在三款不同的产品上。外设引脚分配、内存大小、时钟频率都不一样。如果没有设备树,我得维护三份内核代码。有了设备树,内核镜像完全一样,只换 .dtb 文件就行。嗯,这就是设备树的价值。

1.2 为什么需要设备树?

咱们从几个角度来理解这个问题。

  • 解耦硬件与驱动:驱动代码不再关心“我的 I2C 控制器挂在哪个地址”,而是通过设备树节点去匹配。驱动写一次,换个板子只要改设备树。
  • 减少内核分支:以前每个板子都要维护 arch/arm/mach-xxx 下的板级文件,代码量巨大。设备树统一了接口,内核主线更容易支持多种硬件。
  • 动态配置:有些硬件参数(比如内存大小、MAC 地址)可以在启动时由 bootloader 修改设备树,内核不用重新编译。
  • 社区标准:现在 Linux 内核社区已经全面拥抱设备树,新架构(ARM64、RISC-V)基本都靠它。

核心思想:设备树是硬件描述与驱动代码之间的“契约”。驱动通过设备树节点名称和属性来匹配硬件,而不是靠硬编码的地址和中断号。

1.3 设备树文件结构

设备树涉及三种文件格式:.dts、.dtsi、.dtb。我刚开始学的时候也搞混过,咱们一个一个说清楚。

1.3.1 .dts 文件

.dts 是 Device Tree Source 的缩写,也就是设备树源文件。它是文本文件,用人类可读的格式描述硬件。一个典型的 .dts 文件长这样:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "MyBoard";
    compatible = "mycompany,imx6ull-board";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;
    };

    &uart1 {
        pinctrl-0 = <&pinctrl_uart1>;
        status = "okay";
    };
};

注意看,第一行 /dts-v1/; 是版本声明,表示使用设备树 v1 格式。后面的 #include 可以包含头文件或 .dtsi 文件。根节点 / 下面定义板级信息,比如型号、内存布局。引用其他节点用 &uart1 这种语法,可以覆盖或追加属性。

个人习惯:我写 .dts 文件时,喜欢把板级特有的配置(比如引脚复用、外设使能)放在 .dts 里,把 SoC 通用的部分放在 .dtsi 里。这样复用性最好。

1.3.2 .dtsi 文件

.dtsi 是 Device Tree Source Include 的缩写,相当于设备树的“头文件”。它通常描述 SoC 内部的外设框架,比如 CPU 核心、中断控制器、时钟、GPIO 控制器等。这些信息对同一款 SoC 的所有板子都是通用的。

举个例子,imx6ull.dtsi 里会定义:

uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
    reg = <0x02020000 0x4000>;
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_UART1>;
    status = "disabled";
};

这里定义了 UART1 的寄存器基地址、中断号、时钟信息,但默认状态是 disabled。板级 .dts 文件里通过 &uart1 { status = "okay"; }; 来使能它。

我曾经踩过的坑:有一次我在 .dtsi 里把某个外设的 status 写成了 "okay",结果所有用这个 SoC 的板子都默认打开了这个外设。有些板子根本没接这个外设,导致内核启动时访问不存在的硬件,直接挂掉。所以,.dtsi 里尽量保持 status = "disabled",让板级文件去决定是否使能。

1.3.3 .dtb 文件

.dtb 是 Device Tree Blob 的缩写,也就是设备树的二进制文件。它是 .dts 经过编译后的产物,内核启动时直接解析这个二进制数据。

编译命令很简单:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

反过来,你也可以把 .dtb 反编译成 .dts 来查看:

dtc -I dtb -O dts -o myboard.dts myboard.dtb

这个技巧我在调试时经常用。有时候拿到一个现成的 .dtb 文件,想知道它里面定义了哪些硬件,反编译一下一目了然。

1.4 设备树的核心语法速览

咱们快速过一下设备树的基本语法,后面调试时会频繁用到。

语法元素 说明 示例
节点 用 node-name@address 表示 uart1: serial@02020000
属性 key = value; 形式 reg = <0x02020000 0x4000>;
标签 节点前面的别名,用于引用 uart1:
引用 用 &label 引用节点 &uart1 { status = "okay"; };
包含 #include "file" #include "imx6ull.dtsi"
宏定义 配合 #define 使用 #define GPIO_ACTIVE_HIGH 0

1.5 设备树知识体系结构图

下面这张图总结了设备树的核心知识体系,方便你建立整体认知:

设备树知识体系结构 设备树 (Device Tree) 为什么需要? 文件结构 核心语法 解耦硬件与驱动 减少内核分支 动态配置能力 社区标准(ARM64/RISC-V) .dts 源文件 .dtsi 包含文件 .dtb 二进制文件 dtc 编译工具 节点定义 属性赋值 标签与引用 包含与宏定义 设备树 = 硬件描述 + 驱动匹配 + 动态配置

1.6 小结

设备树不是什么高深莫测的东西。它就是一份硬件配置清单,告诉内核:“嘿,我这块板子上有什么外设,它们挂在哪个地址,用哪个中断。” 你只要把这份清单写对,驱动就能自动匹配上。

我个人建议初学者先别急着背语法,而是多看看现成的 .dts 文件。内核源码里 arch/arm/boot/dts/ 目录下有成百上千个例子,随便挑一个跟自己板子相近的,对照着看,很快就能上手。

一个小技巧:调试时如果发现某个外设不工作,先用 dtc -I dtb -O dts 反编译 .dtb,看看设备树里这个节点的 status 是不是 "okay",reg 地址对不对。我遇到过好几次,问题就出在设备树里某个属性写错了。

好了,设备树基础就回顾到这里。记住三个关键词:.dts 是源码,.dtsi 是共用头文件,.dtb 是内核吃的东西。后面咱们会深入调试技巧,到时候这些基础概念会反复用到。