Linux 设备树(Device Tree)概览

Linux 设备树(Device Tree)概览

适用版本 :Linux 3.x ~ 6.x

目标读者:嵌入式驱动开发工程师、系统架构师、BSP 维护者


目录

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

1. 设计哲学与历史演进

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

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

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

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

  1. 代码冗余:不同板卡使用相同 SoC 时,大量时钟、GPIO、中断代码重复
  2. 维护困难:硬件迭代需要修改内核源码,涉及复杂的编译和发布流程
  3. 生态碎片化:每个厂商维护自己的内核分支,无法汇聚到主线

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_nodeplatform_devicestruct device,最终与 device_driver 匹配。


3. 设备树语法规范

3.1 基本结构

dts 复制代码
/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

规则

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

3.3 关键属性详解

3.3.1 compatible ------ 驱动匹配的基石
dts 复制代码
compatible = "vendor,specific-model", "generic-family";
  • 作用:驱动的匹配标识符,内核按从左到右优先级匹配
  • 驱动侧 :通过 of_device_id 数组声明支持的 compatible 字符串
  • 策略:优先写具体型号,后缀写通用系列,确保兼容性回退
c 复制代码
// 驱动代码示例
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 ------ 寄存器地址与资源描述
dts 复制代码
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 与中断控制器
dts 复制代码
interrupts = <type number trigger>;
  • GIC 中断类型GIC_SPI(共享外设中断)、GIC_PPI(私有外设中断)
  • 触发方式IRQ_TYPE_EDGE_RISING, IRQ_TYPE_LEVEL_HIGH
  • 中断映射 :通过 interrupt-parent 指向中断控制器节点,或继承父节点设置
dts 复制代码
// 中断控制器定义
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)

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

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

4. 地址映射与中断机制

4.1 地址映射体系

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

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

4.2 中断映射体系

dts 复制代码
// 中断控制器节点
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>;          // 掩码决定哪些位参与匹配
};

中断映射流程

  1. 设备声明 interrupts = <0>;
  2. 内核查找 interrupt-parent(或继承父节点)
  3. 若父节点有 interrupt-map,按掩码匹配转换规则
  4. 最终映射到根中断控制器的全局中断号
  5. 驱动通过 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_nodeof_platform_default_populate() 将其转换为 platform_device 并注册到平台总线;最终由 platform_match 完成设备-驱动匹配并调用 driver.probe(),设备可用。

5.2 核心数据结构

c 复制代码
// 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 的转换

c 复制代码
// 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() 完成设备初始化。

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) {
        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:

c 复制代码
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() 是设备树到平台设备的核心转换入口,其递归规则决定了哪些节点会被自动注册:

c 复制代码
// 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 的绑定关系

c 复制代码
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 的引用计数,驱动开发者通常无需手动干预。但在以下场景需要显式处理:

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

6.5 Overlay 节点的内存管理

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

c 复制代码
// 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() 失败时的资源清理

常见内存泄漏场景

c 复制代码
// 错误示例: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 调试引用计数泄漏

bash 复制代码
# 查看当前系统 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 语法

dts 复制代码
// 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 加载流程

bash 复制代码
# 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" 格式,这是强制规范,不是建议:

dts 复制代码
// 正确示例
compatible = "myvendor,myuart-v1", "myvendor,myuart", "generic-uart";

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

命名策略

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

为什么需要多个 compatible?

  1. 向后兼容:新驱动支持旧设备(通过系列通用型号)
  2. 向前兼容:旧驱动支持新设备(通过通用型号回退)
  3. 代码复用:同一驱动支持多个硬件变体
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() 的完整调用链
c 复制代码
// 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() 的源码解析
c 复制代码
// 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() 的核心匹配逻辑
c 复制代码
// 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() 的字符串比对
c 复制代码
// 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 结构体详解
c 复制代码
// include/linux/mod_devicetable.h
struct of_device_id {
    char name[32];        // 按节点名匹配(已废弃,留空)
    char type[32];        // 按类型匹配(已废弃,留空)
    char compatible[128]; // 按 compatible 匹配(核心字段)
    const void *data;     // 私有数据指针(差异化配置)
};

现代驱动中的标准写法

c 复制代码
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 字段是绑定机制中最强大的设计之一,它允许同一驱动支持不同硬件变体,而无需条件分支:

c 复制代码
// 配置结构体定义
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, ...) 的作用
c 复制代码
MODULE_DEVICE_TABLE(of, myuart_of_match);

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

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

模块 alias 的生成规则

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

可以通过 modinfo 验证:

bash 复制代码
$ 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 内核级调试:跟踪绑定过程
c 复制代码
// 在驱动中添加调试信息,追踪绑定过程
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

bash 复制代码
# 启用函数跟踪
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 绑定的特殊之处

