欢迎光临
我们一直在努力

Linux 设备树(Device Tree)概览

Linux 设备树(Device Tree)概览

适用版本:Linux 3.x ~ 6.x 目标读者:嵌入式驱动开发工程师、系统架构师、BSP 维护者


目录

  • 设计哲学与历史演进
  • 核心概念与术语体系
  • 设备树语法规范
  • 地址映射与中断机制
  • 内核解析与设备模型整合
  • 设备树生命周期与内存管理
  • 设备树覆盖(Device Tree Overlay)
  • 绑定机制深度解析(Binding Mechanism)
  • 驱动开发中的 OF API + 系统化速查表 + 诊断手册
  • 子系统级节点详解(Pinctrl / Clock / GPIO / DMA)
  • YAML Schema 验证与 dt-bindings
  • 工程语法高级特性(DTS 进阶)
  • Zephyr 与 Linux 设备树对比
  • 与其他固件接口对比(ACPI / RISC-V / FDT 启动协议)
  • 版本演进与关键内核里程碑
  • 工程实践与调试技巧
  • 完整示例:从 DTS 到驱动

  • 1. 设计哲学与历史演进

    1.1 问题起源:ARM 生态的碎片化困境

    在设备树引入之前(Linux 2.6 及更早),ARM 架构的硬件描述采用静态硬编码方式:

    • 每个 SoC/板卡在内核 arch/arm/mach-xxx/ 目录下维护独立的 BSP 代码
    • 内核中仅 ARM 架构就存在 200+ 个 mach-目录
    • 任何硬件变更(如更换 UART 引脚、调整内存大小)都需要重新编译内核
    • 同一内核镜像无法跨板卡复用

    这种模式的根本问题在于:硬件描述与操作系统内核强耦合。内核中充斥着大量与特定 SoC 绑定的 C 代码,导致:

  • 代码冗余:不同板卡使用相同 SoC 时,大量时钟、GPIO、中断代码重复
  • 维护困难:硬件迭代需要修改内核源码,涉及复杂的编译和发布流程
  • 生态碎片化:每个厂商维护自己的内核分支,无法汇聚到主线
  • 1.2 核心思想:硬件描述与操作系统解耦

    设备树(Device Tree)借鉴了 Open Firmware(IEEE 1275)标准,其核心设计目标是将硬件描述从操作系统源码中彻底剥离:

    维度传统 BSP 模式设备树模式
    硬件描述位置 内核源码(C 结构体) 独立的 .dts 文本文件
    内核镜像 每板卡一个镜像 单一内核 + 多份 DTB
    硬件变更成本 修改源码、重新编译内核 修改 DTS、单独编译 DTB
    可维护性 低(代码冗余) 高(声明式描述)
    多板卡支持 差 优(同一内核适配不同 DTB)
    上游化难度 高(侵入内核核心) 低(独立描述文件)

    更深层的哲学:设备树采用**声明式(Declarative)而非命令式(Imperative)**描述。它不告诉内核「如何初始化硬件」,而是告诉内核「硬件是什么」。这种设计使得内核可以专注于「如何驱动」的通用逻辑,而硬件差异由设备树声明。

    1.3 关键历史节点

    • 2005:PowerPC 架构率先引入设备树支持(IBM 推动)
    • 2011(Linux 3.x):ARM 架构全面转向设备树,标志着 ARM Linux 从「板级支持」向「通用内核 + 设备描述」转型。这是 ARM 内核历史上最大的架构重构之一。
    • 2014(Linux 3.15+):Device Tree Overlay 机制成熟,支持动态外设扩展
    • 2018(Linux 4.20+):DT Schema 验证框架引入,设备树从「语法正确」走向「语义正确」
    • 2020+:设备树成为 RISC-V、ARM64、部分 MIPS 平台的事实标准

    2. 核心概念与术语体系

    2.1 三件套:DTS / DTC / DTB

    在这里插入图片描述

    设备树三件套关系图 —— DTS 源文件经 DTC 编译为 DTB,DTB 由 Bootloader 加载并传递给内核;.dtsi 作为头文件被 .dts 包含,DTC 也支持将 DTB 反编译回 DTS。

    术语全称作用
    DTS Device Tree Source 设备树源码,人类可读的硬件描述文本
    DTSI Device Tree Source Include 设备树头文件,通常包含 SoC 级通用定义
    DTC Device Tree Compiler 设备树编译器,负责 DTS↔DTB 转换
    DTB Device Tree Blob 编译后的二进制设备树,内核启动时解析
    FDT Flattened Device Tree 扁平化设备树,即 DTB 的内存表示形式

    2.2 设备树与内核设备模型的关系

    在这里插入图片描述

    设备树与 Linux 内核设备模型的关系 —— Bootloader 将 DTB 加载到内存并传递给内核;内核通过 drivers/of/ 将扁平化的 FDT 解析为 device_node,再转换为 platform_device;最终在内核设备模型中与 device_driver 匹配并触发 probe(),用户空间通过 /sys、/dev、/proc 访问设备信息。

    关键理解:设备树不是驱动,而是硬件的声明式描述。内核通过解析设备树,将硬件信息转化为内核设备模型中的 device_node → platform_device → struct device,最终与 device_driver 匹配。


    3. 设备树语法规范

    3.1 基本结构

    /dts-v1/; // 版本声明,必须位于首行
    #include "soc.dtsi" // 包含头文件

    / { // 根节点
    model = "My Development Board";
    compatible = "vendor,board";

    #address-cells = <2>; // 子节点 reg 地址用 2 个 u32 表示
    #size-cells = <2>; // 子节点 reg 大小用 2 个 u32 表示

    // 子节点:CPU
    cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    cpu0: cpu@0 {
    compatible = "arm,cortex-a53";
    reg = <0>; // CPU 编号
    };
    };

    // 子节点:内存
    memory@80000000 {
    device_type = "memory";
    reg = <0x0 0x80000000 0x0 0x40000000>; // 起始地址 + 大小 = 1GB
    };

    // 子节点:串口
    uart0: serial@10000000 {
    compatible = "ns16550a";
    reg = <0x0 0x10000000 0x0 0x100>;
    interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk_uart0>;
    status = "okay";
    };
    };

    3.2 节点命名规范

    label: node-name@unit-address

    元素说明示例
    label 标签,用于引用(&label) uart0:
    node-name 节点名称,小写 + 连字符 serial, gpio-keys
    @unit-address 设备在主总线上的首地址 @10000000

    规则:

    • 若节点无寄存器地址(如 cpus、leds),可省略 @unit-address
    • 同一父节点下,node-name@unit-address 必须唯一
    • 标签(label)仅在编译时有效,不出现在 DTB 中

    3.3 关键属性详解

    3.3.1 compatible —— 驱动匹配的基石

    compatible = "vendor,specific-model", "generic-family";

    • 作用:驱动的匹配标识符,内核按从左到右优先级匹配
    • 驱动侧:通过 of_device_id 数组声明支持的 compatible 字符串
    • 策略:优先写具体型号,后缀写通用系列,确保兼容性回退

    // 驱动代码示例
    static const struct of_device_id my_uart_ids[] = {
    { .compatible = "vendor,specific-model" },
    { .compatible = "generic-family" },
    { }
    };
    MODULE_DEVICE_TABLE(of, my_uart_ids);

    3.3.2 reg —— 寄存器地址与资源描述

    reg = <address1 size1 [address2 size2 …]>;

    • 长度由父节点的 #address-cells 和 #size-cells 决定
    • 典型场景:
      • 内存映射 IO:reg = <0x0 0x10000000 0x0 0x100>;(64 位地址 + 32 位大小)
      • 多资源设备:reg = <0x3000 0x100>, <0x4000 0x100>;(2 个寄存器区域)
    3.3.3 interrupts 与中断控制器

    interrupts = <type number trigger>;

    • GIC 中断类型:GIC_SPI(共享外设中断)、GIC_PPI(私有外设中断)
    • 触发方式:IRQ_TYPE_EDGE_RISING, IRQ_TYPE_LEVEL_HIGH 等
    • 中断映射:通过 interrupt-parent 指向中断控制器节点,或继承父节点设置

    // 中断控制器定义
    intc: interrupt-controller@e0000000 {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>; // 中断描述占 3 个 u32
    interrupt-controller; // 标记为中断控制器
    };

    // 外设引用中断控制器
    uart0: serial@10000000 {
    interrupt-parent = <&intc>; // 继承中断控制器
    interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
    };

    3.3.4 status —— 设备使能开关
    值含义
    "okay" 设备使能,内核将为其创建设备实例
    "disabled" 设备禁用,内核跳过该节点(但保留在树中)
    自定义字符串 某些厂商用于特定状态(如 "reserved")

    3.4 引用与覆盖(Phandle & Override)

    // 在根节点外引用并修改已有节点
    &uart0 {
    status = "disabled"; // 关闭 uart0
    current-speed = <115200>; // 修改波特率
    };

    // 使用 phandle 引用时钟节点
    clocks = <&clk_uart0>; // &clk_uart0 是 clk 节点的标签引用


    4. 地址映射与中断机制

    4.1 地址映射体系

    在这里插入图片描述

    设备树地址映射 —— CPU 物理地址通过设备树节点(如 memory@80000000、serial@10000000)描述,经 SoC 总线的 ranges 属性完成地址转换,再映射到 PCI / AMBA 等外设桥接总线。

    4.1.1 ranges 属性

    soc {
    compatible = "simple-bus";
    #address-cells = <2>;
    #size-cells = <2>;
    ranges = <0x0 0x10000000 0x0 0x10000000 0x0 0x10000000>;
    // 子总线地址 父总线地址 长度
    // 实现子地址空间到父地址空间的 1:1 映射
    };

    4.2 中断映射体系

    // 中断控制器节点
    intc: interrupt-controller@e0000000 {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    #address-cells = <0>; // 中断映射无地址单元
    };

    // 中断映射节点(用于多路复用/转换)
    mybridge: bridge@2000 {
    #interrupt-cells = <1>;
    interrupt-map = <0 0 &intc 0 23 1>, // 输入0 → GIC SPI 23
    <1 0 &intc 0 24 1>; // 输入1 → GIC SPI 24
    interrupt-map-mask = <0x1 0>; // 掩码决定哪些位参与匹配
    };

    中断映射流程:

  • 设备声明 interrupts = <0>;
  • 内核查找 interrupt-parent(或继承父节点)
  • 若父节点有 interrupt-map,按掩码匹配转换规则
  • 最终映射到根中断控制器的全局中断号
  • 驱动通过 platform_get_irq() 或 irq_of_parse_and_map() 获取 Linux IRQ 编号

  • 5. 内核解析与设备模型整合

    5.1 启动时解析流程(ARM 架构)

    在这里插入图片描述

    ARM 架构启动时设备树解析流程 —— Bootloader 按 ARM 启动协议(r0=0, r1=machine ID, r2=DTB 物理地址)将 DTB 加载到内存;内核入口 start_kernel 调用 setup_arch(),期间通过 early_init_dt_scan() 早期扫描获取内存与命令行参数;随后 unflatten_device_tree() 将扁平 FDT 展开为 device_node,of_platform_default_populate() 将其转换为 platform_device 并注册到平台总线;最终由 platform_match 完成设备-驱动匹配并调用 driver.probe(),设备可用。

    5.2 核心数据结构

    // 1. 设备树节点(展开后)
    struct device_node {
    const char *name; // 节点名称
    const char *type; // 设备类型(如 "memory")
    phandle phandle; // 唯一标识符
    struct device_node *parent; // 父节点
    struct device_node *child; // 子节点
    struct device_node *sibling;// 兄弟节点
    struct property *properties; // 属性链表
    // …
    };

    // 2. 平台设备(device_node 的驱动模型封装)
    struct platform_device {
    const char *name;
    int id;
    struct device dev; // 内嵌通用设备结构
    u32 num_resources;
    struct resource *resource; // 从 reg 转换而来的资源数组
    // …
    };

    5.3 从 device_node 到 platform_device 的转换

    // drivers/of/platform.c
    int of_platform_populate(struct device_node *root,
    const struct of_device_id *matches,
    const struct of_dev_auxdata *lookup,
    struct device *parent)
    {
    // 遍历根节点下所有子节点
    for_each_child_of_node(root, child) {
    // 为每个节点创建 platform_device 并注册
    of_platform_bus_create(child, matches, lookup, parent, true);
    }
    }

    // 关键转换函数
    static struct platform_device *of_platform_device_create_pdata(
    struct device_node *np,
    const char *bus_id,
    void *platform_data,
    struct device *parent)
    {
    dev = of_device_alloc(np, bus_id, parent); // 分配 platform_device
    dev->dev.bus = &platform_bus_type; // 挂载到 platform 总线
    of_device_add(dev); // 注册到设备模型
    }

    5.4 匹配机制:设备树如何找到驱动

    在这里插入图片描述

    图:platform 总线设备-驱动匹配流程 —— platform_device 通过 dev.of_node 携带设备树节点信息,platform_driver 通过 driver.of_match_table 提供支持的 compatible 列表;匹配时从设备节点提取 compatible 并与驱动的 match 表比对,成功后调用 driver.probe() 完成设备初始化。

    // 平台总线匹配逻辑(简化)
    static int platform_match(struct device *dev, struct device_driver *drv)
    {
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    // 1. 优先使用设备树的 OF 匹配
    if (pdev->dev.of_node) {
    return of_driver_match_device(dev, drv); // 比对 compatible
    }

    // 2. 回退到 ACPI / ID 表匹配
    // …
    }


    6. 设备树生命周期与内存管理

    定位:理解设备树数据结构在内核中的创建、引用、释放全过程,是排查驱动加载顺序异常、内存泄漏、Overlay 卸载失败等深层问题的必备知识。

    6.1 核心数据结构的生命周期

    在这里插入图片描述

    设备树核心数据结构生命周期 —— Bootloader 加载 DTB 后,内核首先得到物理地址空间中的 FDT;经 unflatten_device_tree() 展开为内核堆中的 device_node 树;再由 of_platform_populate() 按需创建 platform_device,其 dev.of_node 指向对应 device_node 并使引用计数加 1;最终在 driver.probe() 中分配的驱动私有数据通过 devm_ 系列 API 与 device 生命周期绑定,remove 时自动释放。

    6.2 device_node 引用计数机制

    struct device_node 采用引用计数管理生命周期,核心 API:

    struct device_node *of_node_get(struct device_node *node); // 引用计数 +1
    void of_node_put(struct device_node *node); // 引用计数 -1,若归零则释放

    必须成对使用的场景:

    场景获取引用释放引用不释放的后果
    驱动持有 dev->of_node of_node_get(dev->of_node) of_node_put(dev->of_node) 在 remove() device_node 内存泄漏
    遍历子节点 of_get_next_child(parent, prev) of_node_put(child) 子节点无法释放
    查找节点 of_find_node_by_path() / of_find_compatible_node() of_node_put() 节点永久驻留内存
    解析 phandle of_find_node_by_phandle() of_node_put() 同上

    内核自动管理的场景(驱动无需手动 put):

    • platform_device.dev.of_node:由 of_device_alloc() 自动 of_node_get(),在 platform_device_release() 时自动 of_node_put()
    • devm_ 系列 API 绑定的资源:如 devm_clk_get()、devm_gpiod_get() 内部自动处理节点引用

    6.3 of_platform_populate() 递归展开规则

    of_platform_populate() 是设备树到平台设备的核心转换入口,其递归规则决定了哪些节点会被自动注册:

    // drivers/of/platform.c
    int of_platform_populate(struct device_node *root,
    const struct of_device_id *matches,
    const struct of_dev_auxdata *lookup,
    struct device *parent)
    {
    for_each_child_of_node(root, child) {
    // 1. 检查 status,若为 "disabled" 则跳过
    if (of_device_is_available(child)) {
    // 2. 创建 platform_device
    of_platform_device_create(child, matches, lookup, parent);

    // 3. 若子节点是总线(如 compatible = "simple-bus"),递归展开
    if (of_match_node(matches, child) ||
    of_device_is_compatible(child, "simple-bus")) {
    of_platform_bus_create(child, matches, lookup, parent, true);
    }
    }
    }
    }

    自动展开的触发条件:

    条件行为典型节点
    compatible = "simple-bus" 遍历所有子节点并注册 soc { compatible = "simple-bus"; }
    compatible = "simple-mfd" 多函数设备展开 电源管理/时钟复合器件
    matches 表命中 按架构特定规则展开 ARM AMBA 总线、PCI 总线
    #address-cells 存在 具备可寻址子设备的总线 I2C、SPI 控制器(需驱动额外处理)

    重要结论:

    • 只有 platform_bus_type 上的设备由 of_platform_populate() 自动创建
    • I2C/SPI 子设备不由该函数直接创建,而是由各自的 i2c_add_adapter() / spi_register_master() 在控制器驱动 probe 后,通过 of_register_i2c_devices() / spi_of_register_devices() 二次扫描创建
    • PCI 设备通过 of_pci_scan_root() 等专用函数扫描,不走 platform 路径

    6.4 platform_device 与 device_node 的绑定关系

    struct platform_device {
    struct device dev;
    // …
    };

    struct device {
    // …
    struct device_node *of_node; // 指向 device_node(引用计数已增加)
    // …
    };

    生命周期绑定链:

    platform_device_register() / platform_device_add()
    └── of_device_alloc() // 分配 platform_device
    ├── of_node_get(np) // device_node 引用计数 +1
    └── dev->of_node = np // 建立绑定
    │
    ▼
    driver.probe(pdev) // 驱动通过 pdev->dev.of_node 访问设备树
    │
    ▼
    platform_device_unregister() / platform_device_del()
    └── platform_device_release()
    └── of_node_put(dev->of_node) // 引用计数 -1

    关键理解:platform_device 的注册与注销自动管理 device_node 的引用计数,驱动开发者通常无需手动干预。但在以下场景需要显式处理:

  • 驱动直接调用 of_find_node_by_xxx() 获取节点:必须 of_node_put()
  • 驱动在 remove() 中缓存了节点指针:必须 of_node_put()
  • Overlay 动态创建的节点:卸载 Overlay 时会自动释放,但驱动需确保在 remove() 前释放所有私有引用
  • 6.5 Overlay 节点的内存管理

    Overlay 机制动态修改设备树,其内存管理遵循申请 → 应用 → 撤销 → 释放四阶段:

    // 1. 申请:解析 DTBO 并创建变更记录
    of_overlay_apply(np, &ov_id); // np = overlay 设备树节点
    │
    ▼
    // 2. 应用:创建新的 device_node 和 platform_device
    of_overlay_apply_changes(ov_id)
    ├── of_changeset_add_property() // 新增/修改属性
    ├── of_changeset_add_node() // 新增节点
    └── of_changeset_attach_node() // 附加到目标节点
    │
    ▼
    // 3. 撤销:移除变更并释放资源
    of_overlay_remove(ov_id);
    ├── of_changeset_revert() // 回滚所有变更
    ├── of_node_put() 所有新增节点 // 引用计数归零 → 释放
    └── platform_device_unregister() // 注销动态创建的设备

    Overlay 内存管理要点:

    阶段内核行为驱动注意事项
    应用 新增节点引用计数初始为 1 若驱动在 probe 中 of_node_get(),需确保 remove 时 of_node_put()
    撤销 调用 of_node_put() 递减引用 若驱动仍持有引用,节点不会释放,导致内存泄漏
    失败回滚 自动调用 of_changeset_revert() 驱动需处理 probe() 失败时的资源清理

    常见内存泄漏场景:

    // 错误示例:Overlay 卸载后节点泄漏
    static int mydrv_probe(struct platform_device *pdev)
    {
    struct device_node *np = pdev->dev.of_node;
    struct device_node *ref = of_parse_phandle(np, "ref-node", 0);
    // ref 节点引用计数 +1,但驱动未保存指针到私有数据
    // Overlay 卸载时,ref 节点引用计数无法归零,永久泄漏
    return 0;
    }

    // 正确做法:保存到私有数据并在 remove 中释放
    struct mydrv_priv {
    struct device_node *ref_node;
    };

    static int mydrv_probe(struct platform_device *pdev)
    {
    struct mydrv_priv *priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    priv->ref_node = of_parse_phandle(pdev->dev.of_node, "ref-node", 0);
    platform_set_drvdata(pdev, priv);
    return 0;
    }

    static int mydrv_remove(struct platform_device *pdev)
    {
    struct mydrv_priv *priv = platform_get_drvdata(pdev);
    of_node_put(priv->ref_node); // 显式释放引用
    return 0;
    }

    6.6 调试引用计数泄漏

    # 查看当前系统 device_node 总数(需开启 CONFIG_DEBUG_FS)
    cat /sys/kernel/debug/devices_deferred

    # 查看特定节点的引用计数(通过 sysfs 间接观察)
    ls -la /sys/firmware/devicetree/base/soc/serial@10000000/ # 若目录存在,说明节点仍在内存

    # 动态检查:在驱动中添加调试打印
    printk("node %p refcount: %d\\n", np, kref_read(&np->kref));


    7. 设备树覆盖(Device Tree Overlay)

    7.1 设计目标

    • 场景:动态外设扩展(如树莓派 HAT 扩展板、FPGA 动态加载逻辑)
    • 问题:不重新编译主 DTB,运行时叠加修改
    • 方案:编译 .dtbo 文件,通过 configfs 动态加载

    7.2 Overlay 语法

    // pwm-overlay.dts
    /dts-v1/;
    /plugin/; // 标记为插件式覆盖

    / { // 覆盖根节点
    fragment@0 {
    target = <&pio>; // 目标节点:pio 控制器
    __overlay__ {
    pwm7_pin: pwm7 {
    pins = "PD22";
    function = "pwm7";
    };
    };
    };

    fragment@1 {
    target = <&pwm>; // 目标节点:pwm 控制器
    __overlay__ {
    pinctrl-names = "default";
    pinctrl-0 = <&pwm7_pin>;
    status = "okay";
    };
    };
    };

    7.3 加载流程

    # 1. 编译 overlay(注意 -@ 参数保留符号信息)
    dtc -@ -I dts -O dtb -o pwm7.dtbo pwm-overlay.dts

    # 2. 通过 configfs 动态加载(内核需开启 CONFIG_OF_OVERLAY)
    mkdir -p /config/device-tree/overlays/pwm7
    cat pwm7.dtbo > /config/device-tree/overlays/pwm7/dtbo

    # 3. 验证
    cat /config/device-tree/overlays/pwm7/status # 显示 applied


    8. 绑定机制深度解析(Binding Mechanism)

    定位:设备树与驱动之间的绑定(Binding)是设备树体系的核心机制。它不仅是 compatible 字符串的简单匹配,而是涉及设备模型、总线匹配、OF 核心层、驱动注册的多层协作体系。理解绑定机制,是排查「驱动 probe 不触发」「设备无法识别」等问题的根本。

    8.1 绑定的本质:从硬件描述到驱动执行的桥梁

    绑定的本质问题是:内核如何将设备树中的一个节点,与已加载的某个驱动关联起来?

    在这里插入图片描述

    绑定机制全景图 —— DTS 节点通过 compatible 声明设备身份,驱动通过 of_match_table 声明可处理的设备;两者由 platform_bus_type.match() / of_driver_match_device() 执行 compatible 比对,匹配成功后调用 driver.probe(pdev) 完成设备初始化与资源注册。

    8.2 绑定的三层模型

    Linux 设备树绑定机制采用三层模型:

    层级组件职责关键代码路径
    第一层:设备声明层 DTS 节点 + compatible 声明「我是谁」 .dts 源文件
    第二层:匹配中介层 platform_bus_type + OF 核心 执行「谁适合我」 driver/base/platform.c / drivers/of/device.c
    第三层:驱动响应层 platform_driver + of_match_table 响应「我来处理」 驱动源码中的 of_match_table

    8.3 第一层:设备声明层 —— compatible 的语义设计

    8.3.1 compatible 的命名规范

    compatible 字符串采用 "vendor,model" 格式,这是强制规范,不是建议:

    // 正确示例
    compatible = "myvendor,myuart-v1", "myvendor,myuart", "generic-uart";

    // 错误示例(缺少 vendor 前缀)
    compatible = "uart-controller"; // ❌ 过于通用,易冲突

    命名策略:

    位置语义示例
    第一个字符串 最精确的型号 "myvendor,myuart-v1"
    第二个字符串 系列通用型号 "myvendor,myuart"
    第三个字符串 跨厂商通用型号 "generic-uart"

    为什么需要多个 compatible?

  • 向后兼容:新驱动支持旧设备(通过系列通用型号)
  • 向前兼容:旧驱动支持新设备(通过通用型号回退)
  • 代码复用:同一驱动支持多个硬件变体
  • 8.3.2 compatible 的存储格式

    在 DTB 中,compatible 不是普通字符串,而是以 , 分隔的字符串列表:

    compatible 字符串内存分段(字节数)总长度
    myvendor,myuart-v1 myvendor(8) + ,(1) + myuart-v1(10) + \\0(1) 20
    myvendor,myuart myvendor(8) + ,(1) + myuart(6) + \\0(1) 16
    generic-uart generic(7) + -(1) + uart(4) + \\0(1) 13

    注:DTB 中的 compatible 是多个 以 \\0 结尾的字符串顺序拼接而成的属性,内核读取后按 null 终止符切分,再对每个字符串内部的 , 或 – 做语义解析。

    内核通过 of_get_property() 获取原始二进制,再通过 of_device_is_compatible() 逐个 , 分隔字符串进行匹配。

    8.4 第二层:匹配中介层 —— 内核匹配源码级分析

    8.4.1 platform_match() 的完整调用链

    // driver/base/platform.c
    static int platform_match(struct device *dev, struct device_driver *drv)
    {
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    // 1. 尝试 OF 风格匹配(设备树优先)
    if (pdev->dev.of_node) {
    // 调用 OF 核心匹配函数
    return of_driver_match_device(dev, drv);
    }

    // 2. 尝试 ACPI 风格匹配
    if (pdev->dev.acpi_node) {
    return acpi_driver_match_device(dev, drv);
    }

    // 3. 尝试 ID 表匹配(传统 platform ID)
    if (pdrv->id_table)
    return platform_match_id(pdrv->id_table, pdev) != NULL;

    // 4. 最原始的名字匹配(fallback)
    return (strcmp(pdev->name, drv->name) == 0);
    }

    匹配优先级:设备树(OF) > ACPI > ID 表 > 名字匹配。这意味着只要设备有 of_node,内核就优先走设备树路径,不会回退到名字匹配。

    8.4.2 of_driver_match_device() 的源码解析

    // drivers/of/device.c
    int of_driver_match_device(struct device *dev, struct device_driver *drv)
    {
    // 提取设备的 device_node
    struct device_node *np = dev->of_node;

    // 调用 of_match_device() 进行实际匹配
    return of_match_device(drv->of_match_table, np) != NULL;
    }

    // drivers/of/device.c
    const struct of_device_id *of_match_device(
    const struct of_device_id *matches,
    const struct device_node *np)
    {
    if (!matches || !np)
    return NULL;

    // 遍历 of_match_table 数组
    for (; matches->name[0] || matches->type[0] || matches->compatible[0]; matches++) {
    // 调用 __of_match_node() 进行逐项匹配
    if (__of_match_node(matches, np))
    return matches; // 返回首个匹配项
    }

    return NULL; // 无匹配
    }

    8.4.3 __of_match_node() 的核心匹配逻辑

    // drivers/of/base.c
    static const struct of_device_id *__of_match_node(
    const struct of_device_id *matches,
    const struct device_node *np)
    {
    // 1. 检查 name 匹配(已废弃,现代设备树不使用)
    if (matches->name[0]) {
    if (np->name && !strcmp(np->name, matches->name))
    return matches;
    continue;
    }

    // 2. 检查 type 匹配(已废弃)
    if (matches->type[0]) {
    if (np->type && !strcmp(np->type, matches->type))
    return matches;
    continue;
    }

    // 3. 检查 compatible 匹配(现代设备树的核心)
    if (matches->compatible[0]) {
    // 调用 of_device_is_compatible() 进行字符串比对
    if (of_device_is_compatible(np, matches->compatible))
    return matches;
    continue;
    }

    return NULL;
    }

    8.4.4 of_device_is_compatible() 的字符串比对

    // drivers/of/base.c
    int of_device_is_compatible(const struct device_node *np, const char *compat)
    {
    const char *cp;
    int cplen, l;

    // 获取 compatible 属性的原始二进制数据
    cp = of_get_property(np, "compatible", &cplen);
    if (cp == NULL)
    return 0;

    // 遍历以 ',' 分隔的字符串列表
    while (cplen > 0 && *cp) {
    l = strlen(cp); // 当前字符串长度

    // 精确匹配(strcmp,不是 strstr)
    if (strcmp(cp, compat) == 0)
    return 1; // 匹配成功

    cp += l + 1; // 跳到下一个字符串
    cplen -= l + 1; // 减少剩余长度
    }

    return 0; // 无匹配
    }

    关键理解:

    • 匹配是精确字符串比较(strcmp),不是子串匹配(strstr)
    • 匹配顺序是设备树 compatible 从左到右,驱动 of_match_table 从上到下
    • 第一个匹配成功即停止,体现了**「最精确优先」**的设计哲学

    8.5 第三层:驱动响应层 —— of_match_table 的完整设计

    8.5.1 of_device_id 结构体详解

    // include/linux/mod_devicetable.h
    struct of_device_id {
    char name[32]; // 按节点名匹配(已废弃,留空)
    char type[32]; // 按类型匹配(已废弃,留空)
    char compatible[128]; // 按 compatible 匹配(核心字段)
    const void *data; // 私有数据指针(差异化配置)
    };

    现代驱动中的标准写法:

    static const struct of_device_id myuart_of_match[] = {
    {
    .compatible = "myvendor,myuart-v2",
    .data = &myuart_v2_config, // v2 版本的配置参数
    },
    {
    .compatible = "myvendor,myuart-v1",
    .data = &myuart_v1_config, // v1 版本的配置参数
    },
    {
    .compatible = "generic-uart",
    .data = &generic_uart_config, // 通用回退配置
    },
    { } // 终止符(sentinel),必须存在
    };
    MODULE_DEVICE_TABLE(of, myuart_of_match);

    static struct platform_driver myuart_driver = {
    .probe = myuart_probe,
    .remove = myuart_remove,
    .driver = {
    .name = "myuart",
    .of_match_table = myuart_of_match, // 绑定到驱动
    },
    };

    8.5.2 .data 字段的差异化配置机制

    .data 字段是绑定机制中最强大的设计之一,它允许同一驱动支持不同硬件变体,而无需条件分支:

    // 配置结构体定义
    struct myuart_config {
    u32 fifo_depth;
    u32 max_baudrate;
    bool has_dma;
    bool has_flowctrl;
    };

    // 不同版本的配置参数
    static const struct myuart_config myuart_v2_config = {
    .fifo_depth = 64,
    .max_baudrate = 3000000,
    .has_dma = true,
    .has_flowctrl = true,
    };

    static const struct myuart_config myuart_v1_config = {
    .fifo_depth = 16,
    .max_baudrate = 115200,
    .has_dma = false,
    .has_flowctrl = false,
    };

    // probe 中根据匹配结果获取配置
    static int myuart_probe(struct platform_device *pdev)
    {
    const struct of_device_id *match;
    const struct myuart_config *cfg;

    // of_match_device() 返回匹配的 of_device_id 条目
    match = of_match_device(myuart_of_match, &pdev->dev);
    if (!match)
    return –EINVAL;

    // 获取 .data 中预存的配置指针
    cfg = match->data;

    dev_info(&pdev->dev, "Detected UART variant: %s, FIFO depth=%u\\n",
    match->compatible, cfg->fifo_depth);

    // 根据配置初始化硬件
    if (cfg->has_dma) {
    // 初始化 DMA 通道
    }

    return 0;
    }

    这种设计的优势:

    • 无硬编码条件分支:不需要 if (version == V2) 这样的判断
    • 新增硬件变体只需扩展 of_match_table:不修改核心逻辑
    • 配置与代码分离:硬件参数集中在设备树和配置表中
    8.5.3 MODULE_DEVICE_TABLE(of, …) 的作用

    MODULE_DEVICE_TABLE(of, myuart_of_match);

    这个宏看似只是声明,实则承担关键功能:

  • 编译时:将 of_match_table 导出到模块的 .modinfo 段
  • 加载时:modprobe 或 depmod 读取模块的 alias 信息,建立 of:N*T*C<compatible> 格式的别名
  • 自动加载:当内核检测到设备树中有匹配的 compatible 节点时,通过 uevent 机制自动加载对应模块
  • 模块 alias 的生成规则:

    设备树 compatible: "myvendor,myuart"
    生成模块别名: of:NuartT<NULL>Cmyvendor,myuart

    可以通过 modinfo 验证:

    $ modinfo myuart.ko
    alias: of:N*T*Cmyvendor,myuart
    alias: of:N*T*Cgeneric-uart

    8.6 绑定失败的全场景根因分析

    8.6.1 绑定失败诊断流程图

    在这里插入图片描述

    绑定失败诊断流程图 —— 从「驱动 probe 未触发」出发,按设备树节点存在性、compatible 匹配、驱动注册状态、总线匹配时机四个维度逐层排查;蓝色为检查项,红色为修复动作,绿色为继续排查;最终若排除所有常见原因,则需借助 ftrace / printk 深入内核调试。

    8.6.2 常见绑定失败场景与修复
    失败场景症状根因修复方案
    compatible 拼写不一致 of_match_device() 返回 NULL 设备树 "myvendor,myuart" vs 驱动 "myvendor,myuar" 使用 cat /proc/device-tree/…/compatible 与驱动源码逐字符比对
    缺少终止符 内核崩溃/越界访问 of_match_table 数组末尾缺少 { } 确保数组以空结构体结束
    of_match_table 未绑定到 driver 驱动加载但 probe 不触发 platform_driver.driver.of_match_table 未赋值 检查 platform_driver 结构体初始化
    CONFIG_OF 未开启 整个设备树系统不工作 内核编译时未启用 Open Firmware 支持 grep CONFIG_OF /boot/config-$(uname -r)
    设备早于驱动注册 设备存在但无驱动绑定 驱动作为模块加载,设备在 initcall 早期注册 使用 deferred_probe_timeout 或内置驱动
    ACPI 优先覆盖 DT ARM64 服务器上设备树不生效 固件同时提供 ACPI 和 DTB,内核优先 ACPI 添加 acpi=off 或修改固件策略
    Overlay 未正确应用 动态添加的设备无驱动 DTBO 编译错误 / target 路径错误 / configfs 未挂载 dtc -@ 编译,检查 /config/device-tree/overlays/xxx/status
    引用了未定义的 phandle DTC 编译报错 &label 指向的节点不存在或标签拼写错误 检查 DTSI 包含顺序和标签定义
    8.6.3 内核级调试:跟踪绑定过程

    // 在驱动中添加调试信息,追踪绑定过程
    static int myuart_probe(struct platform_device *pdev)
    {
    const struct of_device_id *match;

    // 1. 确认 of_node 存在
    if (!pdev->dev.of_node) {
    dev_err(&pdev->dev, "No of_node bound!\\n");
    return –ENODEV;
    }

    // 2. 打印完整设备树路径
    dev_info(&pdev->dev, "Device tree path: %pOF\\n", pdev->dev.of_node);

    // 3. 打印 compatible 属性
    dev_info(&pdev->dev, "Compatible: %pOFn\\n", pdev->dev.of_node);

    // 4. 确认匹配结果
    match = of_match_device(myuart_of_match, &pdev->dev);
    if (!match) {
    dev_err(&pdev->dev, "No matching of_device_id found!\\n");
    return –EINVAL;
    }

    dev_info(&pdev->dev, "Matched: %s, data=%p\\n",
    match->compatible, match->data);

    return 0;
    }

    使用 ftrace 跟踪 platform_match:

    # 启用函数跟踪
    echo function > /sys/kernel/debug/tracing/current_tracer

    # 设置跟踪过滤器
    echo platform_match > /sys/kernel/debug/tracing/set_ftrace_filter
    echo of_driver_match_device >> /sys/kernel/debug/tracing/set_ftrace_filter
    echo of_match_device >> /sys/kernel/debug/tracing/set_ftrace_filter
    echo of_device_is_compatible >> /sys/kernel/debug/tracing/set_ftrace_filter

    # 开始跟踪
    echo 1 > /sys/kernel/debug/tracing/tracing_on

    # 触发设备加载(如加载模块或应用 Overlay)
    modprobe myuart

    # 查看跟踪结果
    cat /sys/kernel/debug/tracing/trace

    8.7 绑定机制的扩展:非 Platform 总线

    虽然设备树绑定最常与 platform_bus_type 关联,但绑定机制同样适用于其他总线:

    总线类型匹配函数设备树节点示例驱动绑定方式
    Platform platform_match() serial@1000 { compatible = "…"; } platform_driver.of_match_table
    I2C i2c_device_match() sensor@48 { compatible = "…"; reg = <0x48>; } i2c_driver.driver.of_match_table
    SPI spi_match_device() flash@0 { compatible = "…"; reg = <0>; } spi_driver.driver.of_match_table
    USB usb_match_device() 通常不通过设备树描述 通过 VID/PID 匹配
    PCI pci_match_device() 通过 interrupt-map 关联 通过 VendorID/DeviceID 匹配
    AMBA amba_match() uart@1000 { compatible = "arm,pl011"; } amba_driver.drv.of_match_table

    I2C 绑定的特殊之处:

    &i2c1 {
    status = "okay";

    // I2C 子设备的 reg = I2C 从设备地址
    temperature-sensor@48 {
    compatible = "bosch,bme280";
    reg = <0x48>; // I2C 地址,不是内存地址
    status = "okay";
    };
    };

    // I2C 驱动绑定
    static const struct of_device_id bme280_of_match[] = {
    { .compatible = "bosch,bme280" },
    { .compatible = "bosch,bmp280" },
    { }
    };
    MODULE_DEVICE_TABLE(of, bme280_of_match);

    static struct i2c_driver bme280_driver = {
    .driver = {
    .name = "bme280",
    .of_match_table = bme280_of_match,
    },
    .probe = bme280_probe,
    .id_table = bme280_id_table, // I2C ID 表(非设备树场景)
    };

    关键差异:I2C/SPI 子设备不是由 of_platform_populate() 直接创建的,而是由控制器驱动在 probe() 中调用 of_register_i2c_devices() / spi_of_register_devices() 二次扫描创建的。这意味着:

    • I2C 控制器必须 status = "okay" 且驱动成功 probe
    • 只有控制器驱动 probe 成功后,才会扫描其子节点并创建 I2C 子设备
    • 若控制器驱动加载失败或被 deferred,所有子设备都不会出现

    9. 驱动开发中的 OF API + 系统化速查表 + 诊断手册

    9.1 OF API 分类体系

    9.1.1 节点查找类

    // 通过路径查找(绝对路径)
    struct device_node *of_find_node_by_path(const char *path); // "/soc/serial@10000000"

    // 通过名称查找(从指定节点开始遍历)
    struct device_node *of_find_node_by_name(struct device_node *from, const char *name);

    // 通过 compatible 查找(最常用)
    struct device_node *of_find_compatible_node(struct device_node *from,
    const char *type,
    const char *compat);

    // 通过别名查找(解析 aliases 节点)
    struct device_node *of_find_node_by_alias(struct device_node *np, const char *alias);

    // 通过 phandle 查找(解析 phandle 值)
    struct device_node *of_find_node_by_phandle(phandle handle);

    // 遍历子节点
    struct device_node *of_get_next_child(struct device_node *node, struct device_node *prev);

    // 引用计数管理(必须成对使用)
    struct device_node *of_node_get(struct device_node *node);
    void of_node_put(struct device_node *node);

    9.1.2 属性读取类

    // 读取单个数值(u8/u16/u32/u64)
    int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
    int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);

    // 读取数组
    int of_property_read_u32_array(const struct device_node *np, const char *propname,
    u32 *out_values, size_t sz);

    // 读取字符串
    int of_property_read_string(const struct device_node *np, const char *propname,
    const char **out_string);

    // 读取字符串数组
    int of_property_read_string_array(const struct device_node *np, const char *propname,
    const char **out_strings, size_t sz);

    // 判断属性是否存在(布尔值)
    bool of_property_read_bool(const struct device_node *np, const char *propname);

    // 获取属性长度(字节数)
    int of_property_count_elems_of_size(const struct device_node *np, const char *propname,
    size_t elem_size);

    // 获取原始属性结构(二进制数据)
    struct property *of_find_property(const struct device_node *np, const char *name,
    int *lenp);

    9.1.3 资源转换类

    // 将 reg 属性转换为 struct resource(包含地址、长度、类型)
    int of_address_to_resource(struct device_node *dev, int index, struct resource *r);

    // 获取地址数量
    int of_property_count_elems_of_size(const struct device_node *np, const char *propname,
    size_t elem_size);

    // 解析并映射中断(底层 API)
    unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

    // 获取中断数量
    int of_irq_count(struct device_node *dev);

    // 获取命名 GPIO
    int of_get_named_gpio(struct device_node *np, const char *propname, int index);

    // 获取命名 GPIO 并转换为 irq
    int of_get_named_gpio_flags(struct device_node *np, const char *list_name,
    int index, enum of_gpio_flags *flags);

    9.1.4 内存映射类

    // 映射 reg 指定的 IO 内存(非 devm,需手动 iounmap)
    void __iomem *of_iomap(struct device_node *np, int index);

    // devm 版本(推荐,自动释放)
    void __iomem *devm_of_iomap(struct device *dev, struct device_node *np, int index,
    resource_size_t *size);

    // 平台设备专用(最推荐)
    void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev, int index);

    9.1.5 中断解析类

    // 解析中断并映射到 Linux IRQ 编号
    unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

    // 获取中断数量
    int of_irq_count(struct device_node *dev);

    // 获取中断资源(填充 struct resource)
    int of_irq_to_resource(struct device_node *dev, int index, struct resource *r);

    // 平台设备专用(推荐)
    int platform_get_irq(struct platform_device *dev, int num);

    9.1.6 时钟/DMA/GPIO 专用类

    // 时钟
    struct clk *devm_clk_get(struct device *dev, const char *id); // id 匹配 clock-names
    struct clk *of_clk_get(struct device_node *np, int index);
    struct clk *of_clk_get_by_name(struct device_node *np, const char *name);

    // DMA
    struct dma_chan *dma_request_slave_channel(struct device *dev, const char *name);
    struct dma_chan *of_dma_request_slave_channel(struct device_node *np, const char *name);

    // GPIO
    struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);
    struct gpio_desc *devm_gpiod_get_index(struct device *dev, const char *con_id, int idx,
    enum gpiod_flags flags);

    9.2 常见错误诊断手册

    9.2.1 诊断表:症状 → 根因 → 修复 → 诊断命令
    症状根因修复方案诊断命令
    驱动 probe 未触发 ① status = "disabled"② compatible 不匹配③ 设备未注册到 platform_bus ① 改为 "okay"② 核对驱动 of_match_table③ 检查 of_platform_populate() 是否覆盖该节点 grep compatible /sys/bus/platform/devices/*/of_node/compatible 2>/dev/nullcat /sys/firmware/devicetree/base/…/status
    platform_get_irq() 返回 -ENXIO (-6) ① interrupts 属性缺失② interrupt-parent 错误或未继承③ #interrupt-cells 不匹配 ① 补充 interrupts② 显式指定 interrupt-parent = <&intc>③ 核对中断控制器 #interrupt-cells hexdump -C /sys/firmware/devicetree/base/…/interruptsdtc -I dtb -O dts /sys/firmware/fdt | grep -A5 "uart@"
    devm_clk_get() 返回 EPROBE_DEFER (-517) 时钟提供者(clock provider)驱动未加载或 status = "disabled" ① 确保时钟控制器节点 status = "okay"② 检查时钟控制器驱动是否编译进内核③ 调整驱动加载顺序(module_init 优先级或 arch_initcall) ls /sys/kernel/debug/clk/grep clk /sys/kernel/debug/devices_deferred
    devm_pinctrl_get() 失败 ① pinctrl-0 引用的节点不存在② pinctrl 驱动未加载③ 引脚冲突(两个外设同时使用同一引脚) ① 检查标签 &uart0_pins 是否正确定义② 确保 pinctrl 驱动已加载③ 检查是否有其他节点占用相同引脚 cat /sys/kernel/debug/pinctrl/pinctrl-devicescat /sys/kernel/debug/pinctrl/pinmux-pins
    内存识别少了一半 #address-cells / #size-cells 与 reg 长度不匹配 核对父节点的 cells 定义与 reg 数组元素个数是否一致 dtc -I dtb -O dts /sys/firmware/fdt | grep -A2 "#address-cells"hexdump -C /sys/firmware/devicetree/base/memory@…/reg
    GPIO 编号错乱 / 无法控制 ① #gpio-cells 不匹配(如应为 2 但写成了 3)② GPIO_ACTIVE_LOW / GPIO_ACTIVE_HIGH 混淆 ① 核对 GPIO 控制器 #gpio-cells② 检查硬件原理图电平逻辑 cat /sys/kernel/debug/gpiogpiod_get_value() 返回值与万用表实测对比
    DMA 传输失败 / 通道申请失败 ① dmas 中的请求号错误② #dma-cells 不匹配③ DMA 控制器驱动未加载 ① 核对 SoC TRM 中的 DMA 请求号② 检查 #dma-cells 与 dmas 数组长度③ 确保 DMA 控制器 status = "okay" ls /sys/class/dma/dmesg | grep -i dma
    of_property_read_u32() 返回 -EINVAL ① 属性名拼写错误② 属性类型不匹配(字符串 vs 数值)③ 属性不存在 ① 核对属性名大小写② 用 hexdump 查看属性原始类型③ 检查 DTS 是否包含该属性 hexdump -C /sys/firmware/devicetree/base/…/属性名dtc -I dtb -O dts … | grep 属性名
    Overlay 加载失败 ① dtc 未加 -@ 参数(符号丢失)② target 节点路径错误③ 内核未开启 CONFIG_OF_OVERLAY ① dtc -@ -I dts -O dtbo …② 核对 target = <&label> 或路径③ 开启内核配置并确保 configfs 已挂载 cat /config/device-tree/overlays/xxx/statusdmesg | grep -i overlay
    设备树编译通过但内核启动崩溃 ① reg 地址越界(访问未映射内存)② interrupts 编号冲突③ memory 节点与实际硬件不符 ① 核对 TRM 寄存器地址范围② 检查中断号是否与其他设备冲突③ 用 Bootloader 命令 md 验证内存 md.l 0x10000000 0x10(U-Boot)dmesg | grep -i "Unable to handle"

    9.3 调试命令速查

    # ========== 查看当前运行设备树 ==========
    # 查看节点属性(二进制格式)
    hexdump -C /sys/firmware/devicetree/base/soc/serial@10000000/reg

    # 查看字符串属性
    cat /sys/firmware/devicetree/base/compatible

    # 查看节点状态
    cat /sys/firmware/devicetree/base/soc/serial@10000000/status

    # ========== 反编译当前 DTB ==========
    cp /sys/firmware/fdt /tmp/current.dtb
    dtc -I dtb -O dts -o current.dts /tmp/current.dtb

    # ========== 查看设备模型注册状态 ==========
    # 查看所有 platform 设备及其 of_node
    ls /sys/bus/platform/devices/

    # 查看设备是否已绑定驱动
    ls /sys/bus/platform/drivers/myuart/

    # 查看 deferred probe 列表(驱动因依赖未满足而延迟加载)
    cat /sys/kernel/debug/devices_deferred

    # ========== 查看子系统状态 ==========
    # 时钟树
    cat /sys/kernel/debug/clk/clk_summary

    # GPIO 状态
    cat /sys/kernel/debug/gpio

    # Pinctrl 引脚占用
    cat /sys/kernel/debug/pinctrl/pinmux-pins

    # DMA 通道
    cat /sys/class/dma/dma0chan0/state

    # ========== 验证设备树语法 ==========
    # 检查 dts 语法
    dtc -I dts -O dtb -o test.dtb myboard.dts

    # 检查 Schema(需内核源码环境)
    ./scripts/dtc/dt-validate arch/arm64/boot/dts/myboard.dts


    10. 子系统级节点详解(Pinctrl / Clock / GPIO / DMA)

    定位:现代 SoC BSP 开发中,驱动工程师每日必打交道的四大子系统。脱离这些子系统,设备树描述的外设只是「骨架」,无法真正工作。

    10.1 Pinctrl / Pinmux 子系统 —— 引脚复用

    10.1.1 核心概念

    SoC 的物理引脚数量有限,但外设数量远超引脚数。Pinctrl 子系统通过设备树描述引脚复用关系,在驱动 probe 时自动完成引脚配置。

    // SoC 级定义(.dtsi):引脚组定义
    &pio {
    uart0_pins: uart0-pins {
    pins = "PA0", "PA1"; // 具体引脚
    function = "uart0"; // 复用功能
    bias-pull-up; // 内部上拉
    drive-strength = <8>; // 驱动能力(mA)
    };

    i2c0_pins: i2c0-pins {
    pins = "PA2", "PA3";
    function = "i2c0";
    bias-pull-up;
    input-schmitt-enable; // 施密特触发
    };
    };

    // 板级定义(.dts):外设引用引脚配置
    &uart0 {
    pinctrl-names = "default", "sleep"; // 状态名称
    pinctrl-0 = <&uart0_pins>; // default 状态引脚
    pinctrl-1 = <&uart0_sleep_pins>; // sleep 状态引脚
    status = "okay";
    };

    10.1.2 关键属性速查
    属性作用域说明
    pinctrl-names 外设节点 定义引脚状态名称列表(如 default, sleep, idle)
    pinctrl-0/1/2… 外设节点 对应 pinctrl-names 索引的引脚组引用
    pins pinctrl 组节点 引脚列表,支持多个 "PA0", "PA1"
    function pinctrl 组节点 复用功能名称,由 SoC pinctrl 驱动定义
    bias-disable pinctrl 组节点 禁止上下拉
    bias-pull-up pinctrl 组节点 使能内部上拉
    bias-pull-down pinctrl 组节点 使能内部下拉
    drive-strength pinctrl 组节点 驱动电流强度(单位 mA,典型值 2/4/8/12)
    input-schmitt-enable pinctrl 组节点 使能施密特触发(抗抖动)
    output-high / output-low pinctrl 组节点 配置为 GPIO 输出并设初始电平
    10.1.3 驱动侧自动绑定

    // 驱动无需手动解析 pinctrl,platform 框架自动处理
    static int myuart_probe(struct platform_device *pdev)
    {
    // 内核自动查找 pinctrl-0 并应用引脚配置
    // 若 pinctrl-names 包含多个状态,驱动可手动切换:
    struct pinctrl *p = devm_pinctrl_get(&pdev->dev);
    struct pinctrl_state *state = pinctrl_lookup_state(p, "sleep");
    pinctrl_select_state(p, state); // 切换到低功耗引脚状态
    }

    10.2 Clock 子系统 —— 时钟树

    10.2.1 核心概念

    现代 SoC 拥有数百个时钟源,形成复杂的时钟树(PLL → 分频器 → 门控 → 外设)。设备树通过 clocks 属性描述外设的时钟依赖关系。

    // 时钟提供者(Clock Provider)定义
    osc24m: osc24m-clk {
    compatible = "fixed-clock";
    #clock-cells = <0>; // 无额外时钟选择参数
    clock-frequency = <24000000>; // 24MHz 晶振
    clock-output-names = "osc24m";
    };

    pll_periph: pll-periph {
    compatible = "allwinner,sun4i-pll";
    #clock-cells = <0>;
    clocks = <&osc24m>; // 输入时钟源
    };

    apb0: apb0-clk {
    compatible = "allwinner,sun4i-apb0-clk";
    #clock-cells = <0>;
    clocks = <&pll_periph>;
    };

    // 外设引用时钟
    &uart0 {
    clocks = <&apb0>; // 引用 apb0 时钟
    clock-names = "apb"; // 驱动通过名称识别时钟
    status = "okay";
    };

    // 多时钟外设(如 SDHCI 需要总线时钟 + 模块时钟)
    &mmc0 {
    clocks = <&ahb_mmc0>, <&mmc0_clk>;
    clock-names = "ahb", "mmc"; // 驱动通过 clock-names 区分
    status = "okay";
    };

    10.2.2 关键属性速查
    属性作用域说明
    #clock-cells 时钟提供者 0 = 单输出时钟;1 = 需额外索引(如多路时钟选择器)
    clock-output-names 时钟提供者 时钟输出名称(调试/可读性)
    clock-frequency 固定时钟 固定频率值(Hz)
    clocks 外设节点 引用的时钟提供者 phandle 列表
    clock-names 外设节点 与 clocks 一一对应的名称,驱动通过 devm_clk_get(dev, "apb") 获取
    assigned-clocks 节点 指定需要配置的时钟
    assigned-clock-rates 节点 指定时钟目标频率
    assigned-clock-parents 节点 指定时钟父源
    10.2.3 驱动侧时钟 API

    struct clk *clk = devm_clk_get(&pdev->dev, "apb"); // 通过 clock-names 匹配
    clk_prepare_enable(clk); // 使能时钟
    u32 rate = clk_get_rate(clk); // 获取当前频率
    clk_set_rate(clk, 48000000); // 设置目标频率(若支持)
    clk_disable_unprepare(clk); // 关闭时钟(remove 时)

    10.3 GPIO 子系统 —— 通用输入输出

    10.3.1 核心概念

    GPIO 子系统与 Pinctrl 紧密耦合:Pinctrl 负责引脚复用配置,GPIO 子系统负责数字电平读写。

    // GPIO 控制器定义(通常在 SoC pinctrl 节点内)
    &pio {
    compatible = "allwinner,sun8i-h3-pinctrl";
    gpio-controller; // 标记为 GPIO 控制器
    #gpio-cells = <6>; // GPIO 描述占 6 个参数(bank, pin, flags…)

    // GPIO 引脚 hog(内核启动时自动占用)
    power_key: power-key {
    gpio-hog; // 标记为 hog
    gpios = <PA 0 GPIO_ACTIVE_LOW>; // 占用 PA0
    input; // 配置为输入
    label = "power-key";
    };
    };

    // 外设引用 GPIO
    &leds {
    status = "okay";

    led0: led@0 {
    label = "status-led";
    gpios = <&pio PA 15 GPIO_ACTIVE_LOW>; // &pio 控制器, bank=PA, pin=15, 低电平有效
    default-state = "off";
    };
    };

    // 中断方式引用 GPIO
    gpio-keys {
    compatible = "gpio-keys";

    button@0 {
    label = "user-button";
    gpios = <&pio PA 0 GPIO_ACTIVE_LOW>;
    linux,code = <KEY_POWER>;
    interrupt-parent = <&pio>; // GPIO 控制器也可作为中断源
    interrupts = <PA 0 IRQ_TYPE_EDGE_FALLING>;
    };
    };

    10.3.2 关键属性速查
    属性作用域说明
    gpio-controller GPIO 控制器节点 标记此节点为 GPIO 控制器
    #gpio-cells GPIO 控制器 描述引用一个 GPIO 所需的参数个数(通常 2 或 3)
    gpios 外设节点 GPIO 引用列表,格式由 #gpio-cells 决定
    gpio-hog 引脚节点 内核启动时自动占用此 GPIO,无需驱动介入
    input / output-high / output-low hog 节点 配置 GPIO 方向与初始电平
    GPIO_ACTIVE_LOW 标志位 低电平有效(与 GPIO_ACTIVE_HIGH 相对)
    10.3.3 驱动侧 GPIO API

    // 获取 GPIO
    struct gpio_desc *gpiod = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_LOW);
    // 参数说明:dev, con_id(匹配 gpios 属性中的 "reset" 后缀), 初始方向

    // 读写电平
    gpiod_set_value(gpiod, 1); // 输出高
    int val = gpiod_get_value(gpiod); // 读取输入电平

    // 获取 GPIO 中断
    int irq = gpiod_to_irq(gpiod);
    request_irq(irq, my_handler, IRQF_TRIGGER_FALLING, "myirq", dev);

    10.4 DMA 子系统 —— 直接内存访问

    10.4.1 核心概念

    高速外设(音频、SDIO、网络、SPI)需要 DMA 传输以避免 CPU 轮询。设备树通过 dmas 属性描述 DMA 通道请求。

    // DMA 控制器定义
    dma: dma-controller@1c02000 {
    compatible = "allwinner,sun8i-h3-dma";
    reg = <0x01c02000 0x1000>;
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
    #dma-cells = <1>; // DMA 请求描述占 1 个参数(通道号/请求号)
    dma-channels = <8>; // 控制器通道数
    dma-requests = <32>; // 外设请求线数量
    };

    // 外设引用 DMA
    &mmc0 {
    dmas = <&dma 23>, <&dma 24>; // 23=RX 通道请求号, 24=TX 通道请求号
    dma-names = "rx", "tx"; // 驱动通过名称区分收发
    status = "okay";
    };

    // 音频设备 DMA 配置
    &i2s0 {
    dmas = <&dma 0>, <&dma 1>;
    dma-names = "rx", "tx";
    // 某些 SoC 需要 dma-ranges 描述 DMA 地址空间限制
    dma-ranges = <0x0 0x40000000 0x20000000>; // 从设备视角到 CPU 视角的地址映射
    };

    10.4.2 关键属性速查
    属性作用域说明
    #dma-cells DMA 控制器 描述一个 DMA 请求所需的参数个数(通常 1)
    dma-channels DMA 控制器 物理通道数量
    dma-requests DMA 控制器 外设请求线数量(决定同时服务多少外设)
    dmas 外设节点 DMA 请求引用列表,格式由 #dma-cells 决定
    dma-names 外设节点 与 dmas 一一对应的名称(如 "rx", "tx")
    dma-ranges 总线/外设 DMA 地址空间转换(类似 ranges,用于非一致性 DMA)
    10.4.3 驱动侧 DMA API

    // 获取 DMA 通道
    struct dma_chan *rx_chan = dma_request_slave_channel(&pdev->dev, "rx");
    struct dma_chan *tx_chan = dma_request_slave_channel(&pdev->dev, "tx");

    // 配置 DMA 传输参数
    struct dma_slave_config cfg = {
    .direction = DMA_MEM_TO_DEV,
    .dst_addr = res->start + FIFO_OFFSET,
    .dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
    .dst_maxburst = 8,
    };
    dmaengine_slave_config(tx_chan, &cfg);

    // 提交 DMA 传输
    struct dma_async_tx_descriptor *desc = dmaengine_prep_slave_single(
    tx_chan, buf, len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
    desc->callback = my_dma_callback;
    dmaengine_submit(desc);
    dma_async_issue_pending(tx_chan);

    10.5 子系统节点关系总图

    在这里插入图片描述

    子系统节点关系总图 —— 上半部分展示外设节点(如 &uart0)通过各类属性引用 Pinctrl、Clock、Interrupt、DMA、GPIO、Regulator 等子系统;下半部分展示驱动 probe() 阶段内核调用对应 API 解析这些属性,完成资源绑定与初始化。其中 gpios、vcc-supply 为可选属性。


    11. YAML Schema 验证与 dt-bindings(现代 BSP 上游化标准)

    定位:Linux 5.x+ 内核要求新提交的设备树绑定必须以 YAML Schema 形式编写。这是从「设备树能编译」走向「设备树正确」的关键门槛,也是向上游社区提交 BSP 的必会技能。

    11.1 为什么需要 Schema 验证

    传统设备树开发的问题:

    • 语法正确 ≠ 语义正确:dtc 只能检查 DTS 语法,无法判断 compatible 是否合法、reg 长度是否匹配 SoC 要求
    • 绑定文档分散:早期 .txt 绑定文档散落在 Documentation/devicetree/bindings/,无机器可读性
    • 维护困难:SoC 厂商的私有 DTS 往往包含大量「能工作但不符合规范」的写法

    YAML Schema 的解决:

    • 机器可读:dt-validate 工具可自动检查设备树是否符合 Schema
    • 自文档化:Schema 本身就是最权威的绑定文档
    • 继承复用:通过 $ref 引用标准绑定(如 gpio.yaml, clock.yaml),避免重复定义

    11.2 YAML Schema 文件结构

    # Documentation/devicetree/bindings/serial/myvendor,myuart.yaml
    $schema: http://devicetree.org/meta–schemas/core–schema.yaml#

    # 标题与维护者信息
    title: MyVendor UART Controller

    maintainers:
    – Your Name <your.email@example.com>

    # 描述
    description: |
    MyVendor UART controller is a standard 16550-compatible UART
    with additional DMA support and configurable FIFO depth.

    # 属性定义(所有节点必须包含的属性及其约束)
    properties:
    compatible:
    const: myvendor,myuart # 强制固定值,不可变
    # 或 enum: [myvendor,myuart-v1, myvendor,myuart-v2]

    reg:
    maxItems: 1 # 只能有 1 个寄存器区域

    interrupts:
    maxItems: 1

    clocks:
    maxItems: 1

    clock-names:
    const: apb # 时钟名称固定为 "apb"

    dmas:
    minItems: 1
    maxItems: 2 # 支持 1~2 个 DMA 通道

    dma-names:
    minItems: 1
    maxItems: 2
    items:
    – enum: [rx, tx] # 元素只能是 "rx" 或 "tx"

    pinctrl-0: true # 允许存在,不限制具体内容
    pinctrl-names: true

    # 自定义属性
    myvendor,fifo-depth:
    $ref: /schemas/types.yaml#/definitions/uint32
    description: Configurable FIFO depth in bytes
    enum: [16, 32, 64, 128] # 只允许这四个值

    # 标准属性继承
    status:
    enum: [okay, disabled] # 限制 status 的合法值

    # 必选属性列表(缺少任一属性,验证报错)
    required:
    – compatible
    – reg
    – interrupts
    – clocks
    – clock–names

    # 条件属性(当存在 dmas 时,dma-names 必须存在)
    allOf:
    – if:
    properties:
    dmas: true
    then:
    required:
    – dma–names

    # 额外属性策略:不允许未定义的属性(严格模式)
    additionalProperties: false

    # 示例(作为文档和测试用例)
    examples:
    – |
    uart@10000000 {
    compatible = "myvendor,myuart";
    reg = <0x10000000 0x100>;
    interrupts = <GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&apb0>;
    clock-names = "apb";
    status = "okay";
    };

    11.3 标准绑定继承($ref)

    properties:
    # 继承标准 GPIO 引用格式
    reset-gpios:
    $ref: /schemas/types.yaml#/definitions/phandle-array
    items:
    – $ref: /schemas/gpio.yaml#/definitions/gpio-consumer
    description: GPIO connected to the reset pin

    # 继承标准时钟引用格式
    clocks:
    $ref: /schemas/types.yaml#/definitions/phandle-array
    items:
    – $ref: /schemas/clock.yaml#/definitions/clock
    minItems: 1
    maxItems: 2

    # 继承标准中断引用格式
    interrupts:
    $ref: /schemas/interrupts.yaml#/definitions/interrupts
    maxItems: 1

    11.4 常用约束类型速查

    约束关键字作用示例
    const 强制固定值 const: myvendor,myuart
    enum 枚举合法值 enum: [16, 32, 64]
    maxItems 数组最大元素数 maxItems: 2
    minItems 数组最小元素数 minItems: 1
    items 定义数组每个元素的类型 items: [{enum: [rx, tx]}]
    pattern 正则匹配字符串 pattern: "^led-[0-9]+$"
    minimum / maximum 数值范围 minimum: 0, maximum: 255
    oneOf 多选一 属性 A 或属性 B 必须存在一个
    allOf 条件组合 如果存在 X,则必须存在 Y

    11.5 验证工具链使用

    # 1. 安装验证工具(内核源码内)
    cd linux/scripts/dtc
    make check # 编译 dtc 并运行自测

    # 2. 验证单个 YAML Schema 语法
    python3 -m dtschema_validate Documentation/devicetree/bindings/serial/myvendor,myuart.yaml

    # 3. 验证 DTS 是否符合所有 Schema(最常用)
    ./scripts/dtc/dt-validate arch/arm64/boot/dts/myvendor/myboard.dtb
    # 或验证源码级 DTS
    ./scripts/dtc/dt-validate arch/arm64/boot/dts/myvendor/myboard.dts

    # 4. 检查具体错误
    # 输出示例:
    # myboard.dts:45: myuart@10000000: 'myvendor,unsupported-prop' is not allowed
    # from schema $id: http://devicetree.org/schemas/serial/myvendor,myuart.yaml#

    11.6 上游提交流程中的 Binding 要求

    提交设备树驱动补丁到 Linux 上游时,必须同时提交:

    1. 驱动代码(drivers/xxx/)
    2. YAML Binding(Documentation/devicetree/bindings/xxx/)
    3. 设备树节点示例(放在 YAML 的 examples 字段)
    4. 实际 SoC 的 DTS/DTSI 变更(arch/arm64/boot/dts/)

    审查重点:
    ├── Schema 是否覆盖了所有驱动解析的属性?
    ├── 是否复用了标准 $ref(gpio.yaml, clock.yaml, interrupt.yaml)?
    ├── examples 字段是否可编译通过 dt-validate?
    └── 是否遗漏了 required 属性或条件约束?


    12. 工程语法高级特性(DTS 进阶)

    12.1 /delete-node/ 与 /delete-property/

    板级 DTS 继承 SoC DTSI 后,常需删除不需要的节点或属性:

    #include "soc.dtsi"

    / {
    // 删除不需要的 GPU 节点
    /delete-node/ gpu@30000000;

    // 删除继承的默认时钟配置
    /delete-property/ assigned-clocks;
    };

    &uart0 {
    // 删除默认的 pinctrl 引用
    /delete-property/ pinctrl-0;
    };

    12.2 /incbin/ —— 二进制数据内联

    用于将固件 blob 直接嵌入 DTB:

    / {
    firmware {
    wifi_fw: wifi-firmware {
    compatible = "myvendor,wifi-fw";
    // 将二进制文件内容作为属性值嵌入
    data = /incbin/("./wifi_firmware.bin");
    };
    };
    };

    12.3 /memreserve/ —— 早期内存预留

    在 FDT 文件头中预留内存,比 reserved-memory 节点更早生效(Bootloader 阶段即生效):

    /memreserve/ 0x80000000 0x00100000; // 预留 1MB @ 0x80000000

    / {
    // …
    };

    与 reserved-memory 的区别:

    • /memreserve/:FDT 头信息,Bootloader 解析 FDT 时即处理,不进入内核设备树
    • reserved-memory:设备树节点,内核 unflatten 后解析,可动态调整

    13. Zephyr 与 Linux 设备树对比

    维度Linux 设备树Zephyr 设备树
    设计目标 描述硬件,供内核解析 描述硬件 + 配置软件,编译时生成代码
    解析时机 内核启动时(运行时) 编译时(生成 C 头文件和宏)
    驱动绑定 运行时通过 compatible 匹配 编译时通过 DEVICE_DT_DEFINE 静态绑定
    内存占用 需保留完整 device_node 树 不保留设备树结构,仅保留提取的配置
    Overlay 支持 运行时动态加载(configfs) 编译时静态合并
    适用场景 通用操作系统(Linux) RTOS(Zephyr)、资源极度受限设备
    规范来源 Devicetree Specification(power.org) 继承并扩展 Linux 规范,增加 binding 约束

    关键差异理解:

    • Linux 设备树是运行时数据库,内核启动后仍可通过 /sys/firmware/devicetree/base/ 访问完整节点
    • Zephyr 设备树是编译时代码生成器,仅用于生成 devicetree_generated.h,不保留运行时树结构

    14. 与其他固件接口对比(ACPI / RISC-V / FDT 启动协议)

    定位:设备树并非唯一的硬件描述接口。理解 ACPI、RISC-V 设备树差异、以及不同架构的 FDT 启动协议,是进行跨架构 BSP 开发和服务器级系统调试的必备知识。

    14.1 ACPI vs 设备树:ARM64 服务器的双支持策略

    14.1.1 为什么 ARM64 服务器同时支持两者?
    维度ACPI设备树(DT)
    设计目标 通用 PC/服务器硬件抽象 嵌入式/SoC 硬件精确描述
    标准化组织 UEFI Forum / ACPI Spec Devicetree.org / Power.org
    运行时能力 支持热插拔、电源状态迁移、方法执行(AML) 静态描述,启动后只读
    固件接口 UEFI 系统表 → ACPI 表 Bootloader 传递 DTB 地址
    驱动模型 ACPI 驱动绑定(PNP0D80 等 HID) OF 驱动绑定(compatible)
    适用场景 服务器、笔记本、通用 ARM64 设备 嵌入式、IoT、定制 SoC

    ARM64 的折中策略:

    • 服务器级 SoC(如 Ampere Altra、AWS Graviton):优先使用 ACPI,因为需要 NUMA、热插拔、PCIe 热移除等高级特性
    • 嵌入式/IoT 设备:优先使用 设备树,因为硬件高度定制化,ACPI 的通用抽象反而成为负担
    • 内核配置:CONFIG_ACPI 与 CONFIG_OF 可同时开启,启动时由固件决定使用哪种描述方式
    14.1.2 启动时选择逻辑

    // arch/arm64/kernel/setup.c
    void __init setup_arch(char **cmdline_p)
    {
    // 1. 优先检查 ACPI
    if (acpi_disabled == 0 && acpi_table_parse_sdt(...)) {
    // 使用 ACPI 描述
    acpi_boot_table_init();
    } else {
    // 2. 回退到设备树
    early_init_dt_scan(phys_to_virt(dtb_phys));
    }
    }

    UEFI 固件的行为:

    • 若 UEFI 系统表中存在有效的 ACPI 2.0+ 表,内核优先走 ACPI 路径
    • 若 ACPI 无效或禁用(acpi=off 命令行),内核回退到 FDT
    • 某些固件(如 EDK2)同时提供 ACPI 表和 DTB,由内核策略决定
    14.1.3 驱动层面的差异

    // ACPI 驱动绑定
    static const struct acpi_device_id myuart_acpi_ids[] = {
    { "PNP0501", 0 }, // ACPI HID(硬件标识)
    { }
    };
    MODULE_DEVICE_TABLE(acpi, myuart_acpi_ids);

    // 设备树驱动绑定
    static const struct of_device_id myuart_of_ids[] = {
    { .compatible = "myvendor,myuart" },
    { }
    };
    MODULE_DEVICE_TABLE(of, myuart_of_ids);

    // 统一驱动:同时支持两种固件接口
    static struct platform_driver myuart_driver = {
    .driver = {
    .name = "myuart",
    .of_match_table = myuart_of_ids,
    .acpi_match_table = ACPI_PTR(myuart_acpi_ids),
    },
    .probe = myuart_probe,
    };

    14.2 RISC-V 设备树的特殊之处

    RISC-V 架构的设备树继承自 Linux/ARM 规范,但在CPU 描述和定时器方面存在显著差异。

    14.2.1 hart 节点:硬件线程描述

    // RISC-V 的 CPU 拓扑使用 hart(硬件线程)概念
    cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    // hart 0:主核心(Boot HART)
    cpu@0 {
    compatible = "riscv";
    reg = <0>; // hart ID = 0
    device_type = "cpu";

    // RISC-V 特有属性
    riscv,isa = "rv64imafdc"; // 支持的指令集扩展
    mmu-type = "riscv,sv48"; // MMU 类型(Sv39/Sv48/Sv57)

    // CPU 能力(类似 ARM 的 enable-method)
    cpu-idle-states = <&CPU_SLEEP_0>;

    // 中断控制器引用(RISC-V 使用 PLIC/APLIC)
    interrupt-controller = <&plic>;
    };

    cpu@1 {
    compatible = "riscv";
    reg = <1>;
    device_type = "cpu";
    riscv,isa = "rv64imafdc";
    mmu-type = "riscv,sv48";
    status = "disabled"; // 可通过 firmware 动态使能
    };
    };

    // 中断控制器(PLIC:Platform-Level Interrupt Controller)
    plic: interrupt-controller@c000000 {
    compatible = "riscv,plic0";
    #address-cells = <0>;
    #interrupt-cells = <1>;
    interrupt-controller;
    reg = <0x0 0xc000000 0x0 0x4000000>;
    riscv,ndev = <95>; // 外部中断源数量(RISC-V 特有)
    };

    14.2.2 timebase-frequency:RISC-V 定时器基准

    // RISC-V 使用统一的机器模式定时器(mtime),由设备树描述频率
    cpus {
    timebase-frequency = <10000000>; // 10 MHz,mtime 计数器递增频率
    };

    // 定时器节点(SBI 固件提供)
    clint@2000000 {
    compatible = "riscv,clint0";
    reg = <0x0 0x2000000 0x0 0x10000>;
    interrupts-extended = <&cpu0_intc 3>, // hart 0 的 M-mode 软件中断
    <&cpu0_intc 7>; // hart 0 的 M-mode 定时器中断
    };

    与 ARM 的关键差异:

    特性ARMRISC-V
    CPU 标识 cpu@0 + reg = <0> cpu@0 + reg = <0> + riscv,isa
    中断控制器 GIC(#interrupt-cells = <3>) PLIC(#interrupt-cells = <1>)
    定时器 架构定时器(arch_timer) CLINT/ACLINT(timebase-frequency)
    CPU 启动 enable-method = "psci" 由 OpenSBI 固件统一管理,设备树中不描述
    缓存描述 next-level-cache 标准 next-level-cache,但缓存大小常由固件动态提供
    14.2.3 cpu-map:NUMA/SMP 拓扑描述

    cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpu-map {
    cluster0 {
    core0 {
    cpu = <&cpu0>;
    };
    core1 {
    cpu = <&cpu1>;
    };
    };
    cluster1 {
    core0 {
    cpu = <&cpu2>;
    };
    core1 {
    cpu = <&cpu3>;
    };
    };
    };
    };

    14.3 FDT 启动协议差异:寄存器传递

    不同架构的 Bootloader 通过寄存器将 DTB 地址传递给内核,这是底层启动代码的硬契约。

    14.3.1 ARM32(AArch32)

    // arch/arm/kernel/head.S
    // 启动时寄存器约定:
    // r0 = 0(固定)
    // r1 = machine type number(早期使用,设备树时代已废弃,但保留为 0)
    // r2 = DTB 物理地址(或 ATAGS 列表地址,设备树启动时指向 DTB)

    // 内核早期汇编代码:
    __HEAD
    ENTRY(stext)
    // 保存 r2(DTB 地址)到安全位置
    // 后续 C 代码通过 __atags_pointer 或 dtb_phys 访问

    历史包袱:ARM32 保留了 machine type 概念(r1),虽然设备树启动时已无实际意义,但 Bootloader 通常仍设置为 0 或保留值。

    14.3.2 ARM64(AArch64)

    // arch/arm64/kernel/head.S
    // 启动时寄存器约定(EFI / U-Boot 统一):
    // x0 = DTB 物理地址(64 位)
    // x1 = 保留(0)
    // x2 = 保留(0)
    // x3 = 保留(0)

    // U-Boot 传递示例:
    // 直接设置 x0 = dtb_phys,然后跳转至内核入口

    ARM64 的简化:完全摒弃了 machine type,仅通过 x0 传递 DTB 地址,这是设备树设计哲学的彻底贯彻。

    14.3.3 RISC-V

    // arch/riscv/kernel/head.S
    // 启动时寄存器约定(SBI 固件 / U-Boot):
    // a0 = hart ID(当前硬件线程编号)
    // a1 = DTB 物理地址
    // a2 = 保留(SBI 扩展用)

    // OpenSBI 固件行为:
    // 1. 选择 Boot HART(通常为 hart 0)
    // 2. 将 DTB 加载到内存安全区域
    // 3. 设置 a0 = hartid, a1 = dtb_addr
    // 4. 跳转到内核入口地址

    RISC-V 的特殊性:a0 同时传递了当前 CPU 身份(hart ID)和设备树地址(a1),内核通过 a0 识别 Boot HART,通过 a1 解析硬件。

    14.3.4 启动协议对比总表
    架构DTB 传递寄存器辅助信息Bootloader 代表
    ARM32 r2 r0=0, r1=machid U-Boot、Barebox
    ARM64 x0 无 U-Boot、EDK2、Trusted Firmware
    RISC-V a1 a0=hartid OpenSBI、U-Boot、RustSBI
    PowerPC r3 r4=param, r5=epapr U-Boot、Yaboot
    MIPS a0 / a1(依平台) 依 Bootloader 实现 U-Boot、CFE
    14.3.5 内核早期解析入口

    // 各架构的统一解析入口(arch/xxx/kernel/setup.c)

    // ARM32
    void __init setup_arch(char **cmdline_p)
    {
    setup_machine_fdt(__atags_pointer); // __atags_pointer = r2
    }

    // ARM64
    void __init setup_arch(char **cmdline_p)
    {
    setup_machine_fdt(dtb_phys); // dtb_phys = x0
    }

    // RISC-V
    void __init setup_arch(void)
    {
    setup_early_pagetable();
    parse_dtb(dtb_early_va); // dtb_early_va = a1 映射后的虚拟地址
    }

    关键理解:无论架构如何差异,最终都收敛到 drivers/of/fdt.c 中的 early_init_dt_scan(),实现架构无关的统一解析。

    14.4 选择策略:何时用 ACPI,何时用设备树?

    场景推荐方案理由
    嵌入式 ARM SoC(全志、瑞芯微、NXP) 设备树 硬件高度定制,引脚复用、时钟树复杂,ACPI 无法描述
    ARM64 服务器(Ampere、鲲鹏、Graviton) ACPI 需要 NUMA、PCIe 热插拔、SR-IOV、高级电源管理
    通用 ARM64 笔记本/平板 ACPI 需要与 Windows on ARM 兼容的固件
    RISC-V 嵌入式(K210、VisionFive) 设备树 生态早期,硬件差异大,ACPI 支持不成熟
    RISC-V 服务器(待成熟) ACPI 或混合 预计跟随 ARM64 路径,服务器级用 ACPI
    需要动态硬件变更(FPGA 部分重配置) 设备树 + Overlay ACPI 不支持运行时硬件描述变更

    15. 版本演进与关键内核里程碑

    内核版本关键特性影响
    2.6.x PowerPC 设备树支持 设备树首次进入 Linux
    3.0+ ARM 架构全面采用设备树 移除大量 mach-xxx 目录,ARM 内核统一化
    3.7+ Device Tree Overlay 引入 支持动态外设扩展
    3.15+ of_platform_default_populate() 统一设备树到平台设备的转换接口
    4.1+ DT Schema 验证框架起步 设备树从语法正确走向语义正确
    4.12+ 动态设备树(Live DT)增强 支持更多运行时节点修改
    5.0+ 改进的 Overlay 应用机制 更稳定的动态外设加载
    6.0+ 设备树编译器(dtc)同步更新 支持更多属性类型和验证规则

    16. 工程实践与调试技巧

    16.1 设备树调试命令

    # 查看当前运行设备树(需开启 CONFIG_PROC_DEVICETREE)
    cat /proc/device-tree/soc/serial@10000000/status
    xxd /proc/device-tree/soc/serial@10000000/reg # 查看二进制属性值

    # 通过 sysfs 查看(推荐)
    ls /sys/firmware/devicetree/base/ # 完整设备树镜像
    hexdump -C /sys/firmware/devicetree/base/memory@80000000/reg

    # 反编译当前运行 DTB
    cp /sys/firmware/fdt /tmp/current.dtb
    dtc -I dtb -O dts -o current.dts /tmp/current.dtb

    16.2 常见问题排查

    现象根因排查方法
    设备未生成 /dev/xxx status = "disabled" 检查设备树节点 status
    驱动 probe 未触发 compatible 不匹配 比对驱动 of_match_table 与设备树
    中断注册失败 interrupts 属性错误 检查 interrupt-parent 和 #interrupt-cells
    内存识别错误 #address-cells / #size-cells 不匹配 确认父节点 cells 定义与 reg 长度一致
    DTB 编译失败 语法错误或 phandle 未解析 dtc 加 -@ 参数检查符号

    16.3 开发规范建议

  • 分层组织:SoC 定义放 .dtsi,板级定义放 .dts,通过 #include 组合
  • compatible 命名:采用 "vendor,model" 格式,避免使用通用名称如 "gpio-leds" 作为首选
  • 状态管理:默认外设节点设为 "disabled",由板级 DTS 按需 "okay"
  • 标签命名:使用有意义的标签(如 uart0:、i2c1:),便于引用和覆盖
  • Schema 验证:Linux 5.x+ 应使用 dt-bindings YAML 文件进行驱动绑定验证

  • 17. 完整示例:从 DTS 到驱动

    17.1 硬件描述(DTS)

    // myboard.dtsi
    soc {
    #address-cells = <1>;
    #size-cells = <1>;

    myuart: uart@10000000 {
    compatible = "myvendor,myuart";
    reg = <0x10000000 0x100>;
    interrupts = <GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&apb0>;
    current-speed = <115200>;
    status = "disabled";
    };
    };

    // myboard.dts
    #include "myboard.dtsi"

    / {
    model = "MyBoard V1";
    compatible = "myvendor,myboard";

    chosen {
    stdout-path = "serial0:115200n8";
    };
    };

    &myuart {
    status = "okay";
    };

    17.2 驱动代码

    #include <linux/module.h>
    #include <linux/platform_device.h>
    #include <linux/of.h>
    #include <linux/io.h>
    #include <linux/interrupt.h>
    #include <linux/clk.h>

    struct myuart_drvdata {
    void __iomem *base;
    int irq;
    struct clk *clk;
    u32 baudrate;
    };

    static const struct of_device_id myuart_of_ids[] = {
    { .compatible = "myvendor,myuart" },
    { }
    };
    MODULE_DEVICE_TABLE(of, myuart_of_ids);

    static int myuart_probe(struct platform_device *pdev)
    {
    struct myuart_drvdata *drv;
    struct resource *res;
    int ret;

    drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL);
    if (!drv)
    return –ENOMEM;

    // 1. 获取并映射寄存器资源(从 reg 属性)
    drv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(drv->base))
    return PTR_ERR(drv->base);

    // 2. 获取中断(从 interrupts 属性)
    drv->irq = platform_get_irq(pdev, 0);
    if (drv->irq < 0)
    return drv->irq;

    ret = devm_request_irq(&pdev->dev, drv->irq, myuart_irq_handler,
    IRQF_SHARED, "myuart", drv);
    if (ret)
    return ret;

    // 3. 获取时钟(从 clocks 属性)
    drv->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(drv->clk))
    return PTR_ERR(drv->clk);
    clk_prepare_enable(drv->clk);

    // 4. 读取自定义属性(从 current-speed)
    ret = of_property_read_u32(pdev->dev.of_node, "current-speed",
    &drv->baudrate);
    if (ret)
    drv->baudrate = 9600; // 默认值

    platform_set_drvdata(pdev, drv);
    dev_info(&pdev->dev, "MyUART probed, baudrate=%u\\n", drv->baudrate);

    return 0;
    }

    static int myuart_remove(struct platform_device *pdev)
    {
    struct myuart_drvdata *drv = platform_get_drvdata(pdev);
    clk_disable_unprepare(drv->clk);
    return 0;
    }

    static struct platform_driver myuart_driver = {
    .probe = myuart_probe,
    .remove = myuart_remove,
    .driver = {
    .name = "myuart",
    .of_match_table = myuart_of_ids,
    },
    };

    module_platform_driver(myuart_driver);
    MODULE_LICENSE("GPL");

    17.3 数据流总结

    在这里插入图片描述

    设备树数据流总结 —— 左侧竖线为 DTS 源文件到驱动初始化的主线:DTS 经 dtc 编译为 DTB,由 Bootloader 加载后,内核通过 unflatten_device_tree() 展开为 device_node 树,再经 of_platform_populate() 创建 platform_device;匹配成功后进入 myuart_probe(),驱动在 probe 中通过右侧 OF API 读取 reg、interrupts、clocks、current-speed 等属性,最终完成硬件初始化。


    附录:参考资源

  • Devicetree Specification(官方规范):https://www.devicetree.org/
  • Linux Kernel Documentation:Documentation/devicetree/(内核源码内)
  • DT Binding Schema:Documentation/devicetree/bindings/(YAML 格式验证规则)
  • DTC 源码:scripts/dtc/(内核源码内)
  • OF Core 源码:drivers/of/(内核源码内)
  • Platform Bus 源码:drivers/base/platform.c(内核源码内)

  • 赞(0)
    未经允许不得转载:171主机测评 » Linux 设备树(Device Tree)概览
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址