跳转到内容
新建笔记

设备树:节点、地址转换与驱动接入

设备树是一份描述硬件拓扑、资源和连接关系的数据。它尤其适合无法通过总线自动发现的片上设备,使同一个驱动可以从不同板级描述中取得地址、时钟、中断和 GPIO 等参数。它不能代替驱动,也不意味着“所有硬件都必须写一个 platform 节点”:USB、PCI 以及 I²C/SPI 子设备各有自己的枚举或创建机制。

本文把 Devicetree Specification v0.3 的基本格式与 Linux 6.8 的设备创建、资源读取区分开。F1C100S 示例按 32 位 ARM 平台解释;原文把它与 ARCH=arm64、AArch64 工具链混用的构建命令不适用于这颗 ARM926EJ-S 芯片。

1. DTS、DTB 与运行时节点

跳转到“1. DTS、DTB 与运行时节点”

常见流程是:板级 .dts 包含 SoC/公共 .dtsi,经预处理与 DTC 编译成 .dtb,由启动程序传给内核。内核再解析成 device_node 等结构,并按总线规则创建 Linux 设备。#include、宏和设备树的 /include/ 属于不同语法机制;内核构建中使用 C 预处理器是正常流程。

设备树源码、编译结果与硬件描述之间的原始示意

原图将若干外设画在 CPU 下面,仅用于表达层级。标准 /cpus 节点描述处理器;不能据此把 SPI、I²S 等片上外设随意塞进某个 CPU 节点。

观察一个节点时依次确认:

  1. 启动时实际使用的 DTB 中是否包含它,状态是否可用。
  2. 对应总线是否把它创建成了一个 Linux 设备。
  3. 是否有匹配的驱动,并且驱动是否已加载。
  4. probe 是否成功,依赖的时钟、供电、GPIO、中断控制器等是否就绪。

/sys/firmware/devicetree/base(一些系统也提供 /proc/device-tree)能帮助确认第一步;其中的属性是二进制数据,不都适合直接当普通文本 cat。看到节点不等于后面的三步全部完成。Linux 的设备树使用模型

2. 节点、标签与常见属性

跳转到“2. 节点、标签与常见属性”
写法含义与约束
label: node-name@unit-addresslabel 用于源文件中的 &label 引用;节点名和地址决定路径,三者不能互换
@1c20800地址不写 0x 前缀;有 reg 时,单元地址通常对应第一个 reg 地址,具体服从总线 binding
compatible = "vendor,model", "vendor,family";从更具体到更通用的兼容描述;逗号后不额外嵌入空格,具体字符串必须有明确的编程模型含义
status = "okay"; / "disabled"可用或禁用;不能拼成 "disable"。Linux 通常将缺省状态视为可用,但仍需满足其他创建条件
status = "reserved"; / "fail" / "fail-sss"规范分别表示设备可运行但保留不用(通常由固件等控制)、不可操作的故障、带设备特定故障码 sss 的失败;不是普通驱动的启用状态
model根节点描述具体板卡型号的字符串;与用于兼容匹配的 compatible 分工不同
reg设备在父总线地址空间中的地址与长度,由父节点的 cell 规则解释
#address-cells、#size-cells本节点对子节点 reg 规定的地址和长度各占几个 32 位 cell;规范要求在需要时明确填写,不能依靠任意祖先继承
ranges子总线地址向父总线地址的映射;空属性表示相同地址空间,缺少属性不等于已经声明恒等映射
interrupts、interrupt-parent中断说明符及控制器关系;格式由对应中断控制器 binding 决定
name旧式独立属性已弃用;新节点按节点名命名,不用它替代标签或匹配表

device_type 也不用于任意设备分类;CPU、内存等兼容场景有专门要求。compatible 既可用于根节点的平台识别,也可用于普通设备的驱动匹配,不是 GIC 专有属性。设备树标准属性

板级型号、别名、启动参数与电源

跳转到“板级型号、别名、启动参数与电源”

下面片段作为 board-properties.dtsi 包含到采用 Linux 6.8 suniv-f1c100s.dtsi 的教学 DTS 中;uart0 标签来自该 SoC 文件。板名和 3.3 V 电源都只是语法示例,不能直接用来宣称某块实际开发板的供电关系。

