2. 设备树简介:设备树起源、语法基础与内核作用

2.1 设备树的起源:从“硬编码”到“描述性”的演进

在早期的 Linux 内核(如 2.4/2.6 时代),嵌入式板级支持包(BSP)中充斥着大量 arch/arm/mach-xxx 下的 C 语言代码,用于静态注册平台设备、定义内存地址、中断号和 GPIO 配置。这种“硬编码”方式导致内核源码与特定硬件板卡高度耦合,每更换一款开发板就需要修改内核源码并重新编译,严重阻碍了代码复用与社区维护。

设备树(Device Tree,DT)的引入彻底改变了这一局面。它起源于 Open Firmware(IEEE 1275)规范,被 PowerPC 架构率先采用,随后在 ARM 架构社区的大力推动下,成为嵌入式 Linux 平台的标准硬件描述语言。其核心思想是:将硬件配置信息从内核源码中剥离,以树形结构的数据文件(.dts)独立描述,由内核在启动时解析并动态生成平台设备。

关键转折点: Linux 3.x 内核开始强制要求 ARM 架构新平台必须使用设备树,从此告别了“每板一源码”的混乱时代。
类比理解: 设备树就像一张“硬件清单 + 连接图”,告诉内核“我有哪些外设、它们挂在哪里、怎么配置”,而不是让内核自己去猜或硬编码。
设备树起源与演进时间线 Open Firmware IEEE 1275 规范 PowerPC 采用 早期架构移植 ARM 社区推动 Linux 3.x 强制 当前标准 ePAPR / Devicetree 设备树将硬件描述与内核源码解耦,实现“一套内核,多板支持” .dts → .dtb → 内核解析 → 动态创建设备

2.2 设备树语法基础:.dts / .dtsi / .dtb 与节点结构

设备树使用文本格式的源文件(.dts)和头文件(.dtsi),通过 dtc(Device Tree Compiler)编译为二进制格式的 .dtb(Device Tree Blob),由 bootloader 传递给内核。其语法简洁,采用树形层级结构,核心元素包括:

文件类型 扩展名 作用 编译产物
源文件 .dts 描述单板硬件,包含根节点与所有外设节点 .dtb
头文件 .dtsi SoC 公共定义、复用节点,通过 #include 包含 —
二进制 .dtb 内核启动时解析的二进制硬件描述 —
叠加层 .dtbo 运行时动态修改设备树(如 FPGA 加载) .dtbo

基本节点语法:

/dts-v1/;

#include "imx6ull.dtsi"

/ {
    model = "Freescale i.MX6 ULL EVK Board";
    compatible = "fsl,imx6ull-evk", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

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

    &ecspi1 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi1>;
        cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>;
        status = "okay";

        spidev@0 {
            compatible = "spidev";
            reg = <0>;
            spi-max-frequency = <20000000>;
        };
    };
};
常见陷阱: 节点名称中的 @ 后面跟的是地址(如 memory@80000000),用于区分同一层级下相同名称的节点,并非设备地址的完整定义。reg 属性才是真正的地址与长度描述。

核心语法要素速查表:

语法元素 示例 说明
根节点 / { }; 设备树的起始点,所有设备节点都是其子节点
节点名 cpu@0 节点名称 + 可选地址单元,地址用于区分同名节点
属性 compatible = "vendor,device"; 驱动匹配的关键字符串,格式为“厂商,设备型号”
地址属性 reg = <0x10000000 0x1000>; 描述设备地址空间:起始地址 + 长度
中断属性 interrupts = <0 29 4>; 中断号、触发类型等,依赖 interrupt-parent
标签引用 &uart1 { ... }; 通过标签(label)引用并扩展已定义的节点
包含文件 #include "imx6ull.dtsi" 引入 SoC 级公共定义,支持 C 预处理宏
设备树节点层级结构示例 / (根节点) cpus memory@80000000 soc chosen ecspi1 uart1 spidev@0 每个节点通过属性描述硬件特征,子节点继承父节点总线上下文 例如:spidev@0 位于 ecspi1 总线节点下,自动继承 SPI 总线协议

2.3 设备树在 Linux 内核中的作用

设备树在内核启动流程中扮演着“硬件配置数据库”的角色,其核心作用贯穿于内核初始化的多个阶段:

  1. 启动阶段硬件识别: 内核启动早期,通过解析 compatible 属性匹配 machine_desc(板级描述符),确定当前运行的硬件平台。
  2. 设备节点动态创建: 内核遍历设备树中的每个节点,根据 compatible 属性查找对应的 platform_driver,匹配成功后调用 probe() 函数,动态创建 platform_device。
  3. 资源自动分配: 内核从设备树节点中读取 reg、interrupts、gpios、clocks 等属性,自动完成 I/O 内存映射、中断注册、GPIO 申请和时钟使能。
  4. 总线与从设备枚举: 对于 I2C、SPI 等总线控制器节点,内核解析其子节点,自动在对应总线上注册从设备,无需手动调用 i2c_new_device() 或 spi_new_device()。
  5. 电源管理与休眠支持: 设备树中的 power-domains、wakeup-source 等属性,为内核电源管理框架提供设备级功耗控制依据。
核心价值总结: 设备树实现了“硬件描述”与“内核驱动”的完全解耦。更换硬件板卡时,只需更换 .dtb 文件,无需修改内核源码,极大提升了嵌入式 Linux 的可移植性和维护效率。

设备树与内核各子系统的交互关系:

内核子系统 设备树提供的属性/节点 作用
平台设备驱动 compatible、reg 驱动匹配与 I/O 资源获取
中断子系统 interrupts、interrupt-parent 中断控制器层级与中断号映射
GPIO 子系统 gpios、gpio-controller GPIO 引脚申请与配置
时钟框架 clocks、clock-names 时钟源选择与频率配置
引脚控制 pinctrl-* 引脚复用功能与电气特性设置
DMA 引擎 dmas、dma-names DMA 通道分配与传输配置
电源管理 power-domains、wakeup-source 设备级电源域与唤醒能力控制
学习建议: 在阅读内核源码时,可以重点关注 drivers/of/ 目录下的 of_device.c、of_platform.c 和 of_irq.c,它们实现了设备树节点到内核设备对象的完整转换逻辑。