设备树(Device Tree, DT)是 7.2.5 内核里硬件描述的唯一主线 。 4.0 时代 ARM32 还有大量板级文件(arch/arm/mach-*/board-*.c), 到 7.2.5 这些文件基本绝迹,硬件信息全部来自设备树(ARM/部分 RISC-V) 或 ACPI(x86/服务器)。
一句话理解设备树 :把"这块板子上有什么硬件、怎么连线"从 C 代码搬到一种 可编译、可传递的数据结构 里,内核启动时解析它,然后自动创建 platform_device / i2c_client / spi_device 等设备对象。
18.1 设备树起源
历史脉络:
| 时期 | ARM Linux 硬件描述方式 |
|---|---|
| 早期 | arch/arm/mach-xxx/board-yyy.c 里写死一堆 platform_device |
| 2011 起 | 引入设备树,逐步取代板级文件 |
| 4.0 | 混合期,多数新板已用 DT,旧板仍有板级文件 |
| 7.2.5 | DT 是唯一主线 ;mach-* 目录几乎只剩少数必需文件 |
为什么板级文件必须死 :每块板都往内核里塞 board-*.c,导致内核里 "代码"和"数据"纠缠,Linus 当年直言 ARM 平台代码是"一堆垃圾"。 设备树把"数据"抽出来,内核只保留"代码"。
设备树也解决了同一个 SoC 不同板子 的问题:SoC 的 .dtsi 描述片上外设, 板子的 .dts 描述板级差异,二者用 include 组合。
18.2 设备树的组成和结构
18.2.1 DTS、DTC 和 DTB
.dts / .dtsi (源码,人类可读)
│ dtc 编译
▼
.dtb (二进制,内核解析)
│ bootloader 传给内核
▼
内核启动时 unflatten 成 struct device_node 树
| 文件 | 含义 | 位置 |
|---|---|---|
.dts |
Device Tree Source,板级描述 | arch/<arch>/boot/dts/ |
.dtsi |
Device Tree Source Include,被 include 的公共部分 | 同上 |
.dtb |
Device Tree Blob,编译产物 | 构建目录 |
.dtso |
Device Tree Source Overlay | 同上 |
.dtbo |
编译后的 overlay | 同上 |
编译器 dtc :源码在 scripts/dtc/(内核自带一份),宿主也可用 device-tree-compiler 包里的 dtc。
bash
# 编译设备树(在内核构建里)
make -C "$KSRC" O="$KBUILD" dtbs
# 手动编译/反编译
dtc -I dts -O dtb -o my.dtb my.dts
dtc -I dtb -O dts -o my.dts my.dtb # 反编译,调试神器
设备树示例:
cpp
/dts-v1/;
/ {
model = "My Example Board";
compatible = "myvendor,my-board", "myvendor,my-soc";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "console=ttyAMA0 earlycon";
};
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x20000000>; /* 512 MiB */
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0>;
};
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
uart0: serial@9000000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x09000000 0x1000>;
interrupts = <0 1 4>; /* SPI 1, level-high */
clocks = <&clk24m>;
clock-names = "apb_pclk";
};
i2c0: i2c@a000000 {
compatible = "myvendor,my-i2c";
reg = <0x0a000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <400000>;
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>;
};
};
};
clk24m: clock-24m {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>;
};
};
18.2.2 根节点兼容性
根节点的 compatible 是平台识别的关键:
compatible = "myvendor,my-board", "myvendor,my-soc";
- 从左到右,从具体到一般。
- 内核用它匹配
machine_desc(早期)或初始化平台(现代)。 - 板子专属的 quirk 用最左的
myvendor,my-board判断。
驱动侧:
cpp
if (of_machine_is_compatible("myvendor,my-board"))
my_board_quirk();
18.2.3 设备节点兼容性
设备节点的 compatible 是驱动匹配的关键,同样从具体到一般:
cpp
uart0: serial@9000000 {
compatible = "arm,pl011", "arm,primecell";
};
驱动:
cpp
static const struct of_device_id mydrv_of_match[] = {
{ .compatible = "arm,pl011", .data = &pl011_data },
{ .compatible = "arm,primecell", .data = &primecell_data },
{ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);
内核用 of_device_is_compatible() 逐个比较,取第一个匹配项, 所以顺序很重要(具体型号在前)。
compatible 的命名规范:"<vendor>,<device>",全小写,逗号分隔, vendor 前缀见 Documentation/devicetree/bindings/vendor-prefixes.yaml。
18.2.4 设备节点及 label 的命名
| 形式 | 示例 | 说明 |
|---|---|---|
| label | uart0: |
供其它节点用 &uart0 引用 |
| 节点名 | serial@9000000 |
通用名 + @ + 地址 |
| 单元地址 | 9000000 |
与 reg 第一个值一致 |
| 节点全路径 | /soc/serial@9000000 |
唯一 |
命名规则:
- 节点名用通用设备类别 (
serial、i2c、ethernet、gpio、mmc), 不用厂商型号(pl011是错的)。 - 有地址的节点必须写
@地址,且与reg的第一个值(#address-cells宽度)一致。 - label 用小写下划线或连字符,全局唯一。
18.2.5 地址编码
地址编码由父节点的 #address-cells 和 #size-cells 决定:
cpp
soc {
#address-cells = <1>; /* 地址用 1 个 u32 表示 */
#size-cells = <1>; /* 长度用 1 个 u32 表示 */
ranges; /* 恒等映射(子地址=父地址) */
uart0: serial@9000000 {
reg = <0x09000000 0x1000>; /* 起始 0x9000000,长度 0x1000 */
};
};
| 属性 | 含义 |
|---|---|
#address-cells |
子节点 reg 中地址占几个 cell |
#size-cells |
子节点 reg 中长度占几个 cell |
ranges |
子地址空间到父地址空间的映射;空 = 恒等 |
reg |
设备地址范围 |
reg-names |
多段 reg 的名字 |
典型组合:
| 场景 | address-cells | size-cells |
|---|---|---|
| 简单外设(SoC 内) | 1 | 1 |
| 64 位地址总线 | 2 | 2 |
| I²C 从设备 | 1 | 0(只有地址,无长度) |
| SPI 从设备 | 1 | 0 |
驱动侧读取地址:
cpp
struct resource res;
of_address_to_resource(np, 0, &res);
devm_ioremap_resource(dev, &res);
/* 或更简单: */
base = devm_platform_ioremap_resource(pdev, 0);
18.2.6 中断连接
设备树用 interrupt-parent + interrupts 描述中断连接:
cpp
intc: interrupt-controller@8000000 {
compatible = "arm,gic-v3";
interrupt-controller;
#interrupt-cells = <3>;
reg = <0x08000000 0x10000>;
};
uart0: serial@9000000 {
interrupt-parent = <&intc>; /* 继承或显式指定 */
interrupts = <0 1 4>; /* GIC SPI 1,电平高 */
};
interrupts 的含义由中断控制器的 #interrupt-cells 决定:
- GIC:
<类型 SPI/PPI 号 触发方式>,共 3 cell。 - 简单 GPIO 中断控制器:
<引脚号 触发方式>,共 2 cell。
中断级联(interrupt-map):GPIO 控制器作为中断控制器时:
cpp
gpio0: gpio@b000000 {
compatible = "myvendor,gpio";
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
interrupts = <0 10 4>; /* 它自己接到 GIC SPI 10 */
};
驱动侧:
cpp
int irq = platform_get_irq(pdev, 0); /* 推荐 */
/* 或 */
int irq = of_irq_get(np, 0);
int n = of_irq_count(np);
platform_get_irq() 会自动处理 DT、ACPI、板级三种来源。
18.2.7 GPIO、时钟、pinmux 连接
GPIO:
cpp
#include <dt-bindings/gpio/gpio.h>
led {
compatible = "gpio-leds";
led0 {
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
label = "green:status";
default-state = "off";
};
};
驱动侧(7.2.5 用描述符接口,整数 GPIO 接口已禁用):
cpp
struct gpio_desc *led;
led = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);
gpiod_set_value(led, 1);
int val = gpiod_get_value(led);
int irq = gpiod_to_irq(led);
时钟:
cpp
uart0: serial@9000000 {
clocks = <&clk24m>, <&clk_apb>;
clock-names = "uartclk", "apb_pclk";
};
clk = devm_clk_get(dev, "uartclk");
clk = devm_clk_get_enabled(dev, NULL); /* 7.2.5 推荐:取到并使能 */
pinmux(pinctrl):
cpp
&iomuxc {
pinctrl_uart0: uart0grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
>;
};
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart0>;
};
pinctrl 由内核自动应用 (pinctrl_bind_pins()),驱动通常什么都不用做------ 这是 7.2.5 相比 4.0 的重要简化。
其它常见连接:
| 连接 | 属性 | 驱动 API |
|---|---|---|
| 电源 | vcc-supply |
devm_regulator_get() |
| 复位 | resets |
devm_reset_control_get() |
| DMA | dmas/dma-names |
dma_request_chan() |
| PHY | phys/phy-names |
devm_phy_get() |
| GPIO | *-gpios |
devm_gpiod_get*() |
| 中断 | interrupts |
platform_get_irq() |
| 时钟 | clocks |
devm_clk_get*() |
| IOMMU | iommus |
自动 |
| 预留内存 | memory-region |
of_reserved_mem_device_init() |
18.3 由设备树引发的 BSP 和驱动变更
设备树把"板级代码"变成了"数据 + 通用驱动":
| 方面 | 4.0 板级文件时代 | 7.2.5 设备树时代 |
|---|---|---|
| 设备定义 | board-*.c 里的 platform_device |
.dts 节点 |
| 资源 | struct resource 数组 |
reg + interrupts |
| 匹配 | platform_device.name |
compatible |
| 私有数据 | platform_data 结构体 |
设备树属性 + device_property_*() |
| 引脚复用 | mach-*/iomux-*.c |
pinctrl 子系统 + .dts |
| 时钟 | clk_register_*() 手写 |
CCF + .dts |
| 电源 | 手写 regulator | regulator 子系统 + .dts |
7.2.5 的迁移要点:
platform_data已被淘汰 ,用device_property_read_u32()、device_property_read_string()或专用 API(gpiod_get()、clk_get())。of_platform_populate()自动为设备树子节点创建platform_device,simple-bus/simple-mfd会自动递归。- ACPI 与 DT 统一 :用
device_property_*()和fwnode_*()API 可以同时支持两种固件描述(推荐新驱动这样写)。 - overlay(动态设备树) 在 7.2.5 通过
of_overlay_fdt_apply()API 使用(drivers/of/overlay.c,CONFIG_OF_OVERLAY), 典型使用者是 DRM 面板、音频卡、FPGA 加载等子系统; 设备树单元测试在drivers/of/unittest.c里。
18.4 常用的 OF API
18.4.1 查找节点
| API | 作用 |
|---|---|
of_find_node_by_name(from, name) |
按节点名找 |
of_find_node_by_path(path) |
按路径找 |
of_find_compatible_node(from, type, compat) |
按 compatible 找 |
of_get_child_by_name(np, name) |
找子节点 |
of_get_next_child(np, prev) / for_each_child_of_node(np, child) |
遍历子节点 |
of_get_parent(np) / of_node_get() / of_node_put() |
父节点/引用计数 |
of_device_is_compatible(np, compat) |
判断兼容性 |
of_machine_is_compatible(compat) |
判断整机 |
18.4.2 读属性
| API | 作用 |
|---|---|
of_property_read_u32(np, name, &v) |
读 u32 |
of_property_read_u32_array(np, name, arr, n) |
读 u32 数组 |
of_property_read_u64(np, name, &v) |
读 u64 |
of_property_read_string(np, name, &s) |
读字符串 |
of_property_read_string_index(np, name, i, &s) |
读字符串数组元素 |
of_property_read_variable_u32_array() |
变长数组 |
of_property_count_u32_elems(np, name) |
元素个数 |
of_property_present(np, name) |
属性是否存在(7.2.5 新名,取代 of_property_read_bool 的部分场景) |
of_property_read_bool(np, name) |
布尔属性 |
18.4.3 解析连接
| API | 作用 |
|---|---|
of_parse_phandle(np, name, index) |
解析单个 phandle |
of_parse_phandle_with_args() |
解析 phandle + 参数 |
of_parse_phandle_with_fixed_args() |
固定 cell 数 |
of_find_property() |
找属性 |
of_property_match_string(np, "clock-names", "apb_pclk") |
按名字找索引 |
of_address_to_resource(np, index, &res) |
地址转 resource |
of_irq_get(np, index) / of_irq_count(np) |
取中断 |
of_get_named_gpio(np, name, index) |
取 GPIO 号(新代码用 devm_gpiod_get) |
of_reserved_mem_device_init(dev) |
关联预留内存 |
18.4.4 现代替代:fwnode 与 device_property
7.2.5 推荐同时支持 DT 和 ACPI的写法:
cpp
/* 不关心底层是 DT 还是 ACPI */
if (!device_property_read_u32(dev, "clock-frequency", &freq))
...
if (device_property_read_bool(dev, "myvendor,special-mode"))
...
/* 取固件节点(DT 或 ACPI 统一抽象) */
struct fwnode_handle *fw = dev_fwnode(dev);
fwnode_property_read_u32(fw, "reg", ®);
device_property_*() 是 fwnode_property_*() 的封装,新驱动应优先使用。
18.5 QEMU 设备树实验
实验 A:启动 aarch64 virt 并观察设备树
bash
# 宿主机
qemu-system-aarch64 \
-machine virt,dumpdtb=/tmp/virt.dtb \
-cpu cortex-a57 -smp 2 -m 512M -nographic \
-kernel "$KBUILD/arch/arm64/boot/Image" \
-append "console=ttyAMA0 root=/dev/ram rdinit=/sbin/init"
# 反编译 QEMU 生成的设备树
dtc -I dtb -O dts -o /tmp/virt.dts /tmp/virt.dtb
less /tmp/virt.dts
观察点 :/proc/device-tree/(或 /sys/firmware/devicetree/base/) 就是内核里的设备树:
bash
# guest 内
ls /proc/device-tree/
cat /proc/device-tree/model
cat /proc/device-tree/compatible | tr '\0' '\n'
ls /proc/device-tree/soc/
cat /proc/device-tree/soc/serial@9000000/compatible | tr '\0' '\n'
实验 B:自己编译并加载设备树
bash
# 宿主机:编译自己的 dts
dtc -I dts -O dtb -o /tmp/myboard.dtb /path/to/myboard.dts
# 用自定义 dtb 启动(qemu virt 可用 -dtb 覆盖)
qemu-system-aarch64 -machine virt -dtb /tmp/myboard.dtb ...
注意 :直接替换 virt 的设备树需要你补全所有必需节点(CPU、GIC、timer), 否则内核起不来。学习阶段建议先反编译 QEMU 的 dtb 再改。
实验 C:x86_64 + ACPI(对照)
x86 不用设备树,用 ACPI。内核把 ACPI 表解析成同样的 struct device 树:
bash
# guest 内
ls /sys/firmware/acpi/tables/
acpidump | less # 需 acpica-tools
对比 of_node 与 fwnode,理解"设备模型统一抽象"。
实验 D:设备树单元测试
drivers/of/unittest.c(CONFIG_OF_UNITTEST,配合 CONFIG_OF_OVERLAY) 会在启动时自动测试 OF API,输出到 dmesg:
bash
dmesg | grep -i "unittest\|OF:"
这是完全无硬件验证设备树解析的最佳路径。
18.6 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
#address-cells/#size-cells 不匹配 |
地址解析错误 | 检查父节点 cells 与 reg 宽度 |
节点名与 reg 地址不一致 |
dtc 警告 | 让 @ 后的值与 reg 首值相同 |
compatible 顺序写反 |
匹配到通用驱动 | 具体型号放最左 |
用 platform_data |
7.2.5 中大量驱动不再支持 | 改用 device property |
gpio_get_value() 等整数接口 |
编译失败/已禁用 | 用 gpiod_*() |
忘记 of_node_put() |
引用计数泄漏 | 与 of_node_get/查找配对 |
interrupts cell 数与控制器不符 |
中断号错误 | 看控制器的 #interrupt-cells |
| 直接改 QEMU 生成的 dtb | 启动失败 | 先反编译再增量修改 |
| pinctrl 手动应用 | 重复配置 | 内核自动应用,驱动不用管 |
只写 clocks 不写 clock-names |
clk_get(dev, "name") 失败 |
多时钟时补 clock-names |
18.7 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| 反编译 QEMU dtb | 理解 DTS/DTB | dtc -I dtb -O dts |
读 /proc/device-tree |
理解内核 DT 视图 | model/compatible |
| 自定义 dtb 启动 | 学习 DT 编写 | 内核能起来 |
| OF API 小模块 | 学习属性解析 | 打印读到的属性 |
| device_property 写法 | 学习 DT/ACPI 统一 | 两种固件都能用 |
| gpiod 实验 | 学习 GPIO 描述符 | 配合 gpio-sim |
| OF unittest | 验证 OF 解析 | dmesg 有测试输出 |
| overlay API 阅读 | 学习动态 DT | of_overlay_fdt_apply() |
18.8 总结
- 7.2.5 中设备树是 ARM/RISC-V 硬件描述的唯一主线,板级文件基本消失。
.dts/.dtsi→dtc→.dtb→ 内核 unflatten 成device_node树。- 根节点
compatible识别平台;设备节点compatible匹配驱动,都从具体到一般。 - 地址编码由父节点
#address-cells/#size-cells决定;reg必须与节点名地址一致。 - 中断用
interrupt-parent+interrupts,cell 数由中断控制器的#interrupt-cells决定。 - GPIO/clock/pinmux/regulator/reset 都有标准的 DT 属性与对应的
devm_*API。 - 7.2.5 的现代写法 :用
device_property_*()/fwnode_*()同时支持 DT 与 ACPI; 用gpiod_*()描述符接口取代整数 GPIO;pinctrl 由内核自动应用。 - 无开发板时,用 QEMU
-machine virt+dumpdtb+dtc学习设备树, 用drivers/of/unittest.c验证 OF 解析。