dts 复制代码
&i2c1 {
    status = "okay";

    // I2C 子设备的 reg = I2C 从设备地址
    temperature-sensor@48 {
        compatible = "bosch,bme280";
        reg = <0x48>;  // I2C 地址,不是内存地址
        status = "okay";
    };
};
c 复制代码
// 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 节点查找类
c 复制代码
// 通过路径查找(绝对路径)
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 属性读取类
c 复制代码
// 读取单个数值(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 资源转换类
c 复制代码
// 将 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 内存映射类
c 复制代码
// 映射 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 中断解析类
c 复制代码
// 解析中断并映射到 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 专用类
c 复制代码
// 时钟
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/null cat /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/.../interrupts `dtc -I dtb -O dts /sys/firmware/fdt
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-devices cat /sys/kernel/debug/pinctrl/pinmux-pins
内存识别少了一半 #address-cells / #size-cellsreg 长度不匹配 核对父节点的 cells 定义与 reg 数组元素个数是否一致 `dtc -I dtb -O dts /sys/firmware/fdt
GPIO 编号错乱 / 无法控制 #gpio-cells 不匹配(如应为 2 但写成了 3) ② GPIO_ACTIVE_LOW / GPIO_ACTIVE_HIGH 混淆 ① 核对 GPIO 控制器 #gpio-cells ② 检查硬件原理图电平逻辑 cat /sys/kernel/debug/gpio gpiod_get_value() 返回值与万用表实测对比
DMA 传输失败 / 通道申请失败 dmas 中的请求号错误 ② #dma-cells 不匹配 ③ DMA 控制器驱动未加载 ① 核对 SoC TRM 中的 DMA 请求号 ② 检查 #dma-cellsdmas 数组长度 ③ 确保 DMA 控制器 status = "okay" ls /sys/class/dma/ `dmesg
of_property_read_u32() 返回 -EINVAL ① 属性名拼写错误 ② 属性类型不匹配(字符串 vs 数值) ③ 属性不存在 ① 核对属性名大小写 ② 用 hexdump 查看属性原始类型 ③ 检查 DTS 是否包含该属性 hexdump -C /sys/firmware/devicetree/base/.../属性名 `dtc -I dtb -O dts ...
Overlay 加载失败 dtc 未加 -@ 参数(符号丢失) ② target 节点路径错误 ③ 内核未开启 CONFIG_OF_OVERLAY dtc -@ -I dts -O dtbo ... ② 核对 target = <&label> 或路径 ③ 开启内核配置并确保 configfs 已挂载 cat /config/device-tree/overlays/xxx/status `dmesg
设备树编译通过但内核启动崩溃 reg 地址越界(访问未映射内存) ② interrupts 编号冲突 ③ memory 节点与实际硬件不符 ① 核对 TRM 寄存器地址范围 ② 检查中断号是否与其他设备冲突 ③ 用 Bootloader 命令 md 验证内存 md.l 0x10000000 0x10(U-Boot) `dmesg

9.3 调试命令速查

bash 复制代码
# ========== 查看当前运行设备树 ==========
# 查看节点属性(二进制格式)
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 时自动完成引脚配置。

dts 复制代码
// 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 驱动侧自动绑定
c 复制代码
// 驱动无需手动解析 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 属性描述外设的时钟依赖关系。

dts 复制代码
// 时钟提供者(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
c 复制代码
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 子系统负责数字电平读写

dts 复制代码
// 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
c 复制代码
// 获取 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 通道请求。

dts 复制代码
// 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
c 复制代码
// 获取 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 解析这些属性,完成资源绑定与初始化。其中 gpiosvcc-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 文件结构

yaml 复制代码
# 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)

yaml 复制代码
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 验证工具链使用

bash 复制代码
# 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 后,常需删除不需要的节点或属性:

dts 复制代码
#include "soc.dtsi"

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

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

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

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

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

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

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

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

dts 复制代码
/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_ACPICONFIG_OF同时开启,启动时由固件决定使用哪种描述方式
14.1.2 启动时选择逻辑
c 复制代码
// 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 驱动层面的差异
c 复制代码
// 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 节点:硬件线程描述
dts 复制代码
// 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 定时器基准
dts 复制代码
// 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 的关键差异

特性 ARM RISC-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 拓扑描述
dts 复制代码
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)
c 复制代码
// 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)
c 复制代码
// 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
c 复制代码
// 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 内核早期解析入口
c 复制代码
// 各架构的统一解析入口(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 设备树调试命令

bash 复制代码
# 查看当前运行设备树(需开启 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 开发规范建议

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

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

17.1 硬件描述(DTS)

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 驱动代码

c 复制代码
#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 读取 reginterruptsclockscurrent-speed 等属性,最终完成硬件初始化。


附录:参考资源

  1. Devicetree Specification(官方规范):https://www.devicetree.org/
  2. Linux Kernel DocumentationDocumentation/devicetree/(内核源码内)
  3. DT Binding SchemaDocumentation/devicetree/bindings/(YAML 格式验证规则)
  4. DTC 源码scripts/dtc/(内核源码内)
  5. OF Core 源码drivers/of/(内核源码内)
  6. Platform Bus 源码drivers/base/platform.c(内核源码内)

相关推荐
Huangjin007_1 小时前
【Linux 系统篇(一)】入门基础指令详解(一)
linux·服务器
嵌入式小能手1 小时前
飞凌嵌入式ElfBoard-i2c与从设备通讯编程示例之i2c设备通信编程示例
linux
mounter6251 小时前
现代 Linux 内核中的数据持久化艺术:从回写(Writeback)到多样化写策略
linux·linux kernel·kernel·writeback·writethrough
会飞的小新2 小时前
《深入理解 DDR Memory Margin》系列之第一章:从 DDR 到 Memory Margin——为什么服务器需要进行内存裕量测试?
linux·服务器
治愈系贝壳2 小时前
ARM ---day1
linux·arm开发·学习
晚风吹长发2 小时前
探索软件测试——用例篇
大数据·linux·服务器·压力测试·测试·测试方法
CHANG_THE_WORLD2 小时前
Linux C 多线程 TCP 并发服务器:从 `accept()` 到资源管理深度剖析
linux·服务器·c语言
运维大师2 小时前
【Linux运维极简教程】10-日志管理与故障排查
linux·运维·服务器
笑锝没心没肺3 小时前
提升CPU平均使用率(自用)
linux·运维