课程导入:设备树的历史背景、为什么需要设备树、学习目标与课程整体框架

设备树(Device Tree)是嵌入式Linux系统开发中绕不开的核心概念。无论是驱动开发者、BSP工程师,还是系统集成人员,几乎每天都会与 .dts、.dtsi、dtc 这些文件打交道。然而,很多工程师对设备树的理解停留在“改一改引脚复用、调一调时钟频率”的层面,一旦遇到复杂的外设配置、多板级兼容、中断映射或PCIe枚举问题,就感到无从下手。本章作为整个课程的开篇,将从设备树诞生的历史背景讲起,剖析它为何能取代传统的“硬编码注册”方式,并给出清晰的学习路径与课程框架,帮助你在后续29章的学习中建立全局视野。

设备树核心概念与演进关系图 传统硬编码方式 • 板级文件 board.c • 平台设备结构体 • 资源硬编码注册 • 代码与硬件强耦合 • 每板需重新编译内核 问题:维护困难、无法通用 设备树 (Device Tree) • .dts / .dtsi 源文件 • dtc 编译为 .dtb • 描述硬件拓扑结构 • 驱动与硬件解耦 • 同一内核支持多板 优势:可维护、可扩展、标准化 学习目标 • 掌握DTS语法 • 理解匹配机制 • 熟练中断/GPIO/时钟 • 独立编写设备树 • 掌握调试与排障 能力:从看懂到实战 演进 赋能 设备树生态关键组件 DTS源文件 dtc编译器 DTB二进制 Bootloader加载 内核解析 设备树描述硬件 → 内核OF API解析 → 驱动匹配与初始化 2005年前 硬编码 2005-2007 PowerPC采用 2011-2012 ARM Linux 3.x 至今 全架构标配

1.1 设备树的历史背景:从硬编码到描述性配置

核心原理:为什么传统方式走到了尽头

在设备树出现之前,Linux内核通过平台设备(Platform Device)和平台驱动(Platform Driver)机制来管理板级硬件。每个开发板都需要在 arch/arm/mach-xxx/ 目录下编写一个 board-xxx.c 文件,其中通过 platform_device_register() 或 platform_add_devices() 将外设资源(如寄存器地址、中断号、DMA通道)硬编码注册到内核中。这种方式在早期硬件简单、板型稀少时尚可接受,但随着ARM架构的爆发式增长,问题迅速暴露:

  • 代码冗余严重:每增加一块新板卡,就要复制粘贴大量相似的注册代码,Linux内核中 arch/arm/mach-* 目录急剧膨胀。
  • 硬件变更成本高:修改一个GPIO引脚或中断号,必须重新编译整个内核,无法实现“一次编译、多板运行”。
  • 驱动与硬件耦合:驱动代码中充斥着 #ifdef CONFIG_BOARD_XXX 之类的条件编译,可读性和可维护性极差。
  • 社区协作困难:不同厂商的板级文件风格各异,代码review和合并效率低下。

设备树(Device Tree)的核心理念是:将硬件描述从内核代码中剥离出来,用独立的数据结构描述“硬件长什么样”,内核启动时解析这份描述,动态创建平台设备。这一思想最早源于PowerPC架构(由Open Firmware项目演化而来),2005年前后开始被PowerPC Linux采用,2011年ARM Linux 3.x内核开始大规模引入,到2012年Linus Torvalds明确要求ARM架构必须使用设备树后,它成为嵌入式Linux的事实标准。

判断方法:如何识别一个系统是否使用设备树

在实际工作中,你可以通过以下方式快速判断目标系统是否启用了设备树:

  1. 查看内核启动日志:如果出现 Machine model: XXX 或 DTB found 字样,说明使用了设备树。
  2. 检查内核配置:CONFIG_OF 选项被启用(y 或 m)。
  3. 查看启动参数:bootloader(如U-Boot)传递的是 .dtb 文件地址,而非传统的 ATAG 参数。
  4. 查看内核源码目录:arch/arm/mach-xxx/ 下不再有大量 board-*.c 文件,取而代之的是 arch/arm/boot/dts/ 下的 .dts 文件。
经验提醒:即使你的项目使用的是旧内核(如Linux 2.6),如果硬件平台较新,也建议尽早迁移到设备树。因为主流SoC厂商(NXP、TI、Rockchip、Allwinner等)的BSP已经全面转向设备树,继续使用传统方式将无法获得厂商的持续支持。

案例拆解:一块开发板从传统方式到设备树的迁移

假设我们有一块基于NXP i.MX6ULL的评估板,传统方式下需要在 board-mx6ull_evk.c 中注册一个UART设备:

