3、历史背景:从平台文件到设备树的演进
在Linux内核发展的早期阶段,ARM架构的板级支持代码(Board Support Package,BSP)中充斥着大量的平台文件(Platform File)。这些文件以C语言结构体的形式,硬编码了开发板上几乎所有的硬件资源信息:从内存起始地址、中断号、GPIO编号,到I2C控制器、SPI总线、USB Host控制器,乃至一个简单的LED指示灯连接在哪个引脚上。这种“代码即硬件描述”的方式,在Linux 2.6时代曾是主流做法。然而,随着嵌入式设备种类爆炸式增长、多核SoC(System on Chip)的普及以及硬件外设的日益复杂,这种模式逐渐暴露出致命的缺陷。设备树(Device Tree,DT)正是在这一背景下,由PowerPC架构率先实践,最终被ARM架构采纳并全面推广的硬件描述机制。本章将深入剖析这一演进过程,理解其背后的技术逻辑与工程价值。
上图清晰地展示了从平台文件到设备树的演进路径。在深入细节之前,我们需要理解一个核心问题:为什么C语言结构体描述硬件的方式会被淘汰?其根本原因在于,平台文件将“硬件是什么”与“驱动怎么做”两件事强行绑定在同一个编译单元中。当硬件发生变化时,即使驱动逻辑完全不变,也必须重新编译整个内核。而设备树则提供了一种全新的视角:硬件信息被抽象为一种独立于内核源码的数据结构,由Bootloader在启动时加载并传递给内核。这种解耦,是嵌入式Linux工程实践的一次重大飞跃。
3.1 核心原理:平台文件的困境与设备树的解耦哲学
要理解设备树的必要性,必须先剖析平台文件模式的深层缺陷。在Linux 2.6时代,一个典型的ARM板级文件(如 arch/arm/mach-imx/mach-mx27_3ds.c)通常包含以下内容:
- 静态内存映射:通过
iotable_init()函数建立物理地址到虚拟地址的静态映射表。 - 平台设备注册:使用
platform_device_register()注册I2C、SPI、USB等外设控制器。 - 板级特定信息:如GPIO按键、LED、以太网PHY地址、LCD面板参数等。
- 资源结构体:定义
struct resource数组,描述设备的起始地址、中断号等。
这种模式的致命问题在于“一板一文件”。以NXP i.MX6系列SoC为例,该芯片被数百家不同公司用于数千种不同的产品设计。如果每个产品都需要在Linux内核中维护一个独立的 board-xxx.c 文件,那么内核社区将面临巨大的维护灾难。更严重的是,许多板级文件之间存在大量重复代码,仅仅因为某个GPIO引脚定义不同,就需要复制整个文件并修改几行。
设备树的引入彻底改变了这一局面。其核心哲学是:内核只负责“如何驱动”,设备树负责“描述有什么”。设备树源文件(DTS)通过一种类似文件系统的树形结构,将硬件设备表示为节点(Node)和属性(Property)。例如,一个I2C控制器节点可能包含以下信息:
i2c1: i2c@21a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <0 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_I2C1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
};
这段描述中,compatible 属性是内核驱动匹配的关键字符串,reg 定义了寄存器地址范围,interrupts 描述了中断信号。内核中的I2C驱动只需通过 of_i2c_register_devices() 等API解析这些节点,即可自动完成设备注册,无需任何板级代码。
3.2 判断方法:何时必须使用设备树?如何识别旧式平台文件?
在实际工程中,判断一个硬件平台是否已完全迁移到设备树,或者是否仍在使用旧式平台文件,是进行内核移植或驱动开发的第一步。以下是几种实用的判断方法:
- 检查内核源码目录结构:在Linux内核源码中,如果
arch/arm/mach-xxx/目录下存在大量的board-*.c文件,且这些文件包含MACHINE_START宏定义,说明该平台仍支持旧式启动方式。如果该目录下主要是Kconfig、Makefile和少量platsmp.c等通用文件,则说明已完全设备树化。 - 查看启动日志:在串口控制台观察内核启动信息。如果出现
Machine model: xxx字样,说明内核是从设备树中获取板卡型号信息的。如果出现Machine: xxx且没有后续的DT相关日志,则可能是平台文件方式。 - 检查Bootloader传递参数:U-Boot等Bootloader通过
bootz或bootm命令加载内核时,如果使用了${fdt_addr}变量来传递DTB地址,则说明启用了设备树。如果仅传递ATAGS(传统参数列表),则未使用设备树。 - 查看设备节点:在Linux用户空间执行
ls /proc/device-tree/或ls /sys/firmware/devicetree/base/,如果能看到model、compatible等文件,说明内核已成功解析设备树。
值得注意的是,虽然ARM架构在2012年左右(Linux 3.7内核)开始强制要求新平台必须使用设备树,但仍有部分老旧的SoC(如三星S3C2440、TI OMAP3等)保留了平台文件支持。对于这些平台,内核提供了 CONFIG_ARM_ATAG_DTB_COMPAT 等兼容选项,允许在启动时通过ATAGS传递部分参数,但这种方式仅作为过渡方案。
3.3 案例拆解:从board-xxx.c到DTS的移植实战
为了更直观地展示设备树带来的变革,我们以一个简化的案例来对比移植过程。假设我们需要在NXP i.MX6ULL开发板上添加一个I2C接口的温度传感器(如LM75)。
平台文件时代的做法(假设存在board文件):
// 在 board-mx6ull_evk.c 中添加
static struct i2c_board_info __initdata mx6ull_i2c1_devices[] = {
{
I2C_BOARD_INFO("lm75", 0x48),
.type = "lm75",
},
};
static void __init mx6ull_evk_init(void)
{
// ... 其他初始化
i2c_register_board_info(1, mx6ull_i2c1_devices,
ARRAY_SIZE(mx6ull_i2c1_devices));
}
这种方式的问题在于:如果另一个客户使用相同的SoC,但将LM75连接到了I2C2总线上,且地址改为0x49,那么就必须修改这个board文件,重新编译内核。如果客户有100种不同的硬件配置,就需要维护100个不同的内核镜像。
设备树时代的做法:
首先,在设备树源文件(如 imx6ull-evk.dts)中,找到I2C1节点的定义,然后添加子节点:
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
lm75: lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
然后,在对应的 imx6ull-pinfunc.h 头文件中确认I2C1的引脚复用定义,并在 imx6ull-evk.dtsi 中配置pinctrl:
&iomuxc {
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6ULL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6ULL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
};
最后,编译设备树:
make imx6ull-evk.dtb
整个过程无需修改任何C代码,只需编辑文本文件并重新编译DTB。当硬件变更时,只需修改DTS文件,重新生成DTB,而内核镜像保持不变。这大大缩短了产品开发周期,并使得同一内核可以支持一个产品系列中的多个变体。
imx6ull.dtsi 包含SoC内部所有外设控制器的默认描述,而 imx6ull-evk.dts 只包含该开发板特有的设备(如外部PHY、传感器、LCD等)。这种分层设计可以最大化代码复用。
3.4 常见误区与总结复盘
在从平台文件向设备树迁移的过程中,工程师常常会陷入一些思维定式或操作误区。以下是几个典型的错误认知及正确的应对策略:
| 误区 | 错误认知 | 正确理解 |
|---|---|---|
| 误区一:设备树是编程语言 | 试图在DTS中编写if-else逻辑或循环 | DTS是纯数据描述,所有逻辑判断应放在驱动代码中 |
| 误区二:设备树可以描述一切 | 认为所有硬件细节都必须放入设备树 | 设备树主要描述“可枚举”的设备,对于模拟信号、电源时序等仍需驱动代码配合 |
| 误区三:修改设备树无需编译 | 直接修改DTS文件后重启系统 | DTS必须通过 dtc 工具编译为DTB二进制,Bootloader加载的是DTB |
| 误区四:设备树与平台文件可以随意混用 | 在设备树中引用平台文件定义的全局变量 | 两者是互斥的启动方式,设备树模式下平台文件中的 platform_device_register 调用会被忽略 |
总结复盘:设备树的演进并非简单的技术替换,而是嵌入式Linux开发模式的深刻变革。它带来了三个核心收益:硬件描述与内核代码解耦,使得SoC厂商可以独立发布内核,板卡厂商只需提供DTS文件;多板卡支持,一个内核镜像可以通过不同的DTB适配多种硬件;社区协作效率提升,减少了大量重复的板级代码,使得内核维护者可以更专注于驱动质量和性能优化。
然而,设备树并非银弹。它引入了新的学习曲线(DTS语法、dtc工具链、Bootloader配合),并且在调试时增加了间接性——当硬件不工作时,需要同时排查DTS描述是否正确、驱动是否匹配、Bootloader是否传递了正确的DTB。因此,深入理解设备树的编译过程、匹配机制和运行时行为,是每一位嵌入式Linux工程师的必修课。
fdt 命令支持。对于生产中的产品,建议先在开发板上验证DTS的正确性,再逐步淘汰平台文件。此外,注意 compatible 字符串的命名规范,应遵循“厂商,型号”的格式,避免与内核驱动不匹配。