Linux设备树理解和应用

设备树允许我们声明和描述系统中的不可发现设备。

设备树是一种易于阅读的硬件描述文件,其格式类似于JSON。设备树具有简单的树形结构,其中的设备由节点和属性表示。这些属性可以是空的(即仅用属性的键来描述是否为真假)​,也可以是键值对,其中值可以包含任意字节流。

  1. 空属性:"只给键,不给值" → 用 "存在 / 不存在" 表示布尔真假
    空属性的核心是:属性只有名称(键),没有任何值(值的长度为 0),它的 "值" 不是通过字节表示,而是通过 "这个属性是否出现在节点里" 来表达 "真 / 假"(启用 / 禁用)。
dts 复制代码
// 以太网设备节点
ethernet@5000 {
    compatible = "my-platform-eth";
    
    // 1. 值为整数数组(最常见):2个32位整数(地址+长度)→ 字节流是 0x50 0x00 0x00 0x00, 0x10 0x00 0x00 0x00
    reg = <0x5000 0x100>;
    
    // 2. 值为字符串:"okay" → 字节流是 'o' 'k' 'a' 'y' '\0'(末尾空字符)
    status = "okay";
    
    // 3. 值为二进制字节(MAC地址):6个字节 → 字节流是 00 11 22 33 44 55
    mac-address = [00 11 22 33 44 55];
    
    // 4. 值为混合类型(少见但允许):1个32位整数 + 字符串 + 1个字节 → 字节流是 0x01 0x00 0x00 0x00, 't' 'e' 's' 't' '\0', 0xFF
    mix-params = <0x01> "test" [FF];
};

<> 是整数数组语法:用于寄存器地址、GPIO 编号等数值型参数,编译器转成大端序字节流;

\[\] 是原始字节数组语法:用于 MAC 地址、芯片 ID 等二进制字节型参数,编译器原样存储;

设备树机制基本概念

通过将CONFIG_OF选项设置为y,便可在内核中启用对设备树的支持。要从驱动程序中获取设备树API,就必须添加以下头文件:

  • int-list-property是一个属性,其中的每个数字(或单元格)都是uint32的整数,该属性有3个单元格,就是3个uint32的整数
  • mixed-list-property是一个混合了元素类型的属性。
  • 文本字符串用双引号表示,可以用逗号创建一个字符串列表
  • 单元格由尖括号分割的32位无符号整数
  • 布尔数据是一个空属性,值的真或假取决于属性是否存在。

设备树命名约定

设备树的每一个节点都必须有一个<name>@\形式的名称。其中<name>是一个长度最多为31个字符的字符串,@\是可选的,取决于该节点是不是可寻址设备。也就是说<address>是用于访问设备的主地址。比如说对于内存映射设备对应内存区域的起始地址,I2C设备的总线设备地址和SPI设备节点的CS索引(相对于控制器)

dts 复制代码
i2c@021a0000
{
	compatible = "fsl,imx6q-i2c", "fsl, imx21-i2c";
	reg = <0x021a0000 0x4000>
	[...]
	expander@20 {
		compatible = "microchip,mcp23017";
		reg=<20>;
		[...]
	};
};

0x021a0000:I2C 控制器寄存器的起始物理地址(和节点名 @后的地址一致);

0x4000:地址长度(16KB,即 0x4000=16*1024);

设备树中的I2C控制器为内存映射设备,因此其节点名称的地址部分对应于其内存区域的起始地址(相对于SoC的内存映射)​。但扩展器为I2C设备,因此其节点名称的地址部分对应于其I2C地址。

别名、标签、phandle和路径

在设备树中,有两种方法可以引用节点,一个是通过路径,一个是通过phandle。如果要通过使用节点路径引用节点,就需要显示地在设备树源中指定节点的完整路径。phandle是与节点关联的唯一32位值,用于唯一标识节点。

设备树允许标签附加到任何节点或者属性值上面。

同时需要在设备树编译器(Device Tree Compiler,DTC)中添加逻辑,以便每当单元格属性中的标签名称以"&"符号作为前缀时,它会被替换为该标签所附加节点的phandle。用相同的逻辑,每当在单元格之外遇到以"&"符号作为前缀的标签(简单值分配)时,它就会被替换为与标签相关联的节点的完整路径。这样,phandle和路径引用便可以通过引用标签而不是显式指定phandle值或节点的完整路径来自动生成。

dts 复制代码
// 给GPIO节点加标签:gpio1(标签名可自定义,方便引用)
gpio1: gpio@0209c000 { 
    compatible = "fsl,imx6q-gpio";
    reg = <0x0209c000 0x4000>;
    // phandle会由DTC自动分配唯一32位值(比如0x123),无需手动写
};

标签和phandle

标签:gpio1(方便引用);

phandle:DTC 自动分配的 32 位唯一值(比如 0x123);

完整路径:/gpio@0209c000(节点在设备树中的全路径)。

"单元格之内" 指:属性值用 <> 包裹(单元格列表),且 &标签 出现在 <> 内部 ------ 这是设备树中 "数值型引用" 的核心场景,DTC 会把 &标签 替换为对应节点的 phandle(32 位唯一值)。

dts 复制代码
// 示例1:interrupt-parent属性(单元格内引用&gpio1)
led@1234 {
    compatible = "my-led";
    // &gpio1在<>(单元格)内 → DTC替换为gpio1的phandle(比如0x123)
    interrupt-parent = <&gpio1>; 
    // 编译后实际值:interrupt-parent = <0x123>;
};

// 示例2:gpio属性(单元格内引用&gpio1)
button@5678 {
    compatible = "my-button";
    // &gpio1在<>内 → 替换为phandle 0x123,最终:gpio = <0x123 0 0>;
    gpio = <&gpio1 0 GPIO_ACTIVE_LOW>; 
};

"单元格之外" 指:属性值不用 <> 包裹(通常是字符串,用 "" 包裹),且 &标签 出现在这些文本内容中 ------DTC 会把 &标签 替换为对应节点的完整路径字符串。

dts 复制代码
// 示例1:字符串属性中引用&gpio1(单元格之外)
custom-device@abcd {
    compatible = "my-custom-dev";
    // &gpio1在""(字符串,单元格之外)→ DTC替换为gpio1的完整路径
    node-reference = "&gpio1"; 
    // 编译后实际值:node-reference = "/gpio@0209c000";
};

// 示例2:简单值分配(无<>/"",极少用)
another-device@ef01 {
    compatible = "my-another-dev";
    // &gpio1无包裹 → 替换为完整路径
    path-ref = &gpio1; 
    // 编译后实际值:path-ref = "/gpio@0209c000";
};

标签只在设备树源格式中使用,而不会在设备树Blob(Device Tree Blod,DTB)中进行编码类似define?。在编译时,每当一个节点被打上标签,并在其他地方使用该标签进行引用时,DTC工具就会从该节点中删除该标签,并向该节点添加phandle属性,以生成并分配一个唯一的32位值。然后DTC工具将在标签被引用节点的每个单元格中使用此phandle(前缀为"&"符号)​。

设备树别名:

用短的 "别名属性名" 替代节点的超长完整路径,不用每次写/soc/i2c@021a0000这种冗长路径,且内核查节点时只需搜/aliases表,不用全局遍历设备树。

别名统一定义在设备树的 /aliases 节点下,格式是「别名属性名 = "节点完整路径"」(路径通常通过&标签生成,不用手写)。