static struct resource uart1_resources[] = {
    {
        .start = 0x02020000,
        .end   = 0x0202003F,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = 32,  // GPIO中断号
        .end   = 32,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device uart1_device = {
    .name = "imx6ull-uart",
    .id   = 0,
    .num_resources = ARRAY_SIZE(uart1_resources),
    .resource = uart1_resources,
};

static int __init mx6ull_evk_init(void)
{
    platform_device_register(&uart1_device);
    return 0;
}
arch_initcall(mx6ull_evk_init);

改用设备树后,同样的硬件描述变为:

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

&iomuxc {
    pinctrl_uart1: uart1grp {
        fsl,pins = <
            MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
            MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
        >;
    };
};

对比可见:设备树方式将寄存器地址、中断号、引脚复用等信息全部“数据化”,驱动代码只需通过OF API(如 of_iomap()、irq_of_parse_and_map())获取资源,不再关心具体板级差异。当硬件改版时,只需修改 .dts 文件并重新编译生成 .dtb,内核镜像完全不用动。

常见误区与总结复盘

风险提示:初学者常误以为设备树是“一种编程语言”或“一种驱动框架”。实际上,设备树只是数据描述格式,它本身不包含任何执行逻辑。真正驱动设备工作的是内核中的OF(Open Firmware)子系统——它负责解析设备树、创建platform_device、并触发驱动匹配。

另一个常见误区是认为“有了设备树就不需要写板级代码了”。事实上,设备树只是替代了“硬件资源注册”部分,对于复杂的板级初始化(如DDR时序校准、特殊电源时序),仍然需要少量板级代码配合。设备树的目标是减少而非消除板级代码。

1.2 为什么需要设备树:解决三大核心痛点

核心原理:设备树带来的范式转变

设备树之所以成为嵌入式Linux的基石,是因为它从根本上解决了传统方式的三大痛点:

痛点 传统方式表现 设备树解决方案
硬件描述与代码耦合 寄存器地址、中断号写在C代码中 硬件信息以节点/属性形式独立描述
多板支持困难 每块板需要单独编译内核 同一内核镜像 + 不同DTB即可适配
驱动复用性差 驱动依赖特定板级宏定义 驱动通过OF API获取资源,与具体板卡解耦

从工程角度看,设备树还带来了额外收益:硬件描述文件(.dts)可以像源代码一样进行版本管理,硬件改版时通过diff即可清晰看到变更点;外设配置可以模块化,通过 .dtsi 文件实现SoC级、板级、项目级的分层复用;便于自动化验证,可以通过dtc编译器检查语法错误,通过dt-validate工具校验与内核binding的兼容性。

操作流程:设备树在系统启动中的完整链路

理解设备树的工作流程,是后续学习的基础。整个链路如下:

  1. 编写源文件:开发人员编写 .dts(板级描述)和 .dtsi(SoC级描述)文件。
  2. 编译生成DTB:使用 dtc 工具将 .dts 编译为二进制 .dtb 文件。
  3. Bootloader加载:U-Boot等bootloader将 .dtb 从存储介质(eMMC、SD卡、网络)加载到内存指定地址。
  4. 内核解析:内核启动早期调用 unflatten_device_tree() 将DTB展开为树状结构,存入 struct device_node 链表。
  5. 驱动匹配:内核遍历设备树中的节点,为每个 compatible 属性匹配对应的驱动,并创建platform_device。
  6. 资源获取:驱动通过 of_property_read_u32()、of_iomap() 等API读取设备树中的属性值,完成初始化。
关键理解:设备树不是“配置内核参数的”,而是“描述硬件拓扑的”。它告诉内核“这里有一个UART,它的寄存器在0x02020000,使用中断32,引脚复用为UART1_TX/RX”。至于这个UART如何工作,那是驱动代码的事。

案例拆解:同一内核适配两块不同开发板

假设我们有两块开发板:Board A 使用eMMC存储,Board B 使用SD卡启动。两块板的SoC相同(都是i.MX6ULL),但存储控制器配置不同。传统方式下,需要为两块板分别编译内核;使用设备树后,只需准备两个 .dtb 文件:

// board-a.dts 关键片段
&usdhc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usdhc1>;
    bus-width = <8>;
    non-removable;
    status = "okay";
};

// board-b.dts 关键片段
&usdhc1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usdhc1>;
    bus-width = <4>;
    cd-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
    status = "okay";
};

U-Boot根据硬件拨码开关或环境变量选择加载 board-a.dtb 或 board-b.dtb,内核镜像完全一致。这种“一次编译、多板运行”的模式,极大降低了产品线的维护成本。

常见误区与总结复盘

风险提示:不要试图在设备树中描述“软件行为”。例如,不要在设备树中写“初始化顺序”或“电源管理策略”——这些属于驱动逻辑,应该放在C代码中。设备树只描述“有什么硬件、硬件在哪里、硬件如何连接”。

