Linux 设备树(Device Tree)概览
适用版本 :Linux 3.x ~ 6.x
目标读者:嵌入式驱动开发工程师、系统架构师、BSP 维护者
目录
- 设计哲学与历史演进
- 核心概念与术语体系
- 设备树语法规范
- 地址映射与中断机制
- 内核解析与设备模型整合
- 设备树生命周期与内存管理
- [设备树覆盖(Device Tree Overlay)](#设备树覆盖(Device Tree Overlay))
- [绑定机制深度解析(Binding Mechanism)](#绑定机制深度解析(Binding Mechanism))
- [驱动开发中的 OF API + 系统化速查表 + 诊断手册](#驱动开发中的 OF API + 系统化速查表 + 诊断手册)
- [子系统级节点详解(Pinctrl / Clock / GPIO / DMA)](#子系统级节点详解(Pinctrl / Clock / GPIO / DMA))
- [YAML Schema 验证与 dt-bindings](#YAML Schema 验证与 dt-bindings)
- [工程语法高级特性(DTS 进阶)](#工程语法高级特性(DTS 进阶))
- [Zephyr 与 Linux 设备树对比](#Zephyr 与 Linux 设备树对比)
- [与其他固件接口对比(ACPI / RISC-V / FDT 启动协议)](#与其他固件接口对比(ACPI / RISC-V / FDT 启动协议))
- 版本演进与关键内核里程碑
- 工程实践与调试技巧
- [完整示例:从 DTS 到驱动](#完整示例:从 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
/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 ------ 驱动匹配的基石
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 个寄存器区域)
- 内存映射 IO:
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@80000000、serial@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>; // 掩码决定哪些位参与匹配
};
中断映射流程:
- 设备声明
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 核心数据结构
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 的引用计数,驱动开发者通常无需手动干预。但在以下场景需要显式处理:
- 驱动直接调用
of_find_node_by_xxx()获取节点 :必须of_node_put() - 驱动在
remove()中缓存了节点指针 :必须of_node_put() - 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?
- 向后兼容:新驱动支持旧设备(通过系列通用型号)
- 向前兼容:旧驱动支持新设备(通过通用型号回退)
- 代码复用:同一驱动支持多个硬件变体
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);
这个宏看似只是声明,实则承担关键功能:
- 编译时 :将
of_match_table导出到模块的.modinfo段 - 加载时 :
modprobe或depmod读取模块的 alias 信息,建立of:N*T*C<compatible>格式的别名 - 自动加载 :当内核检测到设备树中有匹配的
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-cells 与 reg 长度不匹配 |
核对父节点的 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-cells 与 dmas 数组长度 ③ 确保 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 解析这些属性,完成资源绑定与初始化。其中 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 文件结构
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_ACPI与CONFIG_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 开发规范建议
- 分层组织 :SoC 定义放
.dtsi,板级定义放.dts,通过#include组合 - compatible 命名 :采用
"vendor,model"格式,避免使用通用名称如"gpio-leds"作为首选 - 状态管理 :默认外设节点设为
"disabled",由板级 DTS 按需"okay" - 标签命名 :使用有意义的标签(如
uart0:、i2c1:),便于引用和覆盖 - Schema 验证 :Linux 5.x+ 应使用
dt-bindingsYAML 文件进行驱动绑定验证
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 读取 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(内核源码内)