dts 复制代码
// 1. 先给目标节点加标签(源码级引用)
i2c1_node: i2c@021a0000 {  // 标签:i2c1_node
    compatible = "fsl,imx6q-i2c";
    reg = <0x021a0000 0x4000>;
};

// 2. 定义/aliases节点(别名查找表)
aliases {
    // 别名属性名:i2c0 → 映射到&i2c1_node的完整路径(DTC自动替换&标签为路径)
    i2c0 = &i2c1_node;  
    // 再举个例子:gpio0映射到gpio@0209c000的完整路径
    gpio0 = &gpio1_node;
};

DTC 编译时,&i2c1_node 会被替换为节点的完整路径(比如/soc/i2c@021a0000);

最终编译后的 DTB 中,/aliases 节点的内容是:i2c0 = "/soc/i2c@021a0000";(别名保留,路径已生成)。

内核如何使用别名查找节点(核心规则)

内核提供of_find_node_by_path()/of_find_node_opts_by_path()函数查找节点,核心规则:

如果传入的路径不以 "/" 开头,则路径的第一个元素必须是/aliases节点中的属性名 → 内核会先查/aliases,把这个元素替换为对应的完整路径,再按新路径查找节点。

假设我们有上面的别名定义(i2c0 = "/soc/i2c@021a0000"):

场景 1:用别名查找(路径不以 / 开头)

C 复制代码
// 内核代码:传入路径"i2c0"(不以/开头)
struct device_node *node = of_find_node_by_path("i2c0");

检测到路径 "i2c0" 不以/开头 → 去/aliases节点找名为i2c0的属性;

找到i2c0 = "/soc/i2c@021a0000" → 把 "i2c0" 替换为完整路径/soc/i2c@021a0000;

按完整路径查找节点,最终定位到i2c@021a0000节点。

场景 2:不用别名(路径以 / 开头,直接查找)

C 复制代码
// 内核代码:传入完整路径(以/开头)
struct device_node *node = of_find_node_by_path("/soc/i2c@021a0000");

场景 3:别名 + 子路径(更实用)

如果节点有子设备(比如i2c@021a0000/expander@20),也可以结合别名:

C 复制代码
// 传入"i2c0/expander@20"(不以/开头)
struct device_node *node = of_find_node_by_path("i2c0/expander@20");

节点和属性的覆盖

标签还允许覆盖节点和属性。在外部引用中,任何新内容(如节点或属性)都将在编译时被附加到原始节点。但是,在重复(节点或属性)的情况下,外部引用节点的内容将优先于原始节点内容。

总之,后面的定义总是覆盖前面的定义。要覆盖整个节点,只需要像对属性那样重新定义即可。

编译结果是,status属性的值为"okay"​。

设备树源代码和编译器

设备树有两种形式:第一种是文本形式,表示源代码,也称为DTS;第二种是二进制Blob形式,表示已编译的设备树,也称为DTB(用于设备树Blob)或FDT(用于扁平设备树)​。源文件的扩展名是.dts,二进制文件的扩展名是.dtb或.dtbo。.dtbo是一个特定的扩展名,用于已编译的设备树叠加层。另外,还有.dtsi文本文件(其中末尾的i表示"inculde"​)​。这些文件包含SoC级别的定义,位于定义在主机板级别的.dts文件中。

设备树的语法允许用户使用/include/或#include来包含其他文件。这种包含机制也允许使用#define,但最重要的是,它允许将多个平台的共同方面抽象化并放在共享文件中。

这种抽象化让我们可以将源文件分为3个级别,其中最常见的是SoC级别,由SoC供应商(如NXP)提供;第2个级别是系统模块(System on Module,SoM)级别,如Engicam;最后一个级别是载板或客户板级别。

因此,使用相同SoC的所有电子板都不会重新定义SoC的所有外设:这种描述被分解到一个通用文件中。按照惯例,这样的通用文件使用.dtsi扩展名,而最终的设备树使用.dts扩展名。

dts 复制代码
// 1. DTSI文件:imx6q.dtsi(SoC通用定义)
#define IMX6Q_I2C1_BASE 0x021a0000  // 宏定义(类似C的#define)
i2c1: i2c@IMX6Q_I2C1_BASE {  // 用宏定义地址,通用配置
    compatible = "fsl,imx6q-i2c";
    reg = <IMX6Q_I2C1_BASE 0x4000>;
};

// 2. DTS文件:imx6q-myboard.dts(板级定制)
#include "imx6q.dtsi"  // 包含SoC通用定义(类似C的#include)
/ {  // 根节点
    model = "MyBoard based on IMX6Q";
    compatible = "myboard,imx6q";
    
    // 只定义板子特有的I2C子设备(通用的I2C控制器已在dtsi中定义)
    &i2c1 {  // 引用dtsi中的i2c1节点(标签)
        expander@20 {  // 这款板子特有的I/O扩展器
            compatible = "microchip,mcp23017";
            reg = <0x20>;
        };
    };
};

在Linux内核源代码中,ARM设备树源文件分别位于32位和64位ARM SoC /单板的arch/arm/boot/dts/和arch/arm64/boot/dts//目录下。这两个目录下都有一个Makefile,其中列出了可以编译的设备树源文件。

DTC 是源码形式存在的,需要先编译成可执行文件才能用,内核提供了两种构建方式:

自动构建(推荐):当你执行 "编译.dtb" 的命令时,内核构建系统会先检查是否有现成的 dtc 可执行文件;如果没有,会自动把scripts/DTC/下的源码编译成 dtc 工具(作为编译.dtb 的 "依赖项"),无需手动操作;

手动显式构建:如果想提前把 DTC 编译好(比如调试工具、提前准备环境),可以在内核源码主目录执行命令:

bash 复制代码
make scripts

这个命令会编译内核所有的脚本工具(包括 DTC),生成的 dtc 可执行文件会放在scripts/dtc/目录下。

编译.dtb 的前提:启用内核配置选项

编译设备树前,必须确保内核配置中开启了 "设备树支持"(否则内核不知道要编译.dtb,会报错):

配置方式:在内核源码主目录执行make menuconfig(图形化配置),找到对应架构的设备树选项(比如 ARM 架构):

核心选项:CONFIG_OF=y(启用扁平设备树支持,必开);

具体 SoC 选项:比如CONFIG_ARCH_IMX6=y(启用 IMX6 系列 SoC 的设备树支持);

简化操作:如果是编译某款开发板的内核,通常厂家提供的默认配置(比如imx6q_defconfig)已经包含了这些选项,无需手动改。

编译.dtb 的两种核心命令(内核源码主目录执行)

假设你的内核是 ARM 架构,要编译的.dts 文件在arch/arm/boot/dts/目录下(不同架构路径略有差异,比如 RISC-V 是arch/riscv/boot/dts/):

(1)编译单个设备树(精准编译某一个.dts→.dtb)

核心规则:把.dts 文件名的后缀.dts改成.dtb,作为make的目标,且要写全路径;

示例:编译imx6q-myboard.dts→imx6q-myboard.dtb:

bash 复制代码
make arch/arm/boot/dts/imx6q-myboard.dtb

结果:编译后的imx6q-myboard.dtb会生成在arch/arm/boot/dts/目录下。

编译所有已启用的设备树(批量编译)

核心命令:直接用dtbs作为make的目标,内核会遍历所有 "配置中启用的 SoC" 对应的.dts 文件,全部编译成.dtb;

示例:

bash 复制代码
make dtbs

结果:所有启用的.dtb 都会生成在arch/arm/boot/dts/目录下(比如 imx6q-myboard.dtb、imx6q-otherboard.dtb 等)。

以下内容来自arch/arm/boot/dts/Makefile:

Makefile 复制代码
dtb-$(CONFIG_SOC_IMX6Q) +=	\
	imx6dl-alti6p.dtb	\
	imx6dl-aristainetos_y.dtb	\
	[...]
	imx6q-hummingboard.dtb	\
	imx6q-hummingboard2.dtb	\
	imx6q-hummingboard2-emmc-som-v15.dtb	\
	imx6q-hummingboard2-som-v15.dtb	\
	imx6q-icore.dtb	\
	[...]

通过启用CONFIG_SOC_IMX6Q,可以编译其中列出的所有设备树文件,也可以针对特定的设备树文件进行编译。通过运行make dtbs,内核DTC将编译已启用的配置选项中列出的所有设备树文件。

然后,编译所有的设备树文件:

bash 复制代码
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs

对于ARM64平台,命令如下:

bash 复制代码
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make dtbs

同样,必须设置正确的内核配置选项。针对特定的设备树构建(如imx6q-hummingboard2.dts),可以使用如下命令:

bash 复制代码
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6q-hummingboard2.dtb

设备树反向解析

内核配置:CONFIG_PROC_DEVICETREE(暴露活动设备树的开关)

作用:开启这个内核配置选项后,内核会把当前系统 "正在使用的设备树(活动设备树)" 以文件系统目录的形式暴露到/proc/device-tree下;

本质:/proc是内核向用户态暴露运行时信息的 "虚拟文件系统",/proc/device-tree就是活动设备树的 "文件化快照"(每个节点对应一个目录,每个属性对应一个文件);

开启方式:编译内核时,通过make menuconfig找到 "General setup → Configure standard kernel features → Enable access to /proc/device-tree"(不同内核版本路径略有差异),设置为y,然后重新编译内核。

复制代码
# 查看活动设备树的根节点内容
ls /proc/device-tree/
# 输出示例:compatible  model  aliases  soc  i2c@021a0000  ...

# 查看某个节点的属性(比如根节点的compatible)
cat /proc/device-tree/compatible
# 输出示例:myboard,imx6q (对应设备树里的compatible = "myboard,imx6q")

用 DTC 把 /proc/device-tree 转成可读的.dts

如果系统里安装了 DTC 工具(内核源码scripts/dtc/下的 dtc 可执行文件,或系统包管理安装的 dtc),执行以下命令即可转换:

bash 复制代码
# 核心命令:从/proc/device-tree(文件系统格式)转成dts文本,输出到MySBC.dts
dtc -I fs -O dts /proc/device-tree -o MySBC.dts

执行结果:

生成的MySBC.dts文件包含当前系统实际运行的设备树配置(而非源码里的.dts),格式和标准设备树源码完全一致,能直接阅读、对比、调试。

额外补充:直接从 dtb 文件反向解析 dts

如果手里有现成的.dtb 文件(比如从开发板固件里提取的imx6q-myboard.dtb),也能直接转 dts,命令更简单:

bash 复制代码
dtc -I dtb -O dts imx6q-myboard.dtb -o imx6q-myboard-extract.dts

设备树叠加层

设备树叠加层是一种机制,这种机制允许修改活动的设备树,也就是在运行时修改当前的设备树。它允许用户通过更新现有节点和属性或者创建新节点和属性来更新当前设备树。但是,它不允许删除节点或属性。

dts 复制代码
/dts-v1/;
/plugin/;

/{
	fragment@0 {
		target=<phandle>;
	or
		target-path="/path";
		
		__overlay__ {
			property-a;
			property-b = <0x80>;
			node-a {
				...
			};
		};
	};
	
	fragment@1 {
	
	};
}

从中可以发现,需要叠加的基本设备树中的每个节点都必须包含在叠加层设备树的片段节点中。每个片段有两个元素。

· target-path或target。

❏ target-path:指定片段将修改节点的绝对路径。

❏ target:指定片段将修改节点别名(前缀为"&"符号)的相对路径。

· 一个名为__overlay__的节点,它包含应用于引用节点的更改。这样的更改可以是新节点被添加、新属性被添加或现有属性值被新属性值覆盖。由于不能删除属性或节点,因此不可能执行删除操作。

dts 复制代码
// 叠加层必须的头部标记:声明是叠加层(plugin)
/dts-v1/;
/plugin/;  // 关键:标记这是叠加层,不是完整设备树

/ {  // 根节点
    // 片段0:要修改的第一个目标节点(比如I2C控制器节点)
    fragment@0 {
        // 方式1:用target指定目标节点(推荐,用&标签,简洁)
        target = <&i2c1>;  // &i2c1是基础DTB中I2C节点的标签,DTC会转成phandle
        
        // 方式2:用target-path指定(绝对路径,等价于上面的target)
        // target-path = "/soc/i2c@021a0000";
        
        // __overlay__:要对目标节点做的修改(核心)
        __overlay__ {
            // 1. 新增空属性(表示启用)
            i2c-fast-mode;  
            // 2. 修改已有属性的值(覆盖基础DTB的100000为400000)
            clock-frequency = <400000>;  
            // 3. 新增子节点(给I2C加一个新的传感器设备)
            sensor@48 {
                compatible = "ti,adc128d818";
                reg = <0x48>;
            };
        };
    };

    // 片段1:可加多个片段,修改另一个目标节点(比如LED节点)
    fragment@1 {
        target = <&led1>;
        __overlay__ {
            // 修改LED的GPIO极性(覆盖原有值)
            gpio = <&gpio1 5 GPIO_ACTIVE_HIGH>;
        };
    };
};

(1)头部标记:必须的 "身份标识"

/dts-v1/;:声明设备树版本(所有 DTS 都要加);
/plugin/;:核心标记------ 告诉 DTC"这是叠加层,不是完整设备树",编译后生成.dtbo(不是.dtb),内核才能识别为叠加层加载。

(2)fragment@N:"修改片段"(每段改一个目标节点)

fragment@0/fragment@1:每个 fragment 对应 "一次修改操作",@0/@1是片段的编号(唯一即可,从 0 开始);

每个 fragment 只做一件事:指定要修改的目标节点 + 定义要做的修改。

(3)target /target-path:指定 "要改哪个节点"(二选一)

(4)__overlay__节点:定义 "具体要改什么"(核心操作)

__overlay__是叠加层的 "操作指令集",里面的所有内容都会应用到 target/target-path 指定的目标节点上,支持两种操作:

新增内容:

新增属性(如空属性i2c-fast-mode、键值对属性);

新增子节点(如给 I2C 加sensor@48子设备);

修改已有内容:

覆盖现有属性的值(如把clock-frequency从 100K 改成 400K);

⚠️ 核心限制:不能删除任何节点 / 属性------ 比如基础 DTB 中有old-property属性,叠加层无法删掉它,只能修改它的值,或新增其他属性。

叠加层的使用流程(补充理解)

编写叠加层 DTS(如i2c-sensor-overlay.dts);

编译成 DTBO:dtc -I dts -O dtbo -o i2c-sensor-overlay.dtbo i2c-sensor-overlay.dts;

运行时加载:通过内核接口(如echo i2c-sensor-overlay.dtbo > /sys/kernel/config/device-tree/overlays/myoverlay/path)加载,加载后活动设备树立即生效修改。

