课程导入:设备树的历史背景、为什么需要设备树、学习目标与课程整体框架
设备树(Device Tree)是嵌入式Linux系统开发中绕不开的核心概念。无论是驱动开发者、BSP工程师,还是系统集成人员,几乎每天都会与 .dts、.dtsi、dtc 这些文件打交道。然而,很多工程师对设备树的理解停留在“改一改引脚复用、调一调时钟频率”的层面,一旦遇到复杂的外设配置、多板级兼容、中断映射或PCIe枚举问题,就感到无从下手。本章作为整个课程的开篇,将从设备树诞生的历史背景讲起,剖析它为何能取代传统的“硬编码注册”方式,并给出清晰的学习路径与课程框架,帮助你在后续29章的学习中建立全局视野。
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的事实标准。
判断方法:如何识别一个系统是否使用设备树
在实际工作中,你可以通过以下方式快速判断目标系统是否启用了设备树:
- 查看内核启动日志:如果出现
Machine model: XXX或DTB found字样,说明使用了设备树。 - 检查内核配置:
CONFIG_OF选项被启用(y或m)。 - 查看启动参数:bootloader(如U-Boot)传递的是
.dtb文件地址,而非传统的ATAG参数。 - 查看内核源码目录:
arch/arm/mach-xxx/下不再有大量board-*.c文件,取而代之的是arch/arm/boot/dts/下的.dts文件。
案例拆解:一块开发板从传统方式到设备树的迁移
假设我们有一块基于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,内核镜像完全不用动。
常见误区与总结复盘
另一个常见误区是认为“有了设备树就不需要写板级代码了”。事实上,设备树只是替代了“硬件资源注册”部分,对于复杂的板级初始化(如DDR时序校准、特殊电源时序),仍然需要少量板级代码配合。设备树的目标是减少而非消除板级代码。
1.2 为什么需要设备树:解决三大核心痛点
核心原理:设备树带来的范式转变
设备树之所以成为嵌入式Linux的基石,是因为它从根本上解决了传统方式的三大痛点:
| 痛点 | 传统方式表现 | 设备树解决方案 |
|---|---|---|
| 硬件描述与代码耦合 | 寄存器地址、中断号写在C代码中 | 硬件信息以节点/属性形式独立描述 |
| 多板支持困难 | 每块板需要单独编译内核 | 同一内核镜像 + 不同DTB即可适配 |
| 驱动复用性差 | 驱动依赖特定板级宏定义 | 驱动通过OF API获取资源,与具体板卡解耦 |
从工程角度看,设备树还带来了额外收益:硬件描述文件(.dts)可以像源代码一样进行版本管理,硬件改版时通过diff即可清晰看到变更点;外设配置可以模块化,通过 .dtsi 文件实现SoC级、板级、项目级的分层复用;便于自动化验证,可以通过dtc编译器检查语法错误,通过dt-validate工具校验与内核binding的兼容性。
操作流程:设备树在系统启动中的完整链路
理解设备树的工作流程,是后续学习的基础。整个链路如下:
- 编写源文件:开发人员编写
.dts(板级描述)和.dtsi(SoC级描述)文件。 - 编译生成DTB:使用
dtc工具将.dts编译为二进制.dtb文件。 - Bootloader加载:U-Boot等bootloader将
.dtb从存储介质(eMMC、SD卡、网络)加载到内存指定地址。 - 内核解析:内核启动早期调用
unflatten_device_tree()将DTB展开为树状结构,存入struct device_node链表。 - 驱动匹配:内核遍历设备树中的节点,为每个
compatible属性匹配对应的驱动,并创建platform_device。 - 资源获取:驱动通过
of_property_read_u32()、of_iomap()等API读取设备树中的属性值,完成初始化。
案例拆解:同一内核适配两块不同开发板
假设我们有两块开发板: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,内核镜像完全一致。这种“一次编译、多板运行”的模式,极大降低了产品线的维护成本。
常见误区与总结复盘
另一个误区是忽略 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、运行时修改、安全启动、性能优化 | 能解决产品化中的实际问题 |
学习方法建议
设备树是一门“实践出真知”的技术。建议你在学习过程中遵循以下方法:
- 对照硬件手册学习:每学一个子系统(如GPIO),就翻开SoC数据手册,找到对应的寄存器地址和引脚复用表,在设备树中逐一对应。
- 动手修改并验证:在开发板上尝试修改设备树中的某个属性(如时钟频率),观察内核日志和实际行为的变化。
- 善用反编译工具:将内核自带的
.dtb反编译为.dts,学习厂商BSP的写法风格。 - 建立错误日志敏感度:遇到设备无法识别时,先看
dmesg中是否有OF:开头的错误信息,这往往是设备树问题的第一线索。
of_property_read_u32、of_get_named_gpio、of_irq_get 等函数,在实战中逐步熟悉。本课程第7章会提供完整的OF API参考手册。
案例拆解:一个典型的设备树学习路径
假设你接到一个任务:为一块新的ARM开发板添加一个I2C温度传感器(如LM75)。一个完整的学习闭环应该是:
- 查看原理图:确认LM75挂载在哪个I2C总线上(如I2C2),地址是多少(如0x48)。
- 查看SoC手册:确认I2C2的寄存器基地址、中断号、引脚复用选项。
- 编写设备树节点:在
.dtsi中找到I2C2节点,添加LM75子节点,配置compatible、reg、interrupt-parent等属性。 - 配置pinctrl:在
&iomuxc中添加I2C2的引脚复用组。 - 编译并烧录:使用
make dtbs编译,将新的.dtb烧录到开发板。 - 验证驱动:加载
lm75驱动,通过cat /sys/class/hwmon/hwmon0/temp1_input读取温度值。 - 调试排障:如果读取失败,使用
i2cdetect -y 2检查设备是否在总线上,用dmesg查看OF解析是否成功。
这个过程贯穿了设备树学习的全部核心技能:语法、编译、子系统知识、调试方法。本课程的章节安排正是按照这个逻辑展开的。
常见误区与总结复盘
.dts 文件往往包含大量冗余或特定配置,直接复制到自己的项目中可能导致资源冲突或功能异常。务必理解每个属性的含义后再做修改。
最后需要强调的是,设备树不是万能的。它适合描述“静态硬件拓扑”,但对于动态配置(如USB热插拔、PCIe枚举),设备树只提供初始信息,真正的运行时管理仍由内核子系统完成。理解设备树的边界,是成为合格嵌入式工程师的重要一步。
通过本章的学习,你已经建立了设备树的宏观认知。接下来的第2章,我们将正式进入设备树语法世界,从最简单的节点和属性开始,逐步构建完整的硬件描述能力。