4. 设备树在Linux内核中的角色:内核启动流程中设备树的传递、OF API简介

设备树(Device Tree)在Linux内核中扮演着“硬件描述语言”与“平台配置契约”的双重角色。它从引导加载程序(Bootloader)传递到内核,贯穿整个启动流程,最终被OF(Open Firmware)子系统解析为内核可用的资源列表。理解这一角色,是进行设备树调试的前提。

4.1 设备树在内核启动流程中的传递路径

设备树的传递并非简单的内存拷贝,而是一个经过校验、解压、映射和解析的严格流程。以下表格总结了从引导加载程序到内核完成解析的关键阶段:

阶段 执行主体 关键动作 数据结构/寄存器
1. 加载与准备 Bootloader (U-Boot) 从存储介质读取dtb,放入内存,修改chosen节点 dtb在内存中的物理地址
2. 传递入口 Bootloader → 内核 将dtb物理地址写入寄存器x0(ARM64)或通过ATAG传递 寄存器x0 / r2
3. 早期解压 内核解压代码 若内核被压缩,解压后保留dtb地址;若dtb被压缩,则解压 解压后的dtb内存区域
4. 早期映射 setup_arch() 调用early_init_dt_scan(),扫描chosen、memory节点 boot_command_line, memblock
5. 完整解析 unflatten_device_tree() 将扁平dtb展开为内核的struct device_node树 struct device_node, struct property
6. 驱动绑定 of_platform_populate() 遍历device_node树,创建platform_device platform_device, resource
调试提示: 在启动早期,如果dtb地址传递错误或dtb损坏,内核通常会卡在 Uncompressing Linux... done, booting the kernel. 之后无任何输出。此时应检查Bootloader中 fdt_addr 或 bootm fdt 参数是否正确。

下面这张SVG流程图展示了设备树从Bootloader到内核驱动绑定的完整传递路径:

设备树在内核启动中的传递与解析流程 Bootloader 加载dtb到内存 寄存器 x0 / r2 传递dtb物理地址 early_init_dt_scan() 扫描chosen/memory unflatten_device_tree() 扁平dtb → device_node树 of_address_to_resource() reg属性 → struct resource of_platform_populate() 创建platform_device 关键调试点: • 若 early_init_dt_scan() 失败,cmdline 为空,内存信息丢失。 • 若 unflatten_device_tree() 失败,所有驱动无法匹配 compatible 属性。 • 若 of_platform_populate() 失败,总线设备不会被创建。

4.2 OF API 简介:内核操作设备树的接口

OF(Open Firmware)API 是内核提供给驱动开发者操作设备树的标准接口。它分为三个层次:节点查找、属性读取、资源转换。掌握这些API是编写设备树感知驱动的核心。

4.2.1 核心数据结构

内核使用两个主要结构体表示设备树:

// 表示一个设备节点
struct device_node {
    const char *name;           // 节点名称,如 "uart@fe000000"
    const char *type;           // device_type 属性值
    phandle phandle;            // 节点的phandle
    const char *full_name;      // 完整路径,如 "/soc/uart@fe000000"
    struct property *properties;// 属性链表
    struct device_node *parent; // 父节点
    struct device_node *child;  // 子节点
    struct device_node *sibling;// 兄弟节点
};

// 表示一个属性
struct property {
    char *name;                 // 属性名,如 "compatible"
    int length;                 // 属性值长度(字节)
    void *value;                // 属性值指针
    struct property *next;      // 下一个属性
};

4.2.2 常用OF API分类

类别 API函数 功能描述 典型返回值
节点查找 of_find_node_by_name() 通过节点名称查找 struct device_node *
of_find_node_by_type() 通过 device_type 查找 struct device_node *
of_find_compatible_node() 通过 compatible 字符串查找 struct device_node *
属性读取 of_property_read_u32() 读取32位整型属性 0 成功 / -EINVAL
of_property_read_string() 读取字符串属性 0 成功 / -EINVAL
of_get_property() 获取原始属性值指针 const void *
资源转换 of_address_to_resource() 将 reg 属性转换为 struct resource 0 成功 / 负值错误码
of_irq_to_resource() 将 interrupts 属性转换为中断资源 中断号 / 负值错误码
遍历辅助 for_each_child_of_node() 遍历当前节点的所有子节点 宏,无返回值
实战要点: 在驱动中使用OF API时,务必检查返回值。例如 of_property_read_u32(np, "clock-frequency", &freq) 返回非0值时,不能使用freq变量,否则会导致使用未初始化的数据。

4.2.3 OF API调用关系图

下面的SVG图展示了一个典型驱动如何通过OF API从设备树节点获取资源:

驱动通过OF API获取设备资源 device_node compatible = "my,dev" reg = <0xfe000000 0x1000> interrupts = <0 31 4> clock-frequency = <50000000> OF API 调用层 of_match_node() of_address_to_resource() of_irq_to_resource() of_property_read_u32() 驱动资源 struct resource mem int irq_num u32 clock_freq void __iomem *base ftrace追踪点: • 在 of_address_to_resource() 入口和出口放置 ftrace 探针,观察 reg 属性解析过程。 • 追踪 of_property_read_u32() 的调用栈,确认 clock-frequency 是否被正确读取。 • 若驱动 probe 失败,通过 ftrace 查看 of_match_node() 是否返回 NULL。 • 使用 trace-cmd record -p function -l 'of_*' 捕获所有OF API调用。

4.3 设备树解析的早期调试方法

当内核在设备树解析阶段出现问题时,常规的printk可能尚未生效。此时可以使用以下方法进行调试:

  • 早期printk(early_printk):在 setup_arch() 之前启用,可以输出 early_init_dt_scan() 的执行状态。
  • dtb反编译对比:使用 dtc -I dtb -O dts boot.dtb 将传递的dtb反编译为dts,与源码dts对比,确认Bootloader是否修改了节点。
  • ftrace function_graph:在支持早期ftrace的平台上,可以追踪 unflatten_device_tree() 的子函数调用序列。
  • CONFIG_OF_UNITTEST:内核内置的设备树单元测试框架,可以在启动时验证设备树解析的正确性。
常见陷阱: 设备树中的 reg 属性地址是 CPU总线地址,而非外设总线地址。在具有地址转换桥(如PCIe RC)的系统中,of_address_to_resource() 会自动进行地址映射,但若设备树缺少 ranges 属性,转换会失败,返回 -EINVAL。

4.4 本章小结