/ {
model = "VitaLogos F1C100S property exercise";
aliases {
serial0 = &uart0;
};
chosen {
stdout-path = "serial0:115200n8";
bootargs = "loglevel=4";
};
vcc3v3_example: regulator-3v3-example {
compatible = "regulator-fixed";
regulator-name = "vcc3v3-example";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
};
};

aliases 把 serial0 映射到 UART 节点路径;这里的 &uart0 编译后是路径字符串,而不是 GPIO 一类说明符中的 phandle 数组。chosen 承载启动程序选择的参数,stdout-path 指定启动控制台路径或别名,冒号后的 115200n8 表示波特率、无校验和 8 位数据;bootargs 提供内核命令行,本例只设置日志等级。启动程序可能修改这些值,运行时应核对最终 DTB 与 /proc/cmdline。这段声明本身不负责启用 UART 或配置引脚复用。标准板级节点

固定电源的最小和最大电压相同;3300000 的单位是微伏,即 3.3 V。regulator-name 是电源的描述名称,消费者通过各自 binding 要求的 *-supply = <&vcc3v3_example> 建立供电引用。真实配置还需核对输入电源、使能线、时序及是否必须保持开启,不能只按这个标签猜电路。Linux 固定电源 binding

3. reg 的 cell 分组与地址转换

跳转到“3. reg 的 cell 分组与地址转换”

reg 中地址和长度按父节点 cell 数量分组的原图

父节点取 #address-cells = <1>、#size-cells = <1> 时,reg = <A0 L0 A1 L1> 表示两段资源。下标 0、1、2、3 是四个 cell,不是四个地址。CPU 或某些总线允许 #size-cells = <0>,此时不附带长度,不能套用固定“两两一组”。

下面是可单独编译的地址编码练习 example-map.dts。地址沿用原笔记的 PIO 布局,但节点是虚构的格式练习,不能作为实际 GPIO 驱动的板级配置加载。

/dts-v1/;
/* Address-encoding exercise only; this is not a board configuration. */
/ {
compatible = "example,address-encoding";
#address-cells = <2>;
#size-cells = <1>;
soc@1c00000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x01c00000 0x00100000>;
register-demo@20890 {
compatible = "example,register-layout";
reg = <0x00020890 0x4>,
<0x00020894 0x4>,
<0x000208a0 0x4>,
<0x000208ac 0x4>;
reg-names = "cfg0", "cfg1", "data", "pull0";
status = "okay";
};
};
};

这里 soc 的子地址宽度为 1,父地址宽度为 2,长度宽度为 1,所以 ranges 每项占 4 个 cell。子地址 0x20890 映射到 CPU 地址 0x01c20890。每段长度 4,最后一个字节是起始地址加 3;Linux 的 struct resource.end 是包含在资源内的末端。

终端窗口
dtc -I dts -O dtb -o example-map.dtb example-map.dts
fdtget -tx example-map.dtb /soc@1c00000/register-demo@20890 reg
dtc -I dtb -O dts -o example-map-decoded.dts example-map.dtb

这个检查能确认编码和语法,不能证明地址存在、时钟打开或寄存器归属正确。实际 SoC 的 PIO 整块区域通常已经由 pinctrl 驱动占用;不能再把这四段地址交给一个 LED 驱动竞争访问。

4. Linux 如何读取节点与属性

跳转到“4. Linux 如何读取节点与属性”

在已绑定的 platform 驱动中,首先使用 pdev->dev.of_node 或通用设备属性接口。按全局绝对路径反复查找 LED,容易绑定到另一实例,也绕开了设备模型已经提供的上下文。

接口用途及引用规则
of_find_node_by_name、of_find_node_by_type按节点名、旧 device_type 查找;返回节点引用。type 不是任意的 compatible 别名
of_find_compatible_node按兼容字符串查找;可选 type 通常传 NULL
of_find_node_by_path按路径/支持的别名查找;成功后需要 of_node_put
of_get_parent取得带引用的父节点,使用后 of_node_put
of_get_next_child / for_each_child_of_node遍历时会放掉上一个节点引用;提前退出循环时,还要放掉当前节点
of_find_property取得属性和字节长度;返回的内容属于设备树,不应由调用者 kfree
of_property_read_u32、of_property_read_u32_index读取经过字节序转换的整数;输出到真正的 u32 变量,并检查返回值
of_property_read_string读取由设备树拥有的字符串指针;不要释放,不把它当作永久自有副本
of_n_addr_cells、of_n_size_cells查询用于该节点地址解释的父总线 cell 数;Linux 的兼容回退行为不等于可以省略规范要求
of_address_to_resource将节点的某一段地址资源按总线规则转换为 struct resource
platform_get_resource / devm_platform_ioremap_resourceplatform 设备已形成资源表后,按资源项取得或申请映射;不能把 reg 的 cell 下标当资源项下标

