Linux 内核中设备树(Device Tree)的实现机制是一套完整的 “解析 - 匹配 - 初始化” 流程,核心目标是将设备树描述的硬件信息转换为内核可识别的设备结构,并完成驱动与硬件的绑定。以下从技术细节层面解析其实现机制:

一、设备树的加载与解析流程

设备树二进制文件(.dtb)由 Bootloader(如 U-Boot)在启动时传递给内核,内核启动阶段完成解析,主要步骤如下:

  • dtb 文件的传递与验证

    • Bootloader 通过特定寄存器(如 ARM 的r2寄存器)将.dtb 的物理地址传递给内核。
    • 内核首先验证.dtb 的头部校验和(dtb_header中的magic字段和crc32),确保文件完整性。
  • 扁平化设备树(FDT)的解析

    • 内核通过libfdt库(位于scripts/dtc/libfdt/)解析.dtb,将扁平化的二进制数据转换为内存中的树形结构。
    • 关键函数:unflatten_device_tree()将 FDT 节点转换为内核内部的device_node结构体(包含节点名称、属性、子节点指针等)。
  • 构建全局设备树结构

    • 解析完成后,内核形成以of_root为根节点的全局设备树结构,所有硬件节点通过device_nodechildsibling指针关联,体现硬件拓扑关系。

二、设备与驱动的匹配机制

内核通过 “设备树兼容性” 实现硬件与驱动的绑定,核心是compatible属性的匹配:

  • 设备树中的compatible属性

    每个硬件节点通过compatible属性声明自身类型,格式为字符串列表,例如:

    uart@12340000 {
        compatible = "nvidia,tegra210-uart", "ns16550a";
    };
    

    表示该 UART 控制器既兼容 NVIDIA Tegra210 平台的专用驱动,也兼容通用的 ns16550a 驱动。

  • 驱动中的of_match_table匹配表

    驱动通过of_match_table结构体数组声明支持的设备,例如:

    static const struct of_device_id uart_tegra_match[] = {
        { .compatible = "nvidia,tegra210-uart" },
        { /* 终止符 */ }
    };
    MODULE_DEVICE_TABLE(of, uart_tegra_match);
    
    static struct platform_driver uart_tegra_driver = {
        .probe = uart_tegra_probe,
        .driver = {
            .name = "tegra-uart",
            .of_match_table = uart_tegra_match,  // 关联匹配表
        },
    };
    
  • 匹配过程

    • 内核遍历设备树节点,为每个节点创建对应的平台设备(platform_device)。
    • 当驱动注册时(platform_driver_register),内核调用of_match_device()函数,对比驱动的of_match_table与设备节点的compatible属性,找到最精确的匹配(优先匹配列表中靠前的字符串)。
    • 匹配成功后,执行驱动的probe函数,完成硬件初始化。

三、设备树属性的访问接口

内核提供了一系列of_xxx函数(定义在include/linux/of.h),供驱动获取设备树中的硬件信息(如地址、中断、时钟等):

  • 地址与资源获取

    • of_address_to_resource():将设备树中reg属性描述的地址转换为内核资源(struct resource)。
    • 示例:获取 UART 控制器的基地址
      struct resource res;
      of_address_to_resource(dev->of_node, 0, &res);
      void __iomem *base = ioremap(res.start, resource_size(&res));
      
  • 中断信息获取

    • of_irq_get():解析interrupts属性,获取中断号。
      int irq = of_irq_get(dev->of_node, 0);  // 获取第一个中断
      
  • 字符串与数值属性

    • of_property_read_string():读取字符串属性(如model)。
    • of_property_read_u32():读取 32 位数值属性(如clock-frequency)。
  • 子节点与总线遍历

    • of_get_child_count():获取子节点数量(如 I2C 总线上的设备数量)。
    • of_for_each_child_of_node():遍历子节点(如枚举 SPI 总线上的所有外设)。

四、设备树覆盖(Overlay)机制

为支持硬件动态配置(如主板 + 子板组合),内核实现了设备树覆盖机制:

  • Overlay 的原理

    Overlay 是一个独立的.dtbo 文件,描述增量硬件信息,可在系统运行时动态加载到主设备树中,修改或添加节点。

  • 加载流程

    • 用户通过configfssysfs接口触发 Overlay 加载(如echo dtbo > /sys/kernel/config/device-tree/overlays/myoverlay/path)。
    • 内核通过of_overlay_apply()函数将 Overlay 内容合并到主设备树,并触发受影响设备的重新枚举。
  • 应用场景

    适用于可扩展硬件(如开发板扩展坞),无需重新编译主设备树即可支持新外设。

五、核心数据结构与代码路径

  • 关键结构体

    • struct device_node:内存中设备树节点的表示。
    • struct property:节点属性(包含属性名、值、长度)。
    • struct of_device_id:驱动与设备的匹配表。
  • 核心代码路径

    • 解析阶段:start_kernel() → setup_arch() → unflatten_device_tree()
    • 设备创建:of_platform_default_populate_init() → 为每个节点创建platform_device
    • 驱动匹配:platform_driver_register() → driver_match_device() → of_match_device()

总结

Linux 内核的设备树实现机制通过 “解析二进制设备树→构建内存树形结构→基于 compatible 属性匹配驱动→提供标准化接口访问硬件信息” 的流程,实现了硬件描述与内核代码的解耦。这种设计使同一内核镜像可适配多种硬件平台,大幅降低了嵌入式系统的维护成本,是现代 Linux 内核支持多样化硬件的核心技术之一。

关键目录文件

scripts/dtc/libfdt/和drivers/of下都有设备树的文件,比如fdt.c

在 Linux 内核中,scripts/dtc/libfdt/drivers/of/目录下确实都存在与设备树相关的代码文件(如fdt.c),但它们的定位、功能和使用场景有本质区别,分别对应设备树处理的不同阶段和层面。

一、scripts/dtc/libfdt/:设备树编译与解析的基础库

libfdt独立于 Linux 内核的通用设备树操作库,最初源于dtc(Device Tree Compiler)工具,主要用于对设备树二进制文件(.dtb)进行底层操作(解析、修改、生成等)。

核心功能:

  • 处理扁平化设备树(FDT)

    设备树源文件(.dts)编译后生成的.dtb是一种 “扁平化” 二进制格式(FDT),libfdt提供了操作这种格式的基础接口,例如:

    • 解析.dtb头部信息(fdt_check_header)。
    • 查找节点(fdt_find_node_by_path)、读取属性(fdt_getprop)。
    • 修改节点属性(fdt_setprop)、添加子节点(fdt_add_subnode)。
  • 跨环境复用

    libfdt的代码设计独立于 Linux 内核,不仅被内核使用,还被 Bootloader(如 U-Boot)、用户态工具(如dtc)等引用,实现不同环境下对.dtb的统一操作。

  • 代码特点

    • 纯 C 语言实现,无内核依赖(不使用内核数据结构如struct devicestruct resource)。
    • 函数命名以fdt_为前缀(如fdt_open_intofdt_node_offset_by_compatible)。

二、drivers/of/:内核设备树框架的核心实现

drivers/of/(Open Firmware 适配层)是 Linux 内核设备树框架的核心代码,基于libfdt提供的底层接口,实现设备树与内核驱动模型的对接。