除非设备树叠加层仅在根节点下添加新节点(在这种情况下,可以在片段中指定"/"为目标路径属性)​,否则最好通过phandle(<&label_name>)来指定目标节点,因为这可以缩短手动计算节点的完整路径。基本设备树和设备树叠加层之间没有直接的关联或联系,它们都是独立构建的。因此,从设备树叠加层引用远程节点(即基本设备树中的节点)将引发错误,设备树叠加层的构建将因为未定义的引用或标签而失败。就像构建一个没有符号解析空间的动态链接应用程序一样。为了解决这个问题,DTC提供了-@选项,你必须为基本设备树和要编译的所有设备树叠加层指定此选项。它将指示DTC在根目录中生成额外的节点(例如__symbols__、__fixups__和__local_fixups__节点)​,这些节点包含用于phandle名称翻译的解析数据。这些额外的节点分布如下。· 当添加-@选项来构建设备树叠加层时,它会识别标记设备树片段/对象的/plugin/;行。这一行控制了__fixups__和__local_fixups__节点的生成。

· 当添加-@选项来构建基本设备树时,/plugin/;行不存在,因此源头被识别为基本设备树,这导致只生成__symbols__节点。这些额外的节点增大了符号解析的空间。

-@选项只能在DTC的1.4.4或更高版本中找到。只有4.14或更高版本的Linux内核包含满足此要求的内置DTC版本。如果只在设备树叠加层中使用目标路径属性,则不需要使用-@选项。

用-@编译的完整流程(示例)

假设要编译基础 DTB 和叠加层,解决&i2c1的引用问题:

步骤 1:编译基础 DTB(加-@)
bash 复制代码
#   编译基础DTB,生成__symbols__节点(存储i2c1等标签的映射)
dtc -I dts -O dtb -@ -o imx6q-myboard.dtb imx6q-myboard.dts

编译后的imx6q-myboard.dtb会包含__symbols__节点,比如:

dts 复制代码
__symbols__ {
    i2c1 = "/soc/i2c@021a0000";  // 标签i2c1映射到绝对路径
};
步骤 2:编译叠加层 DTBO(加-@)
bash 复制代码
# 编译叠加层,生成__fixups__和__local_fixups__节点
dtc -I dts -O dtbo -@ -o sensor-overlay.dtbo sensor-overlay.dts

叠加层里的target = <&i2c1>会被 DTC 识别为 "需要解析的标签",生成:

dts 复制代码
__fixups__ {
    i2c1 = "";  // 标记i2c1是需要从基础DTB解析的标签
};
__local_fixups__ {
    fragment@0 {
        target = <0>;  // 标记target位置需要替换为i2c1的phandle
    };
};
步骤 3:内核加载叠加层时的 "符号解析"

内核加载 DTBO 时,会:

从基础 DTB 的__symbols__节点找到i2c1对应的 phandle;

替换叠加层__local_fixups__标记的位置(把<0>换成实际 phandle);

完成叠加层对基础 DTB 节点的精准引用,不会报错。

-@选项的使用规则(关键)

DTC 版本≥1.4.4(内核 4.14 + 内置的 DTC 满足,老版本内核需要手动升级 DTC);

低于该版本的 DTC 不支持-@选项,只能用target-path(绝对路径)替代target=<&label>。

-@选项需要同时给基础 DTB 和叠加层的编译命令加(只加一个没用);

这些额外节点(__symbols__等)不会影响设备树的正常功能,仅用于内核加载叠加层时的符号解析,解析完成后会被内核忽略。

在Yocto构建系统中,可以将-@选项添加到机器配置中,或者在开发过程中添加到local.conf文件中,如下所示。

对于Linux内核,要构建设备树叠加层,则应将其添加到SoC架构的Makefile设备树中,例如arch/arm64/boot/dts/freescale/Makefile或arm/arm/boot/dts/Makefile,以dtbo作为扩展名,如下所示。

通过configfs加载设备树叠加层

configfs:Linux 内核提供的「配置文件系统」(虚拟文件系统,类似 sysfs/proc),核心作用是用户态通过文件操作(创建目录 / 写文件)向内核传递配置,这里用来管理设备树叠加层的加载 / 卸载。

设备树叠加层(DTBO):运行时给现有设备树 "打补丁" 的文件,仅增 / 改节点 / 属性,不删。

核心前提:所有操作都是针对「已启动、根文件系统已挂载」的运行中系统(不用重启、不用重新编译内核)。

前提条件 ------ 内核必须开启两个配置(关键!)

CONFIG_OF_OVERLAY=y:启用设备树叠加层功能(内核能解析 DTBO 并修改活动设备树);

CONFIG_CONFIG_FS=y:启用 configfs 文件系统(用户态能通过 configfs 操作内核配置)。

为了实现这一点,内核在编译时必须启用CONFIG_OF_OVERLAY和CONFIG_CONFIG_FS选项以使之后的步骤生效。假设内核配置文件已经存在于目标机中,则可以通过输入以下命令对此进行检查:

bash 复制代码
# 方法1:查看内核.config文件(如果目标机有)
grep -E "CONFIG_OF_OVERLAY|CONFIG_CONFIG_FS" /usr/src/linux/.config

# 方法2:查看运行时内核配置(更直接)
cat /proc/config.gz | gunzip | grep -E "CONFIG_OF_OVERLAY|CONFIG_CONFIG_FS"

输出如果是CONFIG_OF_OVERLAY=y和CONFIG_CONFIG_FS=y,说明满足条件;

如果是=m(模块)或# CONFIG_XXX is not set,则不满足,需要重新编译内核开启这两个选项。

步骤 1:挂载 configfs(如果未挂载)

下面使用configfs将设备树二进制文件插入正在运行的内核中。如果系统尚未挂载configfs,则需要先挂载它:

bash 复制代码
mount -t configfs none /sys/kernel/config

mount: Linux的挂载命令

-t configfs: 指定文件系统类型为configfs(配置文件系统)

none: 表示没有实际的物理设备(因为是虚拟文件系统)

/sys/kernel/config: 挂载的目标目录

确认 overlays 目录存在(验证挂载成功)

挂载 configfs 后,内核会自动创建设备树叠加层的专属目录:

bash 复制代码
# 检查目录是否存在(核心操作目录)
ls /sys/kernel/config/device-tree/overlays/

若能看到这个目录,说明 configfs 挂载成功且内核支持叠加层;

这个目录是管理所有叠加层的 "总入口",每个叠加层对应一个子目录。

创建叠加层专属目录(比如命名为 foo)

每个 DTBO 需要创建一个独立目录(方便管理,比如多个叠加层可分别创建):

bash 复制代码
# 在overlays目录下创建foo目录(名字自定义,比如sensor、led都可以)
mkdir /sys/kernel/config/device-tree/overlays/foo

作用:内核会把这个目录和后续加载的 DTBO 绑定,后续操作都在 foo 目录下进行。

加载 DTBO 文件(两种方式,选其一即可)

这一步是核心 ------ 把编译好的 DTBO 文件传给内核,让内核解析并应用到活动设备树。

方式 1:通过 "路径" 加载(推荐,简单)

bash 复制代码
# 假设DTBO文件在/lib/firmware/foo-overlay.dtbo,把路径写入foo目录下的path文件
echo /lib/firmware/foo-overlay.dtbo > /sys/kernel/config/device-tree/overlays/foo/path