另一个误区是忽略 status 属性的作用。很多外设节点默认是 disabled 状态,必须在板级 .dts 中显式改为 okay 才能被内核扫描。忘记修改 status 是驱动“注册不上”的最常见原因之一。

1.3 学习目标与课程整体框架

核心原理:从“会用”到“会写”再到“会调”

设备树的学习不是单纯记住语法,而是建立一套完整的工程思维。本课程将带你经历三个阶段:

  • 基础阶段(第1-8章):掌握设备树语法、节点/属性规则、dtc编译工具、常用OF API,能够读懂并修改现有设备树。
  • 进阶阶段(第9-20章):深入中断控制器、GPIO子系统、时钟框架、pinctrl、DMA、电源管理等复杂场景,能够独立编写完整的外设设备树节点。
  • 实战阶段(第21-30章):结合具体SoC(如i.MX、Rockchip、Allwinner)进行完整BSP移植,学习设备树调试技巧(如 of_dump、dt-validate)、性能优化和产品化注意事项。

课程知识结构总览

模块 章节范围 核心内容 能力目标
基础语法 第2-5章 节点、属性、标签、引用、宏定义 能读懂并修改现有DTS
编译与调试 第6-8章 dtc用法、反编译、常见错误定位 能独立编译和排查语法问题
核心子系统 第9-14章 中断、GPIO、时钟、pinctrl、DMA 能为外设编写完整设备树节点
复杂场景 第15-20章 PCIe、USB、I2C/SPI、Display、音频 能处理多控制器协同场景
实战移植 第21-26章 新板卡适配、内核配置、bootloader配合 能独立完成BSP设备树移植
高级专题 第27-30章 overlay、运行时修改、安全启动、性能优化 能解决产品化中的实际问题

学习方法建议

设备树是一门“实践出真知”的技术。建议你在学习过程中遵循以下方法:

  1. 对照硬件手册学习:每学一个子系统(如GPIO),就翻开SoC数据手册,找到对应的寄存器地址和引脚复用表,在设备树中逐一对应。
  2. 动手修改并验证:在开发板上尝试修改设备树中的某个属性(如时钟频率),观察内核日志和实际行为的变化。
  3. 善用反编译工具:将内核自带的 .dtb 反编译为 .dts,学习厂商BSP的写法风格。
  4. 建立错误日志敏感度:遇到设备无法识别时,先看 dmesg 中是否有 OF: 开头的错误信息,这往往是设备树问题的第一线索。
经验提醒:不要试图一次性记住所有OF API。建议准备一个速查表,记录常用的 of_property_read_u32、of_get_named_gpio、of_irq_get 等函数,在实战中逐步熟悉。本课程第7章会提供完整的OF API参考手册。

案例拆解:一个典型的设备树学习路径

假设你接到一个任务:为一块新的ARM开发板添加一个I2C温度传感器(如LM75)。一个完整的学习闭环应该是:

  1. 查看原理图:确认LM75挂载在哪个I2C总线上(如I2C2),地址是多少(如0x48)。
  2. 查看SoC手册:确认I2C2的寄存器基地址、中断号、引脚复用选项。
  3. 编写设备树节点:在 .dtsi 中找到I2C2节点,添加LM75子节点,配置 compatible、reg、interrupt-parent 等属性。
  4. 配置pinctrl:在 &iomuxc 中添加I2C2的引脚复用组。
  5. 编译并烧录:使用 make dtbs 编译,将新的 .dtb 烧录到开发板。
  6. 验证驱动:加载 lm75 驱动,通过 cat /sys/class/hwmon/hwmon0/temp1_input 读取温度值。
  7. 调试排障:如果读取失败,使用 i2cdetect -y 2 检查设备是否在总线上,用 dmesg 查看OF解析是否成功。

这个过程贯穿了设备树学习的全部核心技能:语法、编译、子系统知识、调试方法。本课程的章节安排正是按照这个逻辑展开的。

常见误区与总结复盘

风险提示:设备树学习中最容易犯的错误是“照搬厂商代码而不理解”。厂商提供的 .dts 文件往往包含大量冗余或特定配置,直接复制到自己的项目中可能导致资源冲突或功能异常。务必理解每个属性的含义后再做修改。

最后需要强调的是,设备树不是万能的。它适合描述“静态硬件拓扑”,但对于动态配置(如USB热插拔、PCIe枚举),设备树只提供初始信息,真正的运行时管理仍由内核子系统完成。理解设备树的边界,是成为合格嵌入式工程师的重要一步。

通过本章的学习,你已经建立了设备树的宏观认知。接下来的第2章,我们将正式进入设备树语法世界,从最简单的节点和属性开始,逐步构建完整的硬件描述能力。