18-ARM Linux 设备树

设备树(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 的迁移要点:

  1. platform_data 已被淘汰 ,用 device_property_read_u32()、 device_property_read_string() 或专用 API(gpiod_get()、clk_get())。
  2. of_platform_populate() 自动为设备树子节点创建 platform_device, simple-bus/simple-mfd 会自动递归。
  3. ACPI 与 DT 统一 :用 device_property_*() 和 fwnode_*() API 可以同时支持两种固件描述(推荐新驱动这样写)。
  4. 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", &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 总结

  1. 7.2.5 中设备树是 ARM/RISC-V 硬件描述的唯一主线,板级文件基本消失。
  2. .dts/.dtsi → dtc → .dtb → 内核 unflatten 成 device_node 树。
  3. 根节点 compatible 识别平台;设备节点 compatible 匹配驱动,都从具体到一般。
  4. 地址编码由父节点 #address-cells/#size-cells 决定;reg 必须与节点名地址一致。
  5. 中断用 interrupt-parent + interrupts,cell 数由中断控制器的 #interrupt-cells 决定。
  6. GPIO/clock/pinmux/regulator/reset 都有标准的 DT 属性与对应的 devm_* API。
  7. 7.2.5 的现代写法 :用 device_property_*()/fwnode_*() 同时支持 DT 与 ACPI; 用 gpiod_*() 描述符接口取代整数 GPIO;pinctrl 由内核自动应用。
  8. 无开发板时,用 QEMU -machine virt + dumpdtb + dtc 学习设备树, 用 drivers/of/unittest.c 验证 OF 解析。
相关推荐
Snasph3 小时前
20-Linux 芯片级移植及底层驱动
linux 驱动学习
Snasph3 小时前
12- Linux 设备驱动的软件架构思想
linux 驱动学习
Snasph4 小时前
23-从 Linux 4.0 到 7.2.5 的迁移
linux 驱动学习
Snasph11 天前
01-设备驱动概述与开发环境构建
linux 驱动学习
Snasph11 天前
05-Linux 文件系统与设备文件
linux 驱动学习
Snasph11 天前
04-Linux 内核模块
linux 驱动学习