核心功能:

  • 构建内核设备树抽象

    libfdt解析的 FDT 转换为内核可识别的树形结构,核心数据结构为struct device_node(节点)和struct property(属性),例如:

    • of_fdt.c:负责从.dtb解析并构建device_node树(调用libfdt接口)。
    • of_node.c:提供device_node的创建、销毁、遍历等操作。
  • 设备与驱动的匹配机制

    实现基于compatible属性的驱动匹配逻辑,例如:

    • of_match.c:提供of_match_device等函数,用于驱动与设备节点的匹配。
    • of_platform.c:将设备树节点转换为platform_device,接入内核平台驱动模型。
  • 硬件资源的抽象与访问

    封装硬件资源(地址、中断、时钟等)的获取接口,供驱动使用,例如:

    • of_address.c:将设备树reg属性转换为内核resource结构(of_address_to_resource)。
    • of_irq.c:解析interrupts属性,获取中断号(of_irq_get)。
  • 代码特点

    • 深度依赖内核数据结构和框架(如struct devicestruct platform_driver)。
    • 函数命名以of_为前缀(如of_property_read_u32of_get_child_count)。

三、两者的关系与调用流程

libfdt是底层工具库,drivers/of/是内核设备树框架,前者为后者提供基础能力,后者基于前者实现内核级功能,典型调用流程如下:

  • 内核启动阶段

    • 内核通过libfdtfdt_check_header验证.dtb合法性。
    • 调用unflatten_device_tree(位于drivers/of/fdt.c),内部通过libfdtfdt_next_node等函数遍历 FDT 节点,创建device_node树。
  • 驱动访问设备树时

    • 驱动调用of_property_read_stringdrivers/of/property.c)读取属性。
    • 该函数内部调用libfdtfdt_getprop获取原始二进制数据,再转换为内核字符串格式。
  • 动态修改设备树时

    • 设备树覆盖(Overlay)机制通过libfdtfdt_overlay_apply合并.dtbo到主设备树。
    • 内核框架(drivers/of/overlay.c)同步更新device_node结构,并触发设备重新枚举。

四、总结:关键区别

