1. 设备树基础:什么是设备树?为什么需要设备树?设备树文件结构(dts、dtsi、dtb)

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

说实话,我刚开始接触嵌入式Linux那会儿,也被设备树搞得一头雾水。那时候还是古老的板级文件(board file)时代,每个板子都要写一堆C代码来描述硬件。后来设备树出现了,很多人觉得麻烦,但用久了你会发现——这玩意儿真香。

1.1 什么是设备树?

设备树,英文叫Device Tree,简称DT。说白了,它就是一个描述硬件信息的配置文件。你想想看,一个嵌入式板子上有CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器……这些硬件怎么连接?地址是多少?中断号是多少?设备树就是干这个的。

设备树的核心思想是:把硬件描述从内核代码中分离出来。这样,同一份内核镜像可以跑在不同硬件上,只需要更换设备树文件就行。

重要概念:设备树是一个树形结构的数据格式,每个硬件设备都是一个"节点"(node),节点包含若干"属性"(property),用来描述设备的配置信息。

1.2 为什么需要设备树?

在设备树出现之前,Linux内核是怎么处理硬件的?

  • 板级文件时代(2.6内核及之前):每个板子都在arch/arm/mach-xxx/目录下有一个board-xxx.c文件,里面用C代码硬编码了所有硬件信息。比如内存基地址、GPIO引脚分配、时钟频率等。
  • 问题来了:如果你有100种板子,就要写100个board文件。而且每次硬件改动,都要重新编译内核。我在一个项目中就遇到过,客户换了颗Flash芯片,我不得不改board文件、重新编译、烧录……折腾了一整天。

设备树解决了这个问题:

  1. 硬件描述与内核代码解耦:内核不需要知道具体硬件细节,它只管解析设备树。
  2. 减少内核代码量:大量板级文件被移除,内核变得更干净。
  3. 支持多平台:同一份内核镜像,通过更换dtb文件就能适配不同硬件。
  4. 社区标准化:设备树规范由ePAPR标准定义,各大厂商都遵循。

我的经验:有一次调试一块新板子,硬件工程师说"GPIO引脚改了",我只需要改dts文件,重新编译dtb,然后替换掉启动分区里的dtb文件就行。内核都不用重新编译,省了不少时间。

1.3 设备树文件结构

设备树涉及三种文件格式:dts、dtsi、dtb。很多人一开始搞不清它们的区别,我来捋一捋。

文件类型 全称 说明 是否可读
.dts Device Tree Source 设备树源文件,文本格式,开发者编写 是(文本)
.dtsi Device Tree Source Include 设备树头文件,可被dts包含,用于复用公共定义 是(文本)
.dtb Device Tree Blob 设备树二进制文件,由dts编译生成,供内核使用 否(二进制)

嗯,这里要注意:dtsi文件是给dts文件include用的。比如,一个SoC厂商会提供一个通用的soc.dtsi,里面定义了该SoC所有内置外设。然后各个板卡厂商在自己的board.dts里include这个soc.dtsi,再添加板级特有的设备。

举个例子,看看一个简单的dts文件长什么样:

/dts-v1/;

#include "imx6ul.dtsi"

/ {
    model = "Freescale i.MX6 UltraLite Board";
    compatible = "fsl,imx6ul-14x14-evk", "fsl,imx6ul";

    memory {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512MB RAM */
    };

    chosen {
        stdout-path = &uart1;
    };

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

这段代码描述了什么?

  • /dts-v1/; —— 版本声明,表示使用设备树v1格式
  • #include "imx6ul.dtsi" —— 包含SoC级定义
  • / { ... } —— 根节点,整个设备树的起点
  • modelcompatible —— 板子名称和兼容性字符串
  • memory 节点 —— 描述内存大小和起始地址
  • chosen 节点 —— 内核启动参数,比如指定控制台串口
  • &uart1 { ... } —— 引用并修改uart1节点,配置引脚复用和使能

避坑指南:我曾经在调试时发现串口不工作,查了半天发现是dts里忘了写status = "okay"。默认情况下,很多外设节点是disabled状态,必须显式使能。这个坑我踩过,你们别踩。

1.4 设备树编译与反编译

dts文件不能直接给内核用,需要编译成dtb二进制文件。工具链里有个叫dtc(Device Tree Compiler)的工具,就是干这个的。

常用命令:

# 编译dts为dtb
dtc -I dts -O dtb -o board.dtb board.dts

# 反编译dtb为dts(调试时很有用)
dtc -I dtb -O dts -o board.dts board.dtb

# 查看dtb内容(人类可读)
fdtdump board.dtb

我个人习惯在调试时先用fdtdump看看dtb里到底有什么,确认编译是否正确。有一次我发现编译出来的dtb大小不对,一查发现是dts里有个语法错误,dtc报了个warning但我没注意……嗯,从那以后我每次编译都会检查返回值。

1.5 设备树的核心语法速览

这里我快速过一下设备树的基本语法规则,后面章节会详细展开:

  • 节点命名node-name@unit-address,比如uart@021e8000
  • 属性格式property-name = <value>;
  • 常见属性类型
    • <u32> —— 32位整数,如reg = <0x80000000 0x20000000>;
    • string —— 字符串,如compatible = "fsl,imx6ul";
    • phandle —— 指针引用,如&uart1
    • bytestring —— 字节数组,如local-mac-address = [00 11 22 33 44 55];
  • 标签(label):给节点起别名,方便引用,如uart1: serial@021e8000

小技巧:写dts时,我习惯先用dtc -@选项开启符号支持,这样可以在编译时保留标签信息,方便后续overlay(设备树叠加层)使用。

1.6 设备树在启动流程中的角色

设备树在系统启动时扮演什么角色?我画了一张图来说明:

设备树在系统启动流程中的角色 Bootloader U-Boot / Barebox 加载dtb到内存 传递dtb地址 Linux内核启动 解压 → 初始化 解析dtb → 创建device_node 注册设备 设备驱动 probe → 初始化 读取设备树属性 详细流程分解 1. 编译dts → dtb 2. Bootloader加载dtb 3. 内核解析dtb 4. 驱动匹配probe 设备树是硬件描述与内核之间的桥梁 没有设备树,内核就得硬编码硬件信息,维护成本极高 核心:设备树让"一次编译,多处运行"成为可能

从这张图可以看出,设备树贯穿了整个启动流程:

  1. 开发阶段:编写dts/dtsi源文件
  2. 编译阶段:用dtc编译成dtb二进制文件
  3. 启动阶段:Bootloader(如U-Boot)将dtb加载到内存,并把地址传给内核
  4. 内核阶段:内核解析dtb,创建device_node结构体,然后与驱动匹配
  5. 驱动阶段:驱动probe时,从设备树中读取硬件配置参数

一句话总结:设备树就是硬件的"身份证",内核通过它来认识板子上有什么、在哪里、怎么用。

好了,这一章我们聊了设备树是什么、为什么需要它、以及三种文件格式的区别。下一章我会带大家深入设备树的语法细节,包括节点、属性、中断映射、GPIO控制等实战内容。到时候我会拿一个真实的板子dts文件来逐行分析,保证让你看得明明白白。


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