表中的 of_find_node_by_name、of_find_node_by_type 和 of_find_compatible_node 等带 from 参数的查找接口,会从 from 之后继续查找,并消耗传入的 from 引用,即内部调用 of_node_put(from);不是只在 of_get_next_child 中才发生这一行为。对同一个已交出的引用不要再重复 put,若还需要独立保留它,应事先取得自己的引用。新返回的节点仍由调用者在使用后放掉。

接收 MMIO 映射的是 void __iomem *;接收属性数值的是 u32。原代码把 &pointer 强转为 u32 *,不仅丢失 cell 分组与 ranges,还可能只改写指针的一部分。需要直接访问某个由该设备独占的寄存器块时,优先使用 platform 资源映射帮助函数,检查 IS_ERR/PTR_ERR;时钟、复位、供电和寄存器访问宽度仍由对应驱动负责。

节点引用帮助对象继续存在,不自动赋予驱动对动态 overlay 属性缓冲区的永久所有权。若启用运行时设备树变化,应按该机制管理引用和数据副本,避免长期保存已撤销 overlay 中的裸属性地址。Linux OF API 源码

5. 从 DT 节点到 platform_device

跳转到“5. 从 DT 节点到 platform_device”

根级遍历及 simple-bus 等总线的递归填充只是 Linux 常见路径之一。节点没有 reg 或 interrupts 仍可能成为 platform 设备,例如由 GPIO 描述连接的消费者;arm,primecell 走 AMBA 设备创建,I²C/SPI 子节点由相应总线驱动创建。Linux 6.8 的 OF 设备创建代码

因此,不应把规则简化为“根节点下、有 reg 或中断的节点才转成 platform_device”,也不应认为每个子节点都会独立生成一个 platform 设备。GPIO LED 的子节点通常由 gpio-leds 驱动解释,其层级结构本身就是 binding 的一部分。

驱动侧的完整匹配与初始化例子见 设备树平台驱动。文件接口仍需遵守 字符设备的读写与生命周期;设备树只提供描述,不会修复 copy_from_user 返回值、未初始化锁或提前公开回调的问题。

6. F1C100S 的构建与核对顺序

跳转到“6. F1C100S 的构建与核对顺序”

在已配置的相应内核/BSP 中,用该版本的构建系统执行类似:

终端窗口
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- dtbs

这里的工具链前缀仅表示一套 32 位 ARM 工具链;应换成与所用 BSP 相符的实际安装前缀。配置目标、DTS 所在路径、输出目录与启动时选择 DTB 的方式均随版本变化;Linux 6.8 的主线源文件位于 arch/arm/boot/dts/allwinner/。不要根据这条命令猜测旧 BSP 的 defconfig,也不要在还没确认启动介质与布局时直接覆盖 flash。

对真实板级修改,先保存旧 DTB 和启动配置,运行 DTC 与适用的 dtbs_check,然后核对实际启动文件、运行时节点、设备目录和 probe 日志。dtbs_check 依赖完整 binding 和相应检查工具;仅 DTC 编译成功不等于 schema 或硬件通过验证。

__init 会影响函数所在节及初始化后内存回收,编译器并非忽略这个宏。可以晚加载或重新绑定的驱动,其 probe 也不能随意标成仅初始化期有效。modprobe 根据模块依赖与别名工作,不靠忽略源码注解猜出设备。

地址练习已做 DTC 编译、反编译和属性读取,检查四段资源及地址转换;板级属性片段也在上述 SoC 文件背景下编译,并读取别名、启动参数和 3.3 V 电压编码;GPIO 示例另在 Linux 6.8 的 F1C100S SoC 源文件背景下检查预处理与 DTB 编码。这些都是构建期验证,没有将示例写入板卡,也没有把虚构的 example,... 教学 binding 当作已通过上游 schema 审核的硬件接口。