维度 scripts/dtc/libfdt/ drivers/of/
定位 通用 FDT 格式操作库(独立于内核) 内核设备树框架(依赖内核)
处理对象 扁平化二进制设备树(.dtb 内核抽象的device_node树形结构
功能 解析、修改 FDT 的底层接口 设备树与内核驱动模型的对接、资源管理
调用者 内核、Bootloader、用户态工具(如dtc 内核驱动、内核子系统
函数前缀 fdt_xxx(如fdt_getprop of_xxx(如of_property_read_u32

简单说,libfdt是 “操作设备树二进制的工具”,drivers/of/是 “将设备树融入 Linux 驱动体系的桥梁”。前者解决 “如何读写.dtb”,后者解决 “内核如何用设备树驱动硬件”。

struct device_node

在 Linux 内核设备树框架中,struct device_node是描述设备树节点的核心数据结构,用于将设备树(.dts)中的节点信息在内存中进行抽象表示。它是内核解析设备树二进制文件(.dtb)后生成的树形结构节点,贯穿了设备树从解析到驱动匹配的整个流程。

struct device_node的定义与核心成员

该结构定义在 include/linux/of.h 中,核心成员如下(简化版):

struct device_node {
    const char *name;           // 节点名称(如"uart@12340000"中的"uart")
    const char *type;           // 节点类型(对应dts中的"device_type"属性)
    phandle phandle;            // 节点的phandle值(用于跨节点引用)
    const char *full_name;      // 节点的完整路径名(如"/soc/uart@12340000")
    
    struct fwnode_handle fwnode; // 固件节点句柄(用于内核设备模型集成)
    
    struct device_node *parent; // 父节点指针(构成树形结构)
    struct device_node *child;  // 第一个子节点指针
    struct device_node *sibling;// 下一个兄弟节点指针(同层级节点链表)
    
    struct property *properties;// 节点属性链表(如"compatible"、"reg"等)
    struct property *deadprops; // 已删除的属性(用于overlay动态修改)
    
    void *data;                 // 私有数据(供驱动或内核子系统使用)
};

核心成员解析

  • 节点标识相关

    • name:节点的基础名称,通常包含设备类型(如 "uart"、"i2c")和地址(如 "@12340000"),但地址部分会被单独解析,name字段仅保留前缀(如 "uart")。
    • full_name:节点在设备树中的完整路径(如 "/soc/usb@12340000"),是节点的全局唯一标识。
    • phandle:由设备树编译器(dtc)生成的数值标识,用于其他节点通过phandle属性引用该节点(如中断控制器的引用)。
  • 树形结构相关

    • parent/child/sibling:通过指针形成树形结构,反映设备树中节点的层级关系(如 "I2C 控制器" 是 "I2C 设备" 的父节点)。
    • 遍历节点的典型方式:通过child访问子节点,通过sibling遍历同层级所有节点。
  • 属性相关

    • properties:指向节点的属性链表(每个属性为struct property结构),存储节点的所有属性(如 "compatible"、"reg"、"interrupts" 等)。
    • 例如,节点的compatible属性可通过遍历properties链表找到。
  • 内核集成相关

    • fwnode:固件节点句柄,用于将设备树节点与内核的device结构关联(内核设备模型中的抽象)。
    • data:私有数据指针,可由驱动或内核子系统(如 PCI、I2C)用于存储临时信息(如已解析的资源)。

struct device_node的生命周期

  • 创建(内核启动阶段)

    内核启动时,unflatten_device_tree()函数(drivers/of/fdt.c)解析.dtb 文件,通过libfdt库读取节点信息,为每个节点动态分配struct device_node并初始化成员(如名称、父 / 子节点指针、属性等),最终形成以of_root为根的全局设备树。

  • 使用(驱动匹配与硬件初始化)

    • 驱动通过of_xxx系列函数(如of_find_node_by_compatible)查找目标device_node
    • 基于device_node的属性(如 "reg"、"interrupts")解析硬件资源(地址、中断等)。
    • 设备树与驱动匹配时,内核通过节点的compatible属性与驱动的of_match_table比对。
  • 销毁(仅在特殊场景)

    正常情况下,device_node在系统生命周期内一直存在。仅当动态修改设备树(如 overlay 卸载)时,内核通过of_node_release()等函数释放相关节点。

常用操作device_node的函数

内核提供了丰富的 API 用于操作struct device_node,主要定义在 include/linux/of.h 中:

  • 查找节点

    • of_find_node_by_path(const char *path):通过路径查找节点(如of_find_node_by_path("/soc/uart@12340000"))。
    • of_find_compatible_node(struct device_node *from, const char *type, const char *compatible):通过compatible属性查找节点(最常用)。
    • of_get_parent(const struct device_node *node):获取父节点。
    • of_get_next_child(const struct device_node *node, struct device_node *prev):遍历子节点。
  • 获取属性

    • of_find_property(const struct device_node *np, const char *name, int *lenp):查找节点的指定属性。
    • of_property_read_string(const struct device_node *np, const char *propname, const char **out_string):读取字符串属性(如 "model")。
    • of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value):读取 32 位数值属性(如 "clock-frequency")。
  • 引用计数管理

    • of_node_get(struct device_node *node):增加节点的引用计数(防止被意外释放)。
    • of_node_put(struct device_node *node):减少引用计数,当计数为 0 时可能被释放。

###** 总结 **struct device_node是设备树在 Linux 内核中的 “内存镜像”,它将.dtb 文件中的静态硬件描述转换为内核可操作的动态数据结构,是连接设备树与内核驱动模型的关键纽带。通过该结构,内核能够解析硬件拓扑、匹配驱动程序、分配硬件资源,最终实现 “硬件描述与软件解耦” 的设计目标。

一个设备树文件中,每个节点都有一个struct device_node吗?还是整个设备树文件就对应一个struct device_node?

在 Linux 内核解析设备树的过程中,设备树文件中的每个节点(包括根节点、子节点、叶子节点)都会对应一个独立的struct device_node结构体,而非整个设备树文件对应一个。这些struct device_node通过指针(parent/child/sibling)连接,形成与设备树源文件(.dts)结构完全一致的树形结构。

具体说明:

  • 节点与device_node的一一对应关系

    设备树文件(.dts)的结构是树形层级关系,例如:

    / {                  // 根节点
        soc {            // 子节点(系统级芯片)
            uart@12340000 {  // 叶子节点(UART控制器)
                compatible = "ns16550a";
                reg = <0x12340000 0x100>;
            };
            i2c@56780000 {   // 叶子节点(I2C控制器)
                compatible = "ti,omap4-i2c";
                reg = <0x56780000 0x100>;
            };
        };
    };
    

    内核解析后,会生成 4 个struct device_node

    • 根节点(/)对应一个device_node
    • soc子节点对应一个device_node,其父指针指向根节点。
    • uart@12340000节点对应一个device_node,其父指针指向soc节点。
    • i2c@56780000节点对应一个device_node,其父指针指向soc节点,且sibling指针指向uart节点(同层级)。
  • 树形结构的构建

    所有device_node通过以下指针形成完整树:

    这种结构与设备树源文件的层级完全一致,内核通过遍历这些指针即可访问整个设备树的所有节点。

    • parent:指向父节点(根节点的parentNULL)。
    • child:指向第一个子节点(如根节点的child指向soc节点)。
    • sibling:指向同层级的下一个节点(如uart节点的sibling指向i2c节点)。
  • 根节点的特殊性

    整个设备树的入口是根节点对应的device_node,内核通过全局变量of_root(定义在drivers/of/base.c)指向它。所有其他节点都可通过of_root开始遍历找到。

总结

设备树文件中的每个节点(无论层级)都会被解析为一个独立的struct device_node,这些结构体通过指针链接成与原始设备树结构一致的树形结构。这种设计使内核能够像访问真实设备树一样,通过节点间的层级关系高效查找、遍历和操作硬件信息。

of_root

在 Linux 内核的设备树框架中,of_root是一个全局变量,用于指向设备树在内存中构建的树形结构的根节点(root node),即对应设备树源文件(.dts)中最顶层的/节点。它是整个设备树结构的入口点,内核通过of_root可以遍历所有设备树节点。

of_root的定义与作用

  • 定义位置drivers/of/base.c

    struct device_node *of_root;
    EXPORT_SYMBOL(of_root);
    

    它是一个struct device_node类型的指针,被EXPORT_SYMBOL导出,允许内核其他模块(如驱动)访问。

  • 核心作用
    作为设备树内存结构的 “根指针”,所有其他节点(子节点、孙节点等)都可通过of_rootchildsibling等指针遍历访问。例如:

    • 根节点的child指针指向设备树中第一层子节点(如/soc/memory等)。
    • 通过of_root可以查找系统中所有硬件节点(如外设、总线、内存等)。

of_root的初始化过程

of_root内核启动早期被初始化,具体流程如下:

  1. Bootloader(如 U-Boot)将设备树二进制文件(.dtb)的物理地址传递给内核。
  2. 内核解析.dtb文件(通过unflatten_device_tree函数),构建内存中的struct device_node树形结构。
  3. 解析完成后,unflatten_device_tree将根节点的device_node指针赋值给of_root,完成初始化。

此时,of_root即指向设备树最顶层的/节点,后续内核和驱动可通过它访问整个设备树。

基于of_root的常用操作

内核提供的许多设备树遍历函数默认以of_root为起点,例如:

  1. 遍历所有节点

    // 从根节点开始,遍历所有子节点
    struct device_node *node;
    for_each_node_by_path_from(of_root, node) {
        printk("Found node: %s\n", node->full_name);
    }
    
  2. 查找特定节点

    // 从根节点开始,查找兼容属性为"arm,cortex-a53"的节点
    struct device_node *cpu_node = of_find_compatible_node(of_root, NULL, "arm,cortex-a53");
    
  3. 获取根节点的属性
    根节点通常包含系统级属性(如modelcompatible),可通过of_root直接访问:

    const char *model;
    of_property_read_string(of_root, "model", &model);  // 读取根节点的"model"属性
    

总结

of_root是设备树在 Linux 内核中的 “总入口”,它指向设备树的根节点(对应.dts中的/),是内核和驱动访问所有硬件节点的起点。通过of_root,内核可以遍历整个设备树结构、查找特定节点、解析硬件属性,最终实现设备树与驱动的匹配和硬件初始化。

phandle

device_node中的phandle

在 Linux 内核的 struct device_node 中,phandle 是一个用于跨节点引用的数值标识符,它相当于设备树节点的 “内部身份证号”,让不同节点之间可以通过这个数值快速建立关联,而无需依赖复杂的路径名称。

phandle 的核心作用

设备树中,节点之间常需要相互引用(例如,外设节点引用中断控制器节点、子节点引用父节点的电源域)。phandle 就是为简化这种引用而设计的,它的作用类似编程语言中的 “指针”,通过一个唯一数值指向目标节点。

例如,在设备树中,UART 节点需要引用中断控制器节点,可通过 phandle 实现:

/ {
    intc: interrupt-controller@10000000 {  // 中断控制器节点,定义 phandle
        phandle = <0x1>;  // 手动指定 phandle 值(通常由编译器自动生成)
        compatible = "vendor,intc";
    };

    uart@20000000 {  // UART 节点,引用中断控制器
        interrupt-parent = <&intc>;  // 通过 &intc 引用,实际会解析为 phandle 0x1
        interrupts = <0 1>;
    };
};
  • 中断控制器节点的 phandle = <0x1> 表示它的标识符是 0x1
  • UART 节点的 interrupt-parent = <&intc> 本质是引用 phandle = 0x1 的节点。

phandle 的生成与解析

  1. 自动生成
    设备树源文件(.dts)中通常不需要手动指定 phandle,编译器(dtc)会在编译 .dtb 时自动为每个节点分配唯一的 phandle 值(从 1 开始递增),确保整个设备树中无重复。

  2. 标签引用
    在 .dts 中,通过标签(如 intc:)为节点命名,引用时使用 &标签名(如 &intc),编译器会自动将其转换为目标节点的 phandle 值。这避免了手动管理数值的繁琐,也让设备树更易读。

  3. 内核解析
    内核解析 .dtb 时,会将每个节点的 phandle 数值存入 struct device_node 的 phandle 成员,供驱动通过 API 查找引用关系(如 of_find_node_by_phandle())。

struct device_node 中 phandle 的成员定义

struct device_node {
    phandle phandle;  // 节点的 phandle 值(32 位无符号整数)
    // 其他成员...
};
  • 类型 phandle 本质是 u32(32 位无符号整数),确保能覆盖足够多的节点。
  • 若节点未被其他节点引用,phandle 可能为 0(但编译器仍会为其分配唯一值,除非节点被标记为 status = "disabled")。

常用 phandle 操作函数

内核提供了基于 phandle 查找节点的 API,方便驱动处理跨节点引用:

  1. of_find_node_by_phandle(phandle_t ph)
    根据 phandle 值查找对应的 struct device_node
    例:

    struct device_node *intc_node = of_find_node_by_phandle(0x1);
    if (intc_node) {
        // 找到 phandle 为 0x1 的节点(中断控制器)
    }
    
  2. of_parse_phandle(const struct device_node *np, const char *propname, int index)
    解析节点属性中存储的 phandle,返回目标节点。
    例:解析 UART 节点的 interrupt-parent 属性:

    struct device_node *uart_node = ...;  // UART 节点
    struct device_node *intc_node = of_parse_phandle(uart_node, "interrupt-parent", 0);
    

总结

struct device_node 中的 phandle 是设备树节点的唯一数值标识,用于高效实现节点间的跨层级引用。它由编译器自动生成,内核解析后存入 device_node,通过专用 API 可快速查找目标节点,是设备树中描述硬件依赖关系(如中断、电源、时钟)的核心机制。

简单说,phandle 就像节点的 “身份证号”,其他节点通过这个号码就能快速找到它,而不用记住复杂的路径(如 /soc/interrupt-controller@10000000)。

设备树中,不用标签名,直接用节点名,能引用其他节点吗?

在设备树中,可以不使用标签名,直接通过节点的完整路径名引用其他节点,但这种方式不如标签引用直观和灵活,且容易因路径变更导致引用失效。

一、直接使用节点名(路径)引用的语法

设备树中,每个节点都有唯一的完整路径名(从根节点开始的层级路径),例如:

/ {
    soc {
        intc: interrupt-controller@10000000 {  // 带标签的节点
            compatible = "vendor,intc";
        };
        
        uart@20000000 {
            // 方式1:通过标签引用(推荐)
            interrupt-parent = <&intc>;
            
            // 方式2:通过完整路径引用(不依赖标签)
            // interrupt-parent = <&/soc/interrupt-controller@10000000>;
        };
    };
};
  • 标签引用:&intc 是简洁的引用方式,本质是标签 intc 对应的节点路径。
  • 路径引用:&/soc/interrupt-controller@10000000 直接通过完整路径引用,无需标签。

二、路径引用的特点与适用场景

  • 语法要求

    路径必须从根节点(/)开始,逐层写出节点名称,例如:

    • 根节点下的 soc 节点:/soc
    • soc 下的 interrupt-controller@10000000 节点:/soc/interrupt-controller@10000000
      引用时需在路径前加 &,表示 “指向该路径的节点”。
  • 缺点:维护成本高

    若节点的层级或名称发生变化(如 soc 改名为 system),所有基于路径的引用都需要手动修改,否则会导致引用失效。而标签引用只需修改标签对应的节点路径,所有使用该标签的地方会自动同步。

  • 适用场景

    • 临时调试:快速引用某个节点,无需定义标签。
    • 节点路径固定不变的简单设备树:如小型嵌入式系统,硬件结构稳定,节点路径很少变更。

三、为什么标签引用更常用?

标签引用(如 &intc)本质是路径的 “别名”,由设备树编译器(dtc)自动转换为目标节点的 phandle,具有以下优势:

  1. 简洁性:避免重复书写冗长的路径(尤其深层级节点)。
  2. 可维护性:节点路径变更时,只需更新标签与节点的绑定关系(如 intc: new/path/to/intc),所有引用 &intc 的地方无需修改。
  3. 可读性:标签名(如 intcuart)通常反映节点功能,比路径更易理解。

总结

设备树中可以直接通过节点完整路径引用其他节点,但这种方式不如标签引用灵活,维护成本较高。实际开发中,几乎都采用标签引用(&标签名),仅在临时调试或简单场景下使用路径引用。

本质上,无论是标签引用还是路径引用,最终都会被编译器转换为目标节点的 phandle(数值标识),内核通过 phandle 实现节点间的关联。

只有标签会转化成phandle吗?

在设备树中,并非只有带标签的节点才会被分配phandle。实际上,所有节点(无论是否有标签)在编译为.dtb时都会被自动分配唯一的phandle,标签的作用只是为节点提供一个 “别名”,方便在引用时简化写法,而不是决定phandle的生成。

一、phandle的生成规则

设备树编译器(dtc)在将.dts编译为.dtb时,会为每一个节点(包括根节点、子节点、叶子节点,无论是否有标签)自动分配一个唯一的phandle值(通常是从1开始的递增整数)。

例如,一个不含任何标签的设备树:

/ {
    soc {
        interrupt-controller@10000000 {
            compatible = "vendor,intc";
        };
        uart@20000000 {
            interrupt-parent = <&/soc/interrupt-controller@10000000>;
        };
    };
};

编译后:

  • 根节点(/)会被分配phandle(如0x1)。
  • soc节点会被分配phandle(如0x2)。
  • interrupt-controller@10000000节点会被分配phandle(如0x3)。
  • uart@20000000节点会被分配phandle(如0x4)。

即使没有标签,uart节点通过路径&/soc/interrupt-controller@10000000引用中断控制器时,编译器会自动解析该路径对应的节点,并将其phandle0x3)写入interrupt-parent属性中。

二、标签与phandle的关系

标签(如intc:)的作用是为节点创建一个便于引用的别名,而非触发phandle的生成。具体来说:

  1. 标签本身不会直接生成phandle,但标签绑定的节点会被编译器分配phandle(与其他节点一致)。
  2. 当使用&标签名(如&intc)引用节点时,编译器会将标签对应的节点phandle填入引用处,本质与通过路径引用最终解析为phandle的过程完全一致。

例如,带标签的节点:

/ {
    soc {
        intc: interrupt-controller@10000000 {  // 标签intc绑定到该节点
            compatible = "vendor,intc";
        };
        uart@20000000 {
            interrupt-parent = <&intc>;  // 引用标签intc
        };
    };
};

编译后,interrupt-parent属性中存储的依然是interrupt-controller@10000000节点的phandle(如0x3),与无标签时通过路径引用的结果完全相同。

三、总结

  • 所有节点都会被分配phandlephandle是设备树节点的固有属性,由编译器自动生成,与是否有标签无关。
  • 标签仅用于简化引用:标签为节点提供了一个短名称,使引用写法更简洁(&intc vs &/soc/interrupt-controller@10000000),但最终都会解析为节点的phandle

简单说,phandle是节点的 “身份证号”(每个节点都有),标签是节点的 “绰号”(可选,仅为了称呼方便)。

为什么设备树中一个节点有时候会需要引用其他节点?

在设备树中,节点之间的引用是描述硬件依赖关系拓扑结构的核心方式。嵌入式系统的硬件组件并非孤立存在,而是通过总线、中断线、电源域等方式相互关联,节点引用正是为了在设备树中准确表达这些物理连接或逻辑依赖。

一、最常见的引用场景与原因

1. 中断依赖:外设引用中断控制器

几乎所有外设(如 UART、SPI、GPIO)都需要通过中断线连接到中断控制器,设备树中必须明确这种关联,才能让内核正确配置中断路由。

例:UART 节点引用中断控制器节点

/ {
    intc: interrupt-controller@10000000 {  // 中断控制器节点
        compatible = "vendor,intc";
        #interrupt-cells = <2>;  // 表示每个中断需要2个参数(中断号、触发方式)
    };

    uart@20000000 {  // UART外设节点
        compatible = "vendor,uart";
        interrupt-parent = <&intc>;  // 引用中断控制器(核心!)
        interrupts = <5 0x01>;  // 中断号5,上升沿触发(参数格式由中断控制器的#interrupt-cells定义)
    };
};
  • 若不引用intc节点,内核无法知道 UART 的中断信号连接到哪个控制器,导致中断无法响应。

2. 总线从属:子设备引用父总线控制器

外设通常挂载在某种总线(I2C、SPI、USB 等)上,必须通过引用总线控制器节点,明确 “该设备属于哪个总线”,内核才能通过总线驱动枚举子设备。

例:I2C 传感器引用 I2C 控制器

/ {
    i2c1: i2c@30000000 {  // I2C控制器节点
        compatible = "vendor,i2c";
        #address-cells = <1>;  // 子设备地址占1个单元格
        #size-cells = <0>;
    };

    // 传感器挂载在I2C1总线上,必须引用i2c1节点
    &i2c1 {
        temp-sensor@48 {  // 子设备(地址0x48)
            compatible = "ti,tmp102";
            reg = <0x48>;  // I2C设备地址(格式由父总线的#address-cells定义)
        };
    };
  • 若不引用i2c1,内核无法将传感器与具体 I2C 控制器关联,导致传感器无法被枚举和驱动。

3. 电源与时钟:外设引用电源域 / 时钟控制器

嵌入式设备的电源和时钟通常由专门的控制器管理,外设需要引用这些控制器来声明 “使用哪个电源 / 时钟”,内核才能动态管理硬件的功耗和频率。

例:外设引用电源域和时钟控制器

/ {
    power: power-domain@40000000 {  // 电源域控制器
        compatible = "vendor,power";
    };

    clk: clock-controller@50000000 {  // 时钟控制器
        compatible = "vendor,clk";
        #clock-cells = <1>;
    };

    spi@60000000 {  // SPI外设
        compatible = "vendor,spi";
        power-domains = <&power>;  // 引用电源域
        clocks = <&clk 3>;  // 引用时钟控制器的第3路时钟
    };
};
  • 若不引用,内核无法为 SPI 外设分配电源和时钟,导致硬件无法启动。

4. 硬件复用:引用复用控制器

SoC 的引脚(GPIO、外设接口)通常可复用为多种功能(如同一引脚可作 GPIO 或 I2C_SDA),外设需引用复用控制器节点,声明 “使用哪个引脚的哪种功能”。

例:UART 引用引脚复用控制器

/ {
    pinctrl: pinctrl@70000000 {  // 引脚复用控制器
        compatible = "vendor,pinctrl";
        uart_pins: uart-pins {  // UART功能的引脚配置
            pins = "pin12", "pin13";
            function = "uart";
        };
    };

    uart@20000000 {
        compatible = "vendor,uart";
        pinctrl-0 = <&uart_pins>;  // 引用UART的引脚配置
        pinctrl-names = "default";
    };
};
  • 若不引用,内核无法为 UART 配置正确的引脚功能,导致外设无法通信。

二、引用的本质:构建硬件拓扑关系

设备树的核心作用是描述硬件 “是什么” 以及 “如何连接”。节点引用本质是在树形结构中表达硬件的 “父子关系”“从属关系” 或 “依赖关系”,让内核:

  1. 明确硬件的物理连接(如 “中断线连到哪个控制器”);
  2. 理解参数的格式(如 “中断号需要几个参数” 由中断控制器的#interrupt-cells定义);
  3. 按依赖顺序初始化硬件(如 “先初始化电源控制器,再初始化依赖它的外设”)。

总结

节点引用是设备树表达硬件关联的 “语言”,没有引用,设备树只能孤立地描述单个硬件,无法体现系统级的连接关系。内核依赖这些引用才能正确解析硬件拓扑、配置资源(中断 / 时钟 / 电源)、加载驱动,最终让整个嵌入式系统正常工作。

简单说,设备树中的节点引用,就像在地图上标注 “道路 A 连接到桥梁 B”“桥梁 B 通向城市 C”—— 没有这些标注,地图只是孤立的地标,无法指引路线。

struct property

在 Linux 内核设备树框架中,struct property是用于描述设备树节点属性的核心数据结构。设备树中每个节点的属性(如compatiblereginterrupts等)都会被解析为一个struct property实例,用于存储属性的名称、值和长度等信息。

struct property的定义与核心成员

该结构定义在 include/linux/of.h 中,核心成员如下(简化版):

struct property {
    char *name;               // 属性名称(如"compatible"、"reg")
    int length;               // 属性值的长度(单位:字节)
    void *value;              // 属性值的指针(二进制数据)
    struct property *next;    // 下一个属性的指针(形成链表)
    unsigned long _flags;     // 属性标志(如动态分配、已修改等)
    unsigned int unique_id;   // 唯一ID(用于设备树覆盖机制)
    struct bin_attribute attr; // 用于sysfs暴露属性的二进制属性
};

核心成员解析

  • name:属性名称

    字符串形式,对应设备树中属性的名称(如"compatible""reg""clock-frequency")。例如,设备树中compatible = "nvidia,tegra210-uart";name成员值为"compatible"

  • length:属性值长度

    表示value指向的属性值数据的字节数。例如:

    • 字符串属性(如model = "NVIDIA Jetson Nano";)的length为字符串长度 + 1(包含终止符\0)。
    • 数值数组属性(如reg = <0x12340000 0x100>;)的length为数组元素个数 × 每个元素的字节数(若为 32 位数值,每个元素占 4 字节,2 个元素则length=8)。
  • value:属性值数据

    指向属性值的二进制数据(未经过解析的原始数据)。例如:

    • compatible属性的value指向字符串列表的二进制数据(如"nvidia,tegra210-uart\0ns16550a\0")。
    • reg属性的value指向设备地址和长度的二进制表示(如 32 位地址0x12340000和长度0x100的二进制数据)。
  • next:链表指针

    指向当前节点的下一个属性,使同一节点的所有属性形成单链表。例如,一个 UART 节点的属性链表可能是:compatible → reg → interrupts → clock-frequency → NULL

  • 其他成员

    • _flags:用于标记属性的状态(如OF_PROP_DYNAMIC表示动态分配的属性,OF_PROP_DEAD表示已删除的属性)。
    • attr:用于将属性暴露到sysfs文件系统(如/sys/firmware/devicetree/base/soc/uart@12340000/compatible),方便用户态程序访问。

struct propertystruct device_node的关系

每个struct device_node(设备树节点)通过properties成员指向该节点的第一个属性,再通过属性的next指针遍历所有属性,形成如下关联:

device_node {
    properties → property {
        name = "compatible",
        value = "nvidia,tegra210-uart\0ns16550a\0",
        length = 28,
        next → property {
            name = "reg",
            value = [00 00 34 12 00 00 00 10],  // 二进制地址数据
            length = 8,
            next → ...  // 其他属性
        }
    }
};

常用操作struct property的函数

内核提供了一系列 API 用于操作属性,主要定义在 include/linux/of.h 中:

  • 查找属性

    • of_find_property(const struct device_node *np, const char *name, int *lenp):在指定节点中查找指定名称的属性,返回struct property指针(lenp用于输出属性长度)。
  • 读取属性值

    • 字符串属性:of_property_read_string(const struct device_node *np, const char *propname, const char **out_string)
    • 32 位数值:of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value)
    • 数组属性:of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)
  • 动态操作属性

    • of_property_add(struct device_node *np, struct property *prop):向节点添加属性。
    • of_property_del(struct device_node *np, const char *name):从节点删除指定属性(用于设备树覆盖机制)。

总结

struct property是设备树属性在 Linux 内核中的抽象表示,每个属性对应一个struct property实例,存储属性的名称、原始值和长度。这些属性通过链表组织在struct device_node中,与节点形成 “节点 - 属性” 的层级关系,是内核解析设备树硬件信息(如兼容性、地址、中断)的基础数据结构。

有的节点没有属性

在设备树中,确实存在没有显式定义属性的节点(称为 “空节点”),这类节点在 Linux 内核中依然会被解析为struct device_node结构体,只是其properties成员为空(指向NULL)。空节点的存在并非无意义,而是有特定的设计用途。

一、空节点的常见场景与作用

  1. 表示抽象的硬件容器或分组
    空节点可用于对硬件进行逻辑分组,体现硬件拓扑中的层级关系,即使本身没有属性。例如:

    / {
        soc {          // 空节点(无显式属性)
            uart@12340000 { ... };  // soc的子节点
            i2c@56780000 { ... };   // soc的子节点
        };
    };
    

    这里的soc节点没有定义属性,但它作为 “系统级芯片” 的抽象容器,将 UART、I2C 等外设节点组织在一起,清晰反映硬件的物理层级(外设集成在 SoC 内部)。

  2. 作为其他节点的引用目标
    空节点可通过phandle被其他节点引用,作为 “占位符” 或 “标记点”。例如:

    / {
        power-domains {
            pd_core: power-domain-core { };  // 空节点,仅作为引用目标
        };
    
        uart@12340000 {
            power-domains = <&pd_core>;  // 引用空节点
        };
    };
    

    pd_core是一个空节点,但通过phandle被 UART 节点引用,用于表示 UART 归属于该电源域。

  3. 兼容内核默认行为的兼容性节点
    某些内核驱动会默认匹配特定名称的节点(即使节点无属性),此时空节点可触发驱动的默认初始化逻辑。

二、空节点在 Linux 内核中的处理

  1. 解析为struct device_node
    内核解析设备树时,无论节点是否有属性,都会为其创建struct device_node结构体。空节点的properties成员为NULL,但nameparentchild等树形结构相关成员仍会被正确初始化(如名称、父 / 子节点指针)。

  2. 驱动匹配的特殊性
    空节点没有compatible属性,因此无法通过compatible属性与驱动的of_match_table匹配。但部分驱动可能通过节点名称(name)或父节点关系进行匹配(这种情况较少,通常用于兼容性处理)。

  3. 可动态添加属性
    即使节点初始为空,也可通过设备树覆盖(Overlay)机制动态添加属性(如运行时通过.dtbo文件为节点添加status = "okay"),内核会相应更新struct device_nodeproperties链表。

三、总结

设备树中的空节点(无属性节点)是合法且有用的,它们的核心作用是构建硬件拓扑的层级关系作为引用目标,而非直接描述硬件参数。内核会为其创建完整的struct device_node结构体,只是properties链表为空。这种设计体现了设备树 “树形结构优先” 的特点 —— 先通过节点组织硬件关系,再通过属性描述具体参数。

设备树在文件系统中的体现

在 Linux 系统中,设备树的信息会通过虚拟文件系统(sysfs)暴露给用户空间,形成一个可直接访问的树形目录结构。这使得用户和应用程序能够直观地查看设备树中的硬件描述信息,而无需解析原始的.dtb二进制文件。

一、设备树在文件系统中的路径

设备树信息主要暴露在以下路径:

/sys/firmware/devicetree/base/

这个路径对应设备树的根节点(/),其下的子目录结构与设备树源文件(.dts)的节点层级完全一致。

二、目录结构与设备树节点的对应关系

/sys/firmware/devicetree/base/下的每个目录对应设备树中的一个节点,文件名对应节点名称;目录中的文件对应节点的属性。例如,一个简单的设备树:

/ {
    model = "Example Board";
    compatible = "vendor,example-board";
    
    soc {
        uart@12340000 {
            compatible = "ns16550a";
            reg = <0x12340000 0x100>;
            status = "okay";
        };
    };
};

在文件系统中会体现为:

/sys/firmware/devicetree/base/
├── model               # 根节点的"model"属性
├── compatible          # 根节点的"compatible"属性
├── soc/                # "soc"子节点(目录)
│   └── uart@12340000/  # "uart@12340000"子节点(目录)
│       ├── compatible  # UART节点的"compatible"属性
│       ├── reg         # UART节点的"reg"属性(二进制数据)
│       └── status      # UART节点的"status"属性

三、属性文件的内容与访问方式

  1. 文本属性(如modelcompatiblestatus
    这些属性的值是字符串或字符串列表,文件内容直接以文本形式存储,可通过cat命令查看:

    $ cat /sys/firmware/devicetree/base/model
    Example Board
    
    $ cat /sys/firmware/devicetree/base/soc/uart@12340000/compatible
    ns16550a
    
  2. 二进制属性(如reginterruptsclock-frequency
    这些属性的值是数值或字节数组(如地址、中断号),以二进制形式存储,需通过工具解析:

    # 查看reg属性的十六进制数据(0x12340000和0x100)
    $ xxd /sys/firmware/devicetree/base/soc/uart@12340000/reg
    00000000: 0000 3412 0000 0010                    ..4.....
    
  3. 特殊属性

    • name:节点的基础名称(如uart),对应struct device_nodename成员。
    • phandle:节点的数值标识(十六进制),用于跨节点引用。

四、设备树覆盖(Overlay)的体现

如果系统加载了设备树覆盖(.dtbo),动态添加或修改的节点 / 属性会实时反映在/sys/firmware/devicetree/base/中。例如,通过 Overlay 为 UART 节点添加baudrate属性后,会自动生成:

/sys/firmware/devicetree/base/soc/uart@12340000/baudrate

五、作用与应用场景

  1. 调试与诊断
    开发人员可通过文件系统快速查看设备树的实际配置,验证节点是否被正确解析(如status是否为"okay")、属性值是否符合预期(如reg地址是否正确)。

  2. 应用程序获取硬件信息
    用户态程序可通过读取这些文件获取硬件细节(如板载型号、外设地址),实现与硬件相关的逻辑(如动态适配不同开发板)。

  3. 验证设备树加载
    检查/sys/firmware/devicetree/base/是否存在,可确认设备树是否被内核正确加载(若不存在,可能是 Bootloader 未传递.dtb或内核未启用设备树支持)。

总结

设备树在文件系统中通过/sys/firmware/devicetree/base/的树形目录结构呈现,实现了硬件描述信息从内核态到用户态的暴露。这种设计既保留了设备树的原始层级关系,又提供了便捷的访问方式,是调试嵌入式系统和实现硬件适配的重要工具。

 为什么有些地方,proc下也有device-tree目录?

这通常是为了兼容旧版本用户态程序而保留的符号链接,并非实际的设备树信息存储位置。

具体来说:

  • 实际的设备树信息始终存储在 sysfs 中(/sys/firmware/devicetree/base/),这是 Linux 内核规范的设备树暴露路径,遵循 sysfs 管理硬件资源的设计原则。
  • /proc/device-tree 通常是一个符号链接,指向 ../sys/firmware/devicetree/base,目的是兼容早期依赖 /proc 路径访问设备树的程序(如一些旧版嵌入式工具或脚本)。

例如,在支持该链接的系统中,执行以下命令会看到:

ls -l /proc/device-tree
# 输出可能为:lrwxrwxrwx 1 root root 30 ... /proc/device-tree -> ../sys/firmware/devicetree/base

这种设计是为了平滑过渡:既遵循了新的内核规范(使用 sysfs),又不破坏旧有用户态程序的兼容性。但从本质上讲,设备树的 “源头” 仍是 sysfs 路径,/proc 下的目录仅为兼容而存在,并非标准实现。

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device

tree 目录下根据节点名字创建不同文件夹,如图 43.5.1 所示:

图 43.5.1 就是目录/proc/device-tree 目录下的内容,/proc/device-tree 目录下是根节点“/”的

所有属性和子节点,我们依次来看一下这些属性和子节点。

根节点“/”各个属性

在图 43.5.1 中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图 43.5.1 中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的 5个属性。既然是文件那么肯定可以查看其内容,输入 cat 命令来查看model和 compatible 这两个文件的内容,结果如图 43.5.2 所示:

从图 43.5.2 可以看出,文件 model 的内容是“Freescale i.MX6 ULL 14x14 EVK Board”,文 件compatible 的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。打开文件 imx6ull-alientek-emmc.dts查看一下,这不正是根节点“/”的 model 和 compatible 属性值嘛!

根节点“/”各子节点

图 43.5.1 中各个文件夹(图中粗字体文件夹)就是根节点“/”的各个子节点,比如“aliases”、 “backlight”、“chosen”和“clocks”等等。大家可以查看一下 imx6ull-alientek-emmc.dts 和 imx6ull.dtsi 这两个文件,看看根节点的子节点都有哪些,看看是否和图 43.5.1 中的一致。

/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进 入/proc/device-tree/soc 目录中就可以看到 soc 节点的所有子节点,如图 43.5.3 所示:

和根节点“/”一样,图 43.5.3 中的所有文件分别为 soc 节点的属性文件和子节点文件夹。

大家可以自行查看一下这些属性文件的内容是否和 imx6ull.dtsi 中 soc 节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看 soc 节点的子节点信息。

Linux 内核解析 DTB 文件

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备 树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图 43.7.1 所示:

从图 43.7.1 中可以看出,在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工

作的函数为 unflatten_dt_node。

Linux内核如何确认是否匹配设备? 

每个节点都有 compatible 属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:

可以看出,compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了,设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来我们就来学习一下 Linux 内核在使用设备树前后是如何判断是否支持某款设备的。这里的设备说的是开发板吧?不是说外设设备。

使用设备树之前设备匹配方法

在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备,比如在文件 arch/arm/mach-imx/mach-mx35_3ds.c 中有如下定义:

上述代码就是定义了“Freescale MX35PDK”这个设备,其中 MACHINE_START和MACHINE_END 定义在文件 arch/arm/include/asm/mach/arch.h 中,内容如下:

根据 MACHINE_START 和 MACHINE_END 的宏定义,将示例代码 43.3.4.2 展开后如下所示:

从示例代码 43.3.4.3 中可以看出,这里定义了一个 machine_desc 类型的结构体变量 __mach_desc_MX35_3DS , 这 个 变 量 存 储 在 “ .arch.info.init ” 段 中 。 第 4 行 的 MACH_TYPE_MX35_3DS 就 是 “ Freescale MX35PDK ” 这 个 板 子 的 machine id 。 MACH_TYPE_MX35_3DS 定义在文件 include/generated/mach-types.h 中,此文件定义了大量的 machine id,内容如下所示:

第 287 行就是 MACH_TYPE_MX35_3DS 的值,为 1645。  

前面说了,uboot 会给 Linux 内核传递 machine id 这个参数,Linux 内核会检查这个 machine id,其实就是将 machine id 与示例代码 43.3.4.3 中的这些 MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动 Linux 内核。

使用设备树以后的设备匹配方法

当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了DT_MACHINE_START。

可以看出,DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同,在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。

打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:

machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,示例代码 43.3.4.5 中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul"和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。imx6ull-alientek-emmc.dts 中根节点的 compatible 属性值如下:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

其中“fsl,imx6ull”与 imx6ul_dt_compat 中的“fsl,imx6ull”匹配,因此 I.MX6U-ALPHA 开发板可以正常启动 Linux 内核。如果将 imx6ull-alientek-emmc.dts 根节点的 compatible 属性改为其他的值,比如:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ullll"

 重新编译 DTS,并用新的 DTS 启动 Linux 内核,结果如图 43.3.4.1 所示的错误提示:

当我们修改了根节点 compatible 属性内容以后,因为 Linux 内核找不到对应的设备,因此Linux 内核无法启动。在 uboot 输出 Starting kernel…以后就再也没有其他信息输出了。

设备树常用 OF 操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存 器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

查找节点的 OF 函数 

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定 义在文件 include/linux/of.h 中,定义如下:

与查找节点有关的 OF 函数有 5 个,我们依次来看一下。

1、of_find_node_by_name 函数

of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_name(struct device_node *from, const char *name);

函数参数和返回值含义如下:

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

name:要查找的节点名字。

返回值:找到的节点,如果为 NULL 表示查找失败。

2、of_find_node_by_type 函数

of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

函数参数和返回值含义如下:

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。

返回值:找到的节点,如果为 NULL 表示查找失败。

3、of_find_compatible_node 函数

of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:

struct device_node *of_find_compatible_node(struct device_node *from,
                                            const char *type, 
                                            const char *compatible)

函数参数和返回值含义如下:

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示 忽略掉 device_type 属性。

compatible:要查找的节点所对应的 compatible 属性列表。

返回值:找到的节点,如果为 NULL 表示查找失败

4、of_find_matching_node_and_match 函数

of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,函数原型如下:

struct device_node *of_find_matching_node_and_match(struct device_node *from,
                                               const struct of_device_id *matches,
                                               const struct of_device_id **match)

函数参数和返回值含义如下:

from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。

matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。

match:找到的匹配的 of_device_id。

返回值:找到的节点,如果为 NULL 表示查找失败

5、of_find_node_by_path 函数

of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:

inline struct device_node *of_find_node_by_path(const char *path)

函数参数和返回值含义如下:

path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。

返回值:找到的节点,如果为 NULL 表示查找失败

查找父/子节点的 OF 函数

Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数,我们依次来看一下。

1、of_get_parent 函数

of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:

struct device_node *of_get_parent(const struct device_node *node)

函数参数和返回值含义如下:

node:要查找的父节点的节点。

返回值:找到的父节点。

2、of_get_next_child 函数

of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:

struct device_node *of_get_next_child(const struct device_node *node,
                                         struct device_node *prev)

函数参数和返回值含义如下:

node:父节点。

prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为

NULL,表示从第一个子节点开始。

返回值:找到的下一个子节点。

注意,以上函数基本都是返回设备节点结构体; 

提取属性值的 OF 函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内 核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,内容如下:

1、of_find_property 函数

of_find_property 函数用于查找指定的属性,函数原型如下:

property *of_find_property(const struct device_node *np,
                             const char *name,
                             int *lenp)

函数参数和返回值含义如下:

np:设备节点。

name: 属性名字。

lenp:属性值的字节数

返回值:找到的属性。

2、of_property_count_elems_of_size 函数

of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个

数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

int of_property_count_elems_of_size(const struct device_node *np,
                                    const char *propname,
                                    int elem_size)

函数参数和返回值含义如下:

np:设备节点。

proname: 需要统计元素数量的属性名字。

elem_size:元素长度。

返回值:得到的属性元素数量。

3、of_property_read_u32_index 函数

of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32

位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:

int of_property_read_u32_index(const struct device_node *np,
                                 const char *propname,
                                 u32 index, 
                                 u32 *out_value)

函数参数和返回值含义如下:

np:设备节点。

proname: 要读取的属性名字。

index:要读取的值标号。

out_value:读取到的值

返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有

要读取的数据,-EOVERFLOW 表示属性值列表太小。

4、

of_property_read_u8_array 函数

of_property_read_u16_array 函数

of_property_read_u32_array 函数

of_property_read_u64_array 函数

这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属

性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:

函数参数和返回值含义如下:

np:设备节点。

proname: 要读取的属性名字。

out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。

sz:要读取的数组元素数量。

返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没

有要读取的数据,-EOVERFLOW 表示属性值列表太小。

5、

of_property_read_u8 函数

of_property_read_u16 函数

of_property_read_u32 函数

of_property_read_u64 函数

有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用

于读取 u8、u16、u32 和 u64 类型属性值,函数原型如下:

函数参数和返回值含义如下:

np:设备节点。

proname: 要读取的属性名字。

out_value:读取到的数组值。

返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没

有要读取的数据,-EOVERFLOW 表示属性值列表太小。

6、of_property_read_string 函数

of_property_read_string 函数用于读取属性中字符串值,函数原型如下:

int of_property_read_string(struct device_node *np, 
                             const char *propname,
                             const char **out_string)

函数参数和返回值含义如下:

np:设备节点。

proname: 要读取的属性名字。

out_string:读取到的字符串值。

返回值:0,读取成功,负值,读取失败。

7、of_n_addr_cells 函数

of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np)

函数参数和返回值含义如下:

np:设备节点。

返回值:获取到的#address-cells 属性值。

8、of_n_size_cells 函数

of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:

int of_n_size_cells(struct device_node *np)

函数参数和返回值含义如下:

np:设备节点。

返回值:获取到的#size-cells 属性值。

注意,以上只有of_find_property 函数返回属性结构体,其他都是返回int类型 

其他常用的 OF 函数

1、of_device_is_compatible 函数

of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字

符串,也就是检查设备节点的兼容性,函数原型如下:

int of_device_is_compatible(const struct device_node *device,
                             const char *compat)

函数参数和返回值含义如下:

device:设备节点。

compat:要查看的字符串。

返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible

属性中包含 compat 指定的字符串。

2、of_get_address 函数

of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性

值,函数原型如下:

const __be32 *of_get_address(struct device_node *dev, 
                                int index, 
                                u64 *size,
                                 unsigned int *flags)

函数参数和返回值含义如下:

dev:设备节点。

index:要读取的地址标号。

size:地址长度。

flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等

返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。

3、of_translate_address 函数

of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:

u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)

4of_address_to_resource 函数

IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux

内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如下:

对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志定义在文件 include/linux/ioport.h 中,如下所示:

大 家 一 般 最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和

IORESOURCE_IRQ 等。接下来我们回到 of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型,函数原型如下所示

int of_address_to_resource(struct device_node *dev, 
                             int index,
                             struct resource *r)

函数参数和返回值含义如下:

dev:设备节点。

index:地址资源标号。

r:得到的 resource 类型的资源值

 返回值:0,成功;负值,失败。  

5of_iomap 函数  

of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地

址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:

void __iomem *of_iomap(struct device_node *np, int index)

函数参数和返回值含义如下:

np:设备节点。

index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。

返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

关于设备树常用的 OF 函数就先讲解到这里,Linux 内核中关于设备树的 OF 函数不仅仅只 有前面讲的这几个,还有很多 OF 函数我们并没有讲解,这些没有讲解的 OF 函数要结合具体的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数我们在后面的驱动实验中再详细的讲解。

注意,of函数总体分为三类,一类是查找节点,返回一个节点的结构体;第二类是查找属性,基于节点查找对应的属性值;第三类就是一些辅助类的of函数。

补充

一套Linux程序能使用多个设备树文件吗?

在 Linux 系统中,通常情况下内核在启动时只会加载并解析一个设备树文件(DTB),但在特定场景下,系统可以通过动态设备树(Dynamic Device Tree)机制使用多个设备树片段(overlay)来扩展或修改基础设备树的配置。

1. 常规情况:单设备树文件

Linux 内核启动流程中,设备树的处理通常是 “一次性” 的:

  • 启动时,引导程序(如 U-Boot)将一个完整的 DTB 文件加载到内存,并传递给内核。
  • 内核解析该 DTB,初始化所有硬件设备,之后 DTB 不再被修改(静态设备树)。

这种情况下,系统仅使用一个设备树文件,适用于硬件配置固定的场景(如大多数嵌入式设备)。

2. 特殊场景:动态设备树(多个设备树片段)

通过 设备树覆盖(Device Tree Overlay,DTO) 机制,系统可以在基础 DTB 之外加载多个设备树片段(.dtbo 文件),实现硬件配置的动态修改。

工作原理:

  • 基础设备树(base DTB):包含系统的核心硬件配置(如 CPU、内存、主要控制器)。
  • 设备树片段(overlay .dtbo):小型设备树文件,仅包含部分硬件的配置(如扩展板、外设),用于修改或补充基础 DTB 的内容。
  • 动态应用:内核启动后,通过用户态工具(如 dtcoverlayctl)将 .dtbo 加载到内核,动态修改设备树节点(添加 / 删除节点、修改属性),触发设备的重新枚举或配置。

典型用途:

  • 支持可插拔硬件(如开发板扩展模块),无需重新编译基础 DTB。
  • 动态启用 / 禁用外设(如摄像头、传感器)。
  • 同一硬件平台适配不同配置(如不同版本的扩展板)。

示例流程:

  1. 内核加载基础 DTB(如 base.dtb),初始化核心硬件。
  2. 用户态加载扩展片段(如 camera_overlay.dtbo),动态添加摄像头节点。
  3. 内核检测到设备树变化,为新增的摄像头节点匹配驱动并初始化。

3. 限制与注意事项

  • 并非所有内核都支持:动态设备树需要内核启用 CONFIG_OF_OVERLAY 配置,且对硬件驱动有一定要求(需支持动态 probe/remove)。
  • 片段依赖基础 DTB:设备树片段不能独立使用,必须依赖基础 DTB 的节点结构(如通过 target-path 指定要修改的节点)。
  • 复杂性:多个片段可能存在冲突(如修改同一节点的同一属性),需要严格管理依赖关系。

总结

  • 常规场景:Linux 系统仅使用一个基础设备树文件(DTB)。
  • 高级场景:通过设备树覆盖(DTO)机制,可以动态加载多个设备树片段(.dtbo),扩展或修改基础配置。

这种设计兼顾了嵌入式系统的稳定性(基础配置固定)和灵活性(支持动态扩展),广泛应用于需要适配多种硬件组合的场景(如开发板、工业控制设备)。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