1. 设备树基础概念

各位同学好,我是老李。做嵌入式Linux驱动开发十几年了,今天咱们来聊聊设备树。说实话,我刚入行那会儿还没有设备树这玩意儿,那时候写驱动,板级信息全是硬编码在C文件里的。后来换了平台、换了板子,代码改得那叫一个痛苦。设备树的出现,说白了就是要把硬件描述和驱动代码彻底解耦。

什么是设备树

设备树(Device Tree),是一套描述硬件资源的数据结构。它用树形结构把CPU、内存、外设、中断、GPIO、时钟这些硬件信息组织起来。内核启动时,Bootloader把设备树二进制文件(DTB)传给内核,内核解析后就知道板子上有什么硬件、怎么访问它们。

我习惯把设备树比作一张硬件“配置单”。你想想看,驱动代码是通用的,但板子上的硬件资源是千差万别的。设备树就是告诉驱动:“我这块板子的UART挂在哪个地址上,中断号是多少,时钟频率是多少。”驱动拿到这些信息,就能正确初始化硬件。

设备树的核心组成:

  • DTS(Device Tree Source):文本格式的源文件,我们工程师直接编写
  • DTC(Device Tree Compiler):编译器,把DTS转成DTB
  • DTB(Device Tree Blob):二进制格式,内核实际使用的数据
  • DTBO(Device Tree Blob Overlay):叠加层,用于动态修改设备树

举个例子,一个简单的设备树节点长这样:

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard";

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
        clock-frequency = <24000000>;
        status = "okay";
    };
};

这段代码描述了一个UART控制器,地址在0x10000000,中断号33,时钟24MHz。驱动通过of_match_table匹配compatible属性,就能找到对应的硬件资源。

为什么需要设备树

在设备树出现之前,Linux内核是怎么处理板级信息的?答案是板级文件(Board File)。每个板子都要在arch/arm/mach-xxx目录下写一个C文件,里面硬编码了所有硬件资源。比如:

static struct resource uart_resources[] = {
    {
        .start = 0x10000000,
        .end   = 0x10000fff,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = IRQ_UART0,
        .end   = IRQ_UART0,
        .flags = IORESOURCE_IRQ,
    },
};

这种方式有什么问题?我遇到过最头疼的情况:同一个SoC,客户换了颗DDR颗粒,内存地址变了。我得改板级文件、重新编译内核、烧录验证。如果同时维护五六块板子,那简直是噩梦。

设备树解决了三个核心痛点:

  • 硬件描述与驱动解耦:驱动代码不再包含板级信息,换板子只需换DTB
  • 减少内核分支:同一份内核镜像可以支持多种硬件配置
  • 社区统一标准:ARM、RISC-V、x86都在用,生态统一

我的经验:曾经有个项目,同一款SoC用在三款不同产品上,外设配置差异很大。如果没有设备树,我得维护三份内核代码。用了设备树之后,只需要一份内核镜像,启动时加载不同的DTB文件就行。开发效率提升不止一个量级。

设备树与ACPI的区别

ACPI(高级配置与电源管理接口)是x86平台的标准硬件描述方式。很多从x86转过来的同学会问:设备树和ACPI到底有什么区别?我整理了一张对比表:

对比项 设备树(DT) ACPI
起源平台 PowerPC / ARM x86 / IA64
描述方式 文本(DTS)→ 二进制(DTB) ASL → AML(字节码)
运行时修改 支持Overlay动态叠加 支持DSDT动态修改
电源管理 需要驱动自行实现 内置电源管理方法
热插拔支持 较弱,需额外机制 原生支持
复杂度 轻量,学习曲线平缓 庞大,规范上千页
适用场景 嵌入式、IoT、FPGA 服务器、PC、笔记本

说白了,ACPI更像一个“虚拟机”,它有一套自己的语言(ASL)和解释器(AML),能执行复杂的电源管理策略。设备树则简单得多,它只描述硬件“长什么样”,不描述硬件“怎么用”。

避坑指南:我曾经见过有人试图在ARM服务器上用ACPI替代设备树。理论上可行,但实际坑很多。ACPI的AML解释器在ARM上性能不佳,而且很多嵌入式外设(比如I2C传感器、SPI设备)在ACPI规范里根本没有标准描述方式。我的建议是:ARM平台老老实实用设备树,别折腾ACPI。

设备树在Linux内核中的角色

设备树在内核启动流程中扮演着“硬件信息传递者”的角色。从Bootloader到内核初始化,再到驱动加载,设备树贯穿始终。

我画了一张流程图,展示设备树在内核中的生命周期:

设备树在内核中的生命周期 Bootloader加载DTB 内核解压并解析DTB unflatten_device_tree() of_platform_populate() 驱动匹配(compatible) of_iomap() / of_get_property() 驱动probe()执行 运行时访问设备树(of_xxx API) 设备树Overlay动态加载 系统关闭,释放设备树 说明: 设备树在内核启动时被解析为内存中的struct device_node树,驱动通过of_xxx API访问。 Overlay机制允许系统运行时动态添加/移除设备节点,常用于FPGA重配置场景。

从这张图可以看出,设备树在内核中经历了加载→解析→展开→匹配→使用→释放的完整生命周期。其中几个关键环节:

  • unflatten_device_tree():把扁平化的DTB展开成树形结构,每个节点对应一个struct device_node
  • of_platform_populate():遍历设备树,为每个compatible节点创建platform_device
  • 驱动匹配:内核根据compatible、type、name等属性,找到对应的驱动
  • 资源获取:驱动通过of_iomap()映射寄存器,of_get_property()读取属性

我的习惯:调试设备树问题时,我经常在drivers/of/目录下加打印,或者用of_dump内核参数查看设备树展开后的完整结构。另外,/proc/device-tree目录在运行时可以直接查看设备树内容,非常方便。

嗯,这里要注意一点:设备树不是万能的。它只描述硬件“有什么”,不描述硬件“怎么用”。比如一个I2C设备,设备树告诉驱动它的地址和中断号,但具体的通信协议、寄存器操作,还是得驱动代码自己实现。设备树是“数据”,驱动是“逻辑”,两者配合才能正常工作。

好了,设备树的基础概念就讲到这里。记住一句话:设备树是硬件和驱动之间的“翻译官”,它让硬件描述标准化、可复用。后面的章节,我们会深入设备树的语法、绑定规则、以及如何编写高质量的设备树文件。