作用:内核读取 path 文件中的 DTBO 路径,加载并解析该文件,自动把叠加层的修改应用到当前设备树。

方式 2:直接写入 DTBO 内容(适合 DTBO 不在文件系统的场景,比如临时生成)

bash 复制代码
# 把DTBO文件的二进制内容直接写入foo目录下的.dtbo文件
cat /tmp/foo-overlay.dtbo > /sys/kernel/config/device-tree/overlays/foo/.dtbo

效果和方式 1 完全一致,只是传递 DTBO 的方式不同(一个传路径,一个传二进制内容)。

验证叠加层生效

加载后,内核会立即把叠加层的修改(新增节点 / 修改属性)应用到活动设备树:

bash 复制代码
# 查看/proc/device-tree(活动设备树),确认新增/修改的节点/属性存在
ls /proc/device-tree/新增节点名
cat /proc/device-tree/修改的属性名
删除叠加层(撤销修改)
bash 复制代码
rmdir /sys/kernel/config/device-tree/overlays/foo

注意:必须用rmdir(删除空目录),不能用rm -rf,因为目录里的文件是内核虚拟的,删除目录就会触发内核撤销叠加层。

表示和寻址设备

在设备树中,至少一个节点代表一个设备,设备节点可以填充其他节点也可以填充属性。

每个设备都可以独立运行,但在某些情况下,设备可能希望被其父设备访问,或者父设备可能想要访问其中一个子设备。例如,当总线控制器(父节点)想要访问位于其总线上的一个或多个设备(声明为子节点)时,就会出现这种情况。典型的示例包括I2C控制器和I2C设备、SPI控制器和SPI设备、CPU和内存映射设备等。因此,设备寻址的概念应运而生。设备寻址因reg属性而引入,该属性在每个可寻址设备中都会用到,但reg属性的具体含义或解释取决于父设备(大多数情况下是总线控制器)​。在子设备中,reg属性的含义和解释取决于其父设备的#address-cells和#size-cells属性。

每个可寻址设备都有一个reg属性,它是一个元组列表,形式为reg=<address0 size0address1 size1 address2 size2 ...>,其中每个元组表示设备使用的地址范围。#size-cells用于表示有多少个32位单元格来表示大小,如果大小不相关,则可能为0。#address-cells用于表示有多少个32位单元格来表示地址。也就是说,每个元组中的地址元素根据#address-cells进行解释;#address-cells与#size-cells大小相同,而每个元组中的空间大小是根据#size-cells进行解释的。总之,可寻址设备从它们的父节点继承#size-cell和#address-cell属性,而父节点通常表示总线控制器。如果设备本身具有#size-cell和#address-cell属性,这并不会影响该设备本身的功能。

dts 复制代码
// 父节点:I2C控制器(总线管理员)
i2c1: i2c@021a0000 {
    compatible = "fsl,imx6q-i2c";
    // 控制器自己的reg:由它的父节点(SOC)解析(SOC的#address-cells=1,#size-cells=1)
    reg = <0x021a0000 0x4000>;  // 地址=0x021a0000(1个单元格),大小=0x4000(1个单元格)
    
    #address-cells = <1>;  // 告诉子设备:地址用1个32位单元格表示
    #size-cells = <0>;     // 告诉子设备:大小无关,用0个单元格(I2C设备只有7位地址,无大小)
};

// 子节点:I2C传感器(总线租户)
&i2c1 {  // 引用父节点I2C控制器
    sensor@48 {
        compatible = "ti,adc128d818";
        reg = <0x48>;  // 按父节点规则解析:仅地址=0x48(1个单元格),无大小(0个单元格)
    };
};
dts 复制代码
// 父节点:SOC根节点(内存总线管理员)
/ {
    #address-cells = <1>;  // 地址用1个32位单元格
    #size-cells = <1>;     // 大小用1个32位单元格
    
    soc {
        // 子节点:GPIO控制器(内存映射设备)
        gpio1: gpio@0209c000 {
            compatible = "fsl,imx6q-gpio";
            // 按父节点规则解析:地址=0x0209c000(1个单元格),大小=0x4000(1个单元格)
            reg = <0x0209c000 0x4000>;  
            
            // 以下规则是给GPIO控制器的子设备(如GPIO引脚)用的,和自身无关!
            #address-cells = <1>;  
            #size-cells = <0>;     
        };
    };
};

设备自己定义的#address-cells/#size-cells,只作用于它的子设备,不影响自身reg的解析;

自身reg的解析规则,永远由它的父节点的#address-cells/#size-cells决定;

比如上面的 GPIO 控制器,它自己的#address-cells=1是给 GPIO 引脚用的,而它自身的reg是由 SOC 根节点的规则解析的。

处理SPI和I2C设备寻址

SPI和I2C设备都属于非内存映射设备,需要通过父设备驱动程序即总线控制器驱动程序代表CPU执行间接访问。每个I2C、SPI设备节点总是表示为其所在的I2C/SPI控制器节点的子节点。对于非内存映射设备,#size-cells属性为0,寻址元组中的大小元素为空。这意味着这种设备的reg属性始终为一个单元格。

dts 复制代码
&i2c3 {
	[...]
	status = "okay";
	temperature-sensor@49 {
		compatible = "national, lm73";
		reg = <0x49>
	};
	pcf8523: rtc@68 {
		compatible = "nxp,pcf8523";
		reg = <0x68>
	};
};

&ecspi1 {
	fsl,spi-num-chipselects = <3>;
	cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>;
	status = "okay";
	[...]
	
	ad7606r8_0: ad7606r8@1 {
		compatible = "ad7606-8";
		reg = <1>;
		spi-max-frequency = <1000000>;
		interrupt-parent = <&gpio4>;
		interrupts = <30 0x0>;
		convst-gpio = <&gpio6 18 0>;	
	};
};

如果查看arch/arm/boot/dts/imx6qdl.dtsi中的SoC级别文件,就会注意到#size-cells和#address-cells分别在I2C和SPI控制器节点(标记为i2c3和ecspi1)中设置为0和1。这有助于你理解它们的reg属性,该属性仅为地址值设置了一个单元格,而对于大小值则未进行设置。

I2C设备的reg属性用于指定设备在总线上的地址。对于SPI设备,reg表示分配给该设备的片选线在控制器节点的片选列表中的索引。例如,对于ad7606r8 ADC,片选索引为1,对应于cs-gpios中的<&gpio5 17 0>。这是控制器节点的片选列表。其他控制器的绑定可能不同,可以参考位于Documentation/devicetree/bindings/spi目录中的文档。

设备树中逗号的通用使用场景

总结:只要一个属性需要表示 "多个同类的配置项(多组 <...>)",就用逗号分隔,常见场景包括:

多 GPIO 配置:如cs-gpios、leds-gpios、keys-gpios等;

多中断配置:如interrupts = <1 0>, <2 1>;(设备有多个中断源);

多地址配置:如reg = <0x1000 0x100>, <0x2000 0x200>;(设备占用多个地址范围);

多节点引用:如clocks = <&clk1>, <&clk2>;(设备使用多个时钟)。

interrupt-parent = <&gpio4>; ------ 指定中断父控制器

作用:声明当前设备(AD7606R8)的中断信号,由哪个 "中断控制器" 管理;

<&gpio4>:引用设备树中gpio4这个 GPIO 控制器节点的 phandle(唯一标识);

硬件含义:AD7606R8 的中断输出引脚,连接到gpio4控制器管理的 GPIO 引脚上(具体引脚号由interrupts指定);

补充:GPIO 控制器本质也是一种中断控制器(每个 GPIO 引脚可配置为中断源),因此这里把gpio4作为中断父控制器。

  1. interrupts = <30 0x0>; ------ 指定具体中断配置

作用:在interrupt-parent指定的控制器(gpio4)下,声明具体的中断引脚和触发方式;

格式解析:<30 0x0>是两个 32 位单元格(由gpio4节点的#interrupt-cells = <2>决定,这是 GPIO 控制器的通用规则):

第一个值30:gpio4控制器下的引脚编号(即 GPIO4_30 引脚);

第二个值0x0:中断触发方式(不同芯片略有差异,通用规则):

0x0:低电平触发(默认);

0x1:高电平触发;

0x2:下降沿触发;

0x3:上升沿触发;

0x4:双边沿触发;

硬件含义:AD7606R8 的中断引脚连接到 GPIO4_30,中断触发方式为低电平触发。

convst-gpio = <&gpio6 18 0>; ------ 指定 CONVST 引脚的 GPIO 配置

背景:AD7606R8 是模数转换芯片(ADC),CONVST是 "转换启动" 引脚 ------ 向该引脚发信号,芯片才会开始模拟信号到数字信号的转换;

格式解析:<&gpio6 18 0>是三个 32 位单元格(GPIO 配置的通用格式):

&gpio6:引用 gpio6 控制器节点的 phandle;

18:gpio6 控制器下的引脚编号(GPIO6_18);

0:GPIO 的标志位(通用规则):

0:默认配置(如推挽输出、无上下拉);

1:GPIO_ACTIVE_HIGH(高电平有效);

2:GPIO_ACTIVE_LOW(低电平有效);

8:GPIO_PULL_UP(上拉);

0x10:GPIO_PULL_DOWN(下拉);

硬件含义:AD7606R8 的 CONVST(转换启动)引脚,连接到 GPIO6_18 引脚,采用默认配置(0)。

内存映射设备和设备寻址

内存映射设备,其中内存区域可由CPU访问。对于此类设备节点,reg属性仍然定义设备的地址,并且采用reg = <address0 size0 address1 size1address2 size2 ...>的形式。每个内存区域由一个元组表示,其中第一个元素是内存区域的基地址,第二个元素是内存区域的大小。它可以转换为以下形式:reg = <base0length0 base1 length1 address2 length2 ...>。在这里,每个元组表示设备使用的地址范围。

处理资源

resource结构体

· start:根据资源标志位的不同,start可能是内存区域的起始地址、中断线编号、DMA通道编号或寄存器偏移量。

· end:这是内存区域或寄存器偏移量的结束位置。对于IRQ或DMA通道,大多数情况下,start和end的值相同。

· name:如果有的话,是资源的名称。

· flags:表示资源的类型,可能的值如下。

❏ IORESOURCE_IO:用于PCI/ISA I/O端口区域。start是该区域的第一个端口,end是最后一个端口。
❏ IORESOURCE_MEM:用于I/O内存区域。start表示该区域的起始地址,end表示结束地址。
❏ IORESOURCE_REG:寄存器偏移量,主要用于MFD(Multi-Function Device,多功能设备)​。start表示相对于父设备寄存器的偏移量,end表示寄存器区段的结束位置。
❏ IORESOURCE_IRQ:IRQ中断线编号。在这种情况下,要么start和end可能具有相同的值,要么end是无关紧要的。
❏ IORESOURCE_DMA:对于DMA通道标识符,end的使用方式与IRQ相同。然而,当有多个单元格用于DMA通道标识符或者当有多个DMA控制器时,你将不太明确会发生什么。IORESOURCE_DMA对于多个控制器系统并不具有可扩展性。

每个资源都会被分配一个resource结构体实例。这意味着对于一个被分配了两个内存区域和一个IRQ中断线的设备,将会分配3个resource结构体实例。此外,同一类型的资源将按照它们在设备树文件(或板级文件)中声明的顺序进行分配和索引(从0开始)​。这意味着分配的第一个内存区域的索引是0,依此类推。

资源是怎么划分的?

一个资源类型 = 一个 resource 实例:设备有几种不同类型的资源(比如 2 个内存区域 + 1 个 IRQ),就分配几个resource结构体(共 3 个);

同类型资源按声明顺序索引:同一类型的多个资源(比如 2 个内存区域),按在设备树 / 板级文件中写的顺序,索引从 0 开始(第一个内存区索引 0,第二个索引 1)。

dts 复制代码
my_device@02000000 {
    reg = <0x02000000 0x1000>, <0x02010000 0x2000>;  // 2个内存区域
    interrupts = <50 0x0>;  // 1个IRQ
};

内核会分配 3 个resource实例:

实例 1:flags=IORESOURCE_MEM,start=0x02000000,end=0x02000fff(索引 0);

实例 2:flags=IORESOURCE_MEM,start=0x02010000,end=0x02011fff(索引 1);

实例 3:flags=IORESOURCE_IRQ,start=50,end=50(索引 0)。

什么是PCI/ISA

ISA 总线:非常老旧的总线(80 年代),速度慢,主要用于老款串口、并口、声卡,现在嵌入式几乎不用;

PCI 总线:替代 ISA 的总线(90 年代至今),速度快,用于 PCIe(PCI Express,嵌入式中常见于网卡、SSD、扩展卡);

什么是IO端口区域

核心定义:CPU 专门划分的一块「独立地址空间」(和物理内存地址不重叠),用于访问 PCI/ISA 外设的寄存器,比如串口的 0x3f8-0x3ff、网卡的 0x200-0x20f;

特点:

地址范围小(通常 0x0000-0xFFFF),只能通过专用指令访问(x86 的in/out指令,ARM 无专用指令,模拟实现);

resource中:start = 第一个端口号,end = 最后一个端口号(比如串口 0x3f8-0x3ff,start=0x3f8,end=0x3ff);

嵌入式场景:ARM/MIPS 等嵌入式 CPU 几乎不用 I/O 端口,全用 I/O 内存,所以IORESOURCE_IO在嵌入式中极少见到(主要 x86 用)。

什么是IO内存区域

核心定义:把外设的寄存器 / 内存映射到 CPU 的物理内存地址空间(和 CPU 访问 DDR 的地址同属一个空间),是嵌入式系统的主流方式;

特点:

地址范围大(比如 ARM 的 0x00000000-0xFFFFFFFF),CPU 用普通的内存读写指令(ldr/str)访问;

resource中:start = 物理内存起始地址,end = 结束地址(比如你熟悉的 I2C 控制器reg = <0x021a0000 0x4000>,start=0x021a0000,end=0x021a3fff);

例子:GPIO 控制器、I2C/SPI 控制器、UART 等嵌入式外设,全是IORESOURCE_MEM类型。

什么是MFD

核心定义:一个物理芯片集成了多个独立功能模块(比如一颗芯片同时包含 GPIO、串口、ADC、PMIC),这类设备叫 MFD;

IORESOURCE_REG 的作用:MFD 的子功能模块(比如 ADC)的寄存器地址,不是 "物理内存地址",而是相对于 MFD 父设备基地址的偏移量;

dts 复制代码
// MFD父设备(物理地址0x02000000)
mfd_chip@02000000 {
    reg = <0x02000000 0x10000>;  // IORESOURCE_MEM:物理地址
    #address-cells = <1>;
    #size-cells = <1>;
    
    // MFD子功能:ADC模块(寄存器偏移)
    adc@100 {
        reg = <0x100 0x200>;  // IORESOURCE_REG:偏移0x100,大小0x200
    };
};

父设备的reg是物理地址(IORESOURCE_MEM),子模块的reg是相对于父设备的偏移(IORESOURCE_REG),内核会计算出 ADC 的实际物理地址 = 0x02000000+0x100=0x02000100。

DMA通道标识符

先懂 DMA:DMA(直接内存访问) 是让外设不经过 CPU,直接读写内存的技术(比如 SPI 设备直接把数据写到 DDR,不用 CPU 中转)。

DMA 控制器:管理 DMA 操作的硬件模块(嵌入式 SoC 通常有多个 DMA 控制器,比如 DMA1、DMA2);

DMA 通道:DMA 控制器给每个外设分配的 "专属编号"(比如 DMA1 有 8 个通道:0-7),外设要使用 DMA,必须指定通道号;

IORESOURCE_DMA:resource的 flags 标记该资源是 DMA 通道标识符,此时 start=end = 通道号(比如通道 3,start=3,end=3)。

设备树中,DMA 通道的标识可能需要多个 32 位单元格(不止一个数字),比如:

简单场景:只有 1 个 DMA 控制器,通道号 = 3 → dma = <3>(1 个单元格);

复杂场景:有 2 个 DMA 控制器(DMA1、DMA2),通道号需要 "控制器编号 + 通道号" → dma = <1 3>(2 个单元格:1=DMA2 控制器,3 = 通道 3)。

resource的start/end是单一数值(32 位整数),只能存一个数字 ------ 如果 DMA 通道需要 2 个单元格(比如 1 3),start只能存其中一个(比如 3),但内核无法知道 "3 是 DMA1 的通道 3,还是 DMA2 的通道 3",这就是 "不太明确会发生什么"。

嵌入式 SoC(比如 IMX6/IMX8)通常有多个 DMA 控制器(比如 DMA1、DMA2、SDMA),每个控制器有自己的通道号(0-7):

用IORESOURCE_DMA标记的 resource,只能记录 "通道号 3",但无法记录 "属于哪个 DMA 控制器";

内核驱动拿到start=3后,不知道该用 DMA1 还是 DMA2,必须额外加逻辑(比如硬编码),无法适配 "新增 DMA 控制器" 的场景 ------ 这就是 "IORESOURCE_DMA 对于多个控制器系统并不具有可扩展性"。

获取资源

为了获取相应的资源,我们可以使用通用的API,即platform_get_resource()。该函数需要提供资源类型和资源类型中的索引。该函数定义如下:

platform_get_resource()

在上面的函数原型中,dev是我们为其编写驱动程序的平台设备,type是资源类型,num是同一资源类型中资源的索引。如果成功,该函数将返回一个指向resource结构体的有效指针,否则返回NULL。

platform_get_resource_byname()

当同一类型的资源有多个时,使用索引可能会产生歧义。为此,你可以选择使用资源名称作为替代方案,并且引入platform_get_resource()的变体函数platform_get_resource_ byname()。给定资源标志(或类型)和资源名称,该变体函数将返回对应的资源,而无论它们在声明中的顺序如何。

命名资源的概念

当驱动程序期望获得某种类型的资源列表时(比如两个IRQ中断线,其中一个用于Tx,另一个用于Rx)​,由于不能保证资源列表的顺序,因此驱动程序不应该做出任何假设。如果驱动程序逻辑被硬编码为期望先得到Rx IRQ,但设备树中已经用Tx IRQ填充了,则会发生不匹配异常。为了避免这种不匹配,我们引入了命名资源(如时钟、IRQ、DMA通道和内存区域)的概念。这包括定义资源列表并对其命名,这样无论它们的索引是什么,给定的名称都将与资源匹配。命名资源的概念还使得设备树资源的分配易于阅读和理解。

reg-names:用于在reg属性中为内存区域指定名称列表

interrupt-names:用于在interrupts属性中为每个中断线指定名称

dma-names:用于dma属性

clock-names:用于为clocks属性中的时钟指定名称。

dts 复制代码
fake_device {
	compatible = "packt, fake-device";
	reg = <0x4a064000 0x800>,
			<0x4a064800 0x200>,
			<0x4a064c00 0x200>;
	reg-names = "ohci", "ehci", "config";
	interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>,
					<0 67 IRQ_TYPE_LEVEL_HIGH>;
	interrupt-names = "ohci", "ehci";
};

在上面的示例中,我们为设备分配了3个内存区域和两个中断线。资源名称列表和资源之间是一对一的映射关系。这意味着索引为0的名称将分配给相同索引处的资源。在驱动程序中提取每个命名资源的代码如下:

C 复制代码
// 1. 修正变量声明:所有resource变量为指针,中断变量命名语义化
struct resource *res_meml, *res_mem_config, *res_irq_ohci, *res_irq_ehci;
int ohci_irq, ehci_irq;

// 2. 正确获取内存资源(原逻辑正确,仅变量声明修正)
res_meml = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ohci");
res_mem_config = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");

// 3. 方式1(推荐):用专用接口platform_get_irq_byname获取中断号
ohci_irq = platform_get_irq_byname(pdev, "ohci");
ehci_irq = platform_get_irq_byname(pdev, "ehci");

// 3. 方式2(备选):先获取resource结构体,再取start字段(等价于方式1)
// res_irq_ohci = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "ohci");
// res_irq_ehci = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "ehci");
// ohci_irq = res_irq_ohci ? res_irq_ohci->start : -ENODEV;
// ehci_irq = res_irq_ehci ? res_irq_ehci->start : -ENODEV;

// 4. (可选)添加错误检查(工业级代码必备)
if (!res_meml) {
    dev_err(&pdev->dev, "Failed to get 'ohci' memory resource\n");
    return -ENODEV;
}
if (!res_mem_config) {
    dev_err(&pdev->dev, "Failed to get 'config' memory resource\n");
    return -ENODEV;
}
if (ohci_irq < 0) {
    dev_err(&pdev->dev, "Failed to get 'ohci' IRQ\n");
    return ohci_irq;
}
if (ehci_irq < 0) {
    dev_err(&pdev->dev, "Failed to get 'ehci' IRQ\n");
    return ehci_irq;
}

处理资源步骤

设备探测:内核启动后,识别出"系统中存在这个设备"(比如从设备树找到I2C控制器节点)

设备资源:设备工作必须的硬件资源如物理内存地址(reg)、中断号(interrupts)、GPIO、时钟、DMA通道等。

板级文件/机器文件:老版本无设备树的时候,硬件资源写在"板级文件"/"机器文件"里面

of_platform核心:基于设备树(Open Frimware OF)的平台总线核心模块,负责从设备树提取资源

platform核心:平台总线核心模块(无设备树)负责从板级文件提取资源,兼容设备树场景

resource结构体:封装"硬件资源"的统一标准数据结构,把不同类型的资源(地址、中断等)标准化,方便驱动使用。

步骤 1:设备探测(前提)

内核启动后,会做两件事完成 "设备探测":

有设备树:of_platform核心扫描设备树,找到compatible = "fsl,imx6q-i2c"的 I2C 控制器节点,确认 "这个设备存在";

无设备树(老版本):platform核心扫描板级文件(如board-imx6q.c)里的设备列表,确认 "I2C 控制器存在"。

步骤 2:资源收集(核心动作 1)

"收集" 是指of_platform/platform核心去 "提取" 该设备需要的硬件资源:

设备树场景(主流):of_platform核心解析 I2C 控制器节点的属性,提取资源:

从reg = <0x021a0000 0x4000>提取 "物理内存地址资源";

从interrupts = <50 0x0>提取 "中断资源";

老版本板级文件场景:platform核心从板级文件的资源数组提取,比如:

C 复制代码
// 板级文件里的资源定义(老版本)
static struct resource i2c1_resources[] = {
    [0] = {
        .start = 0x021a0000,  // 地址起始
        .end   = 0x021a3fff,  // 地址结束(大小0x4000)
        .flags = IORESOURCE_MEM,  // 标记是内存资源
    },
    [1] = {
        .start = 50,          // 中断号
        .end   = 50,
        .flags = IORESOURCE_IRQ,  // 标记是中断资源
    },
};

步骤 3:资源封装(resource 结构体的作用)

of_platform/platform核心会把提取到的资源,封装成内核统一的resource结构体(不管资源来自设备树还是板级文件,都转成这个结构体)。

先看resource结构体的简化定义(内核源码linux/ioport.h):

C 复制代码
struct resource {
    resource_size_t start;  // 资源起始值(如地址0x021a0000、中断号50)
    resource_size_t end;    // 资源结束值(如地址0x021a3fff、中断号50)
    const char *name;       // 资源名称(如"I2C1 MEM")
    unsigned long flags;    // 资源类型标记(如IORESOURCE_MEM=内存,IORESOURCE_IRQ=中断)
    struct resource *parent, *sibling, *child;  // 链表指针,管理多个资源
};

步骤 4:资源分配(核心动作 2)

内核把封装好的resource结构体 "分配" 给 I2C 控制器的驱动 ------ 驱动不用再直接解析设备树节点(或板级文件),只需通过内核提供的接口(如platform_get_resource())就能拿到resource结构体,进而获取硬件资源:

C 复制代码
// I2C控制器驱动中获取资源(伪代码)
struct platform_device *pdev = ...;  // 平台设备
// 获取内存资源
struct resource *mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 获取中断资源
struct resource *irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

// 驱动用这些资源操作硬件:比如映射物理地址到虚拟地址
void __iomem *i2c_base = ioremap(mem_res->start, resource_size(mem_res));
// 申请中断
request_irq(irq_res->start, i2c_irq_handler, 0, "i2c1", NULL);

提取应用程序特定数据

提取字符串属性

使用of_property_ read_string*()函数读取单个字符串属性。该函数定义如下:

什么是device_node?

struct device_node是 Linux 内核中表示设备树节点的核心结构体(定义在linux/of.h)------ 设备树中的每一个节点(比如fake_device、i2c3、ecspi1),在内核启动解析dtb文件后,都会在内存中生成一个device_node实例。

你可以把它理解为:设备树节点在内核中的 "身份证",所有操作设备树节点的函数(比如读取属性、获取子节点、解析资源),都需要先拿到这个节点的device_node指针,才能定位到具体的设备树节点。

如何获取device_node
C 复制代码
#include <linux/of.h>

// 方式1:从平台设备(platform_device)中获取(最常用)
struct platform_device *pdev = ...;  // 驱动匹配到的平台设备
struct device_node *np = pdev->dev.of_node;

// 方式2:通过路径查找节点(比如查找根节点下的fake_device)
struct device_node *np = of_find_node_by_path("/fake_device");

// 方式3:通过compatible匹配查找节点
struct device_node *np = of_find_compatible_node(NULL, NULL, "packt,fake-device");

⚠️ 注意:of_find_node_by_path/of_find_compatible_node获取的节点,使用完需要调用of_node_put(np)释放引用,避免内存泄漏;从platform_device获取的节点无需手动释放(内核自动管理)。

of_property_read_string


of_property_read_string_index


of_property_read_string_array


读取单元格和无符号的32位整数

of_property_read_u32


of_property_read_u32_index


of_property_read_u32_array


处理布尔属性

of_property_read_bool()

提取和解析子节点

for_each_child_of_node()

可以使用for_each_child_of_node()函数遍历给定节点的子节点:

dts 复制代码
/my_parent_node {
    compatible = "my,parent-node";
    status = "okay";

    // 子节点1
    child_node1 {
        compatible = "my,child1";
        name = "child1";
    };

    // 子节点2
    child_node2 {
        compatible = "my,child2";
        name = "child2";
    };
};
C 复制代码
#include <linux/module.h>
#include <linux/of.h>       // 设备树核心头文件(必须包含)
#include <linux/of_device.h>

// 模块初始化函数
static int __init dt_child_traverse_init(void)
{
    struct device_node *parent_node;  // 父节点指针
    struct device_node *child_node;   // 子节点指针
    const char *child_name;           // 子节点名称

    // 1. 先找到要遍历的父节点(替换为你实际的设备树节点路径)
    parent_node = of_find_node_by_path("/my_parent_node");
    if (!parent_node) {
        pr_err("Failed to find parent node!\n");
        return -EINVAL;
    }
    pr_info("Found parent node: %s\n", parent_node->name);

    // 2. 核心:使用for_each_child_of_node遍历所有子节点
    for_each_child_of_node(parent_node, child_node) {
        // 获取子节点名称(也可以获取属性、地址等)
        child_name = of_get_property(child_node, "name", NULL);
        if (!child_name) {
            child_name = child_node->name;  // 备用:直接取节点名
        }

        // 打印子节点信息(示例:名称、路径)
        pr_info("  Found child node: %s, full path: %s\n", 
                child_name, child_node->full_name);

        // 【可选】获取子节点的自定义属性(比如"compatible")
        const char *compatible;
        if (of_property_read_string(child_node, "compatible", &compatible)) {
            pr_warn("  Child %s has no compatible property\n", child_name);
        } else {
            pr_info("  Child %s compatible: %s\n", child_name, compatible);
        }
    }

    // 3. 释放父节点引用(必须!避免内存泄漏)
    of_node_put(parent_node);
    return 0;
}

// 模块退出函数
static void __exit dt_child_traverse_exit(void)
{
    pr_info("DT child traverse module exit\n");
}

module_init(dt_child_traverse_init);
module_exit(dt_child_traverse_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("for_each_child_of_node usage example");
相关推荐
博客-小覃9 分钟前
Zabbix之华为交换机的日志记录信息操作详细教程
服务器·网络·华为·zabbix
叠叠乐21 分钟前
redmi k90 pro max 强解BL,刷海外rom, 并刷入sukisu ultra
linux
stolentime27 分钟前
FreeDomain 本地开发环境快速搭建指南
运维·服务器·网络
向量引擎31 分钟前
从零起步,如何打造专属向量引擎 API 中转工作流?
java·服务器·前端
xiaoye-duck1 小时前
《Linux系统编程》Linux 进程间通信之管道基础解析:从匿名管道原理到基于管道的进程池实现
linux
z200509301 小时前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
bush42 小时前
嵌入式linux学习记录四
linux·运维·学习
lihao lihao3 小时前
软硬链接
linux·运维·服务器
TOWE technology3 小时前
智能安防监控系统如何做好防雷?——视频信号SPD综合应用方案解析
运维·服务器·防雷产品·信号保护·信号防雷·spd