LV06-10-设备树-03-设备树深入分析

dtb是什么格式?内核怎么处理设备树的?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、dtb 文件格式

1. dtb在内存中是什么样子?

设备树 Blob (DTB) 格式是设备树数据的平面二进制编码。 它用于在软件程序之间交换设备树数据。 例如, 在启动操作系统时, 固件会将 DTB 传递给操作系统内核。

DTB 格式在单个、 线性、 无指针数据结构中对设备树数据进行编码。 它由一个小头部和三个可变大小的部分组成: 内存保留块、 结构块和字符串块。 这些应该以该顺序出现在展平的设备树中。 因此, 设备树结构作为一个整体, 当加载到内存地址时, 将类似于下图

image-20250221101732222

Tips:设备树的dtb文件是以大端模式存储,所以打开的时候要注意一下。

2. dtb格式分析

2.1 设备树源码

下面我们以这个设备树为例进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};
aliases {
led1 = "/gpio@22020101";
};
node1 {
#address-cells = <1>;
#size-cells = <1>;
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};
node2 {
node1-child {
pinnum = <01234>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

而我们之后要分析的是二进制的 dtb 文件, 所以需要使用 dtc 工具将上面的 dts 文件编译成 dtb 文件,前面已经了解过了。

1
./dtc -I dts -O dtb -o dtb_file_format.dtb dtb_file_format.dts

2.2 二进制打开是什么样?

用二进制分析软件(可以用 BinaryViewer 或者 hexdump 命令)打开 dtb 文件并设置大端模式。我这里用的是BinaryViewer ,这个是免费软件,去官网下载安装就可以了。打开后,内容如下:

image-20250221114541086

Tips: hexdump 命令使用格式如下:

1
hexdump -C dtb_file_format.dtb

2.3 解读二进制文件

2.3.1 Header

devicetree 的头布局由 struct fdt_header 结构定义。所有的头字段都是 32 位整数, 以大端格式存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct fdt_header {
fdt32_t magic; // 设备树头部的魔数
fdt32_t totalsize; // 设备树文件的总大小
fdt32_t off_dt_struct; // 设备树结构体(节点数据) 相对于文件开头的偏移量
fdt32_t off_dt_strings; // 设备树字符串表相对于文件开头的偏移量
fdt32_t off_mem_rsvmap; // 内存保留映射表相对于文件开头的偏移量
fdt32_t version; // 设备树版本号
fdt32_t last_comp_version; // 最后一个兼容版本号
/* version 2 fields below */
fdt32_t boot_cpuid_phys; // 启动 CPU 的物理 ID
/* version 3 fields below */
fdt32_t size_dt_strings; // 设备树字符串表的大小
/* version 17 fields below */
fdt32_t size_dt_struct; // 设备树结构体(节点数据) 的大小
};

其中 fdt32_t 就是uint32_t。每个字段的描述如下所示 :

字段 描述
magic 该字段为固定值 0xd00dfeed(大端) 。
totalsize 该字段包含设备树数据结构的总大小(以字节为单位) 。 此大小应包含结构的所有部分: 标题、 内存保留块、 结构块和字符串块, 以及块之间或最后一个块之后的任何空闲空间间隙。
off_dt_struct 该字段包含结构块从头开始的以字节为单位的偏移量。
off_dt_strings 该字段包含字符串块从头开始的以字节为单位的偏移量。
off_mem_rsvmap 该字段包含从头开始的内存保留块的字节偏移量。
version 该字段包含设备树数据结构的版本。
last_comp_version 向后兼容的设备树数据结构的最低版本。
boot_cpuid_phys 与设备树 CPU 节点的 reg 属性对应
size_dt_strings 设备树字符串块部分的字节长度。
size_dt_struct 设备树结构块部分的字节长度。

然后来查看二进制文件, 其中 4 个字节表示一个单位, 前十个单位分别代表上述的十个字段如下图:

image-20250221114739533
字段 十六进制数值 代表含义
magic D00DFEED 固定值
totalsize 000002A4 转换为十进制之后为 676, 表示该文件大小为 676 字节
off_dt_struct 00000038 表示结构块从 00000038 这个地址开始, 和后面的size_dt_struct 结构块大小参数一起可以确定结构块的存储范围
off_dt_strings 0000024C 表示字符串块从 0000024C 这个地址开始, 和后面的size_dt_strings 字符串块大小参数一起可以确定字符串块的存储范围
off_mem_rsvmap 00000028 表示内存保留块的偏移为 00000028, header 之后结构快之前都是属于内存保留块。
version 00000011 11 转换为十进制之后为 17, 表示当前设备树结构版本为17
last_comp_version 00000010 10 转换为十进制之后为 16, 表示向前兼容的设备树结构版本为 16
boot_cpuid_phys 00000000 表示设备树的 teg 属性为 0
size_dt_strings 00000058 表 示 字 符 串 块 的 大 小 为 00000058 , 和 前 面 的off_dt_strings 字符串块偏移值一起可以确定字符串块的范围
size_dt_struct 00000214 表示结构块的大小为 00000214, 和前面的 off_dt_struct结构块偏移值一起可以确定结构块的范围

2.3.2 内存保留块

内存保留块(Memory Reserved Block) 是用于客户端程序的保护和保留物理内存区域的列表。 这些保留区域不应被用于一般的内存分配, 而是用于保护重要数据结构, 以防止客户端程序覆盖这些数据。 内存保留块的目的是确保特定的内存区域在客户端程序运行时不被修改或使用。 由于在示例设备树中没有设置内存保留块, 所以相应的区域都为 0, 如下 :

image-20250221115127342

保留区域列表: 内存保留块是一个由一组 64 位大端整数对构成的列表。 每对整数对应一个保留内存区域, 其中包含物理地址和区域的大小(以字节为单位) 。 这些保留区域应该彼此不重叠。

保留区域的用途: 客户端程序不应访问内存保留块中的保留区域, 除非引导程序提供的其他信息明确指示可以访问。 引导程序可以使用特定的方式来指示客户端程序可以访问保留内存的部分内容。 引导程序可能会在文档、 可选的扩展或特定于平台的文档中说明保留内存的特定用途。

格式: 内存保留块中的每个保留区域由一个 64 位大端整数对表示。 每对由 struct fdt_reserve_entry 结构表示

1
2
3
4
struct fdt_reserve_entry {
fdt64_t address;
fdt64_t size;
};

其中的第一个整数表示保留区域的物理地址, 第二个整数表示保留区域的大小(以字节为单位) 。 每个整数都以 64 位的形式表示, 即使在 32 位架构上也是如此。 在 32 位 CPU 上,整数的高 32 位将被忽略。

内存保留块为设备树提供了保护和保留物理内存区域的功能。 它确保了特定的内存区域在客户端程序运行时不被修改或使用。 这样可以确保引导程序和其他关键组件在需要的情况下能够访问保留内存的特定部分, 并保护关键数据结构免受意外修改。

2.3.3 结构块

结构块是设备树中描述设备树本身结构和内容的部分。 它由一系列带有数据的令牌序列组成, 这些令牌按照线性树结构进行组织。

  • (1) 令牌类型

结构块中的令牌分为五种类型, 每种类型用于不同的目的。

a. FDT_BEGIN_NODE (0x00000001): FDT_BEGIN_NODE 标记表示一个节点的开始。 它后面跟着节点的单元名称作为额外数据。 节点名称以以空字符结尾的字符串形式存储, 并且可以包括单元地址。 节点名称后可能需要填充零字节以对齐, 然后是下一个标记, 可以是除了 FDT_END之外的任何标记。

b. FDT_END_NODE (0x00000002): FDT_END_NODE 标记表示一个节点的结束。 该标记没有额外的数据, 紧随其后的是下一个标记, 可以是除了 FDT_PROP 之外的任何标记。

c. FDT_PROP(0x00000003): FDT_PROP 标记表示设备树中属性的开始。 它后面跟着描述属性的额外数据, 该数据首先由属性的长度和名称组成, 表示为下面这样的结构

1
2
3
4
struct{
fdt32_t len;
fdt32_t nameoff;
};

长度表示属性值的字节长度, 名称偏移量指向字符串块中存储属性名称的位置。 在这个结构之后, 属性的值作为字节字符串给出。 属性值后可能需要填充零字节以对齐, 然后是下一个令牌, 可以是除了 FDT_END 之外的任何标记。

d. FDT_NOP (0x00000004): FDT_NOP 令牌可以被解析设备树的程序忽略。 该令牌没有额外的数据, 紧随其后的是下一个令牌, 可以是任何有效的令牌。 使用 FDT_NOP 令牌可以覆盖树中的属性或节点定义, 从而将其从树中删除, 而无需移动设备树 blob 中的其他部分。

e. FDT_END (0x00000009): FDT_END 标记表示结构块的结束。 应该只有一个 FDT_END 标记, 并且应该是结构块中的最后一个标记。 该标记没有额外的数据, 紧随其后的字节应该位于结构块的开头偏移处, 该偏移等于设备树 blob 标头中的 fdt_header.size_dt_struct字段的值。

  • (2) 树状结构

设备树的结构以线性树的形式表示。 每个节点由 FDT_BEGIN_NODE 标记开始, 由FDT_END_NODE 标记结束。 节点的属性和子节点在 FDT_END_NODE 之前表示, 因此子节点的FDT_BEGIN_NODE 和 FDT_END_NODE 令牌嵌套在父节点的令牌中。

  • (3) 结构块的结束

结构块以单个 FDT_END 标记结束。 该标记没有额外的数据, 它位于结构块的末尾, 并且是结构块中的最后一个标记。 FDT_END 标记之后的字节应位于结构块的开头偏移处, 该偏移等于设备树 blob 标头中的 fdt_header.size_dt_struct字段的值。

对结构块开头的部分内容进行分析:

image-20250221142011534
十六进制数值 代表含义
00000001 根节点的开始
00000000 根节点没有节点名, 所以这里名字为 0
00000003 设备树中属性的开始
00000017 代表该属性的大小, 换算成十进制为 23, 也就是”This is my devicetree!”这一字符串的长度
00000000 代表该属性在字符串块的偏移量, 这里为 0, 表示无偏移
54686973 - 65210000 model 的具体值

通过使用结构块, 设备树可以以一种层次化的方式组织和描述系统中的设备和资源。 每个节点可以包含属性和子节点, 从而实现更加灵活和可扩展的设备树表示。

2.3.4 字符串块

字符串块用于存储设备树中使用的所有属性名称。 它由一系列以空字符结尾的字符串组成, 这些字符串在字符串块中简单地连接在一起, 具体示例如下

image-20250221144542579
  • ( 1) 字符串连接

字符串块中的字符串以空字符(\0) 作为终止符来连接。 这意味着每个字符串都以空字符结尾, 并且下一个字符串紧跟在上一个字符串的末尾。 这种连接方式使得字符串块中的所有字符串形成一个连续的字符序列。

  • ( 2) 偏移量引用

在结构块中, 属性的名称是通过偏移量来引用字符串块中的相应字符串的。 偏移量是一个无符号整数值, 它表示字符串在字符串块中的位置。 通过使用偏移量引用, 设备树可以节省空间, 并且在属性名称发生变化时也更加灵活, 因为只需要更新偏移量, 而不需要修改结构块中的属性引用。

  • ( 3) 对齐约束

字符串块没有对齐约束, 这意味着它可以出现在设备树 blob 的任何偏移处。 这使得字符串块的位置在设备树 blob 中是灵活的, 并且可以根据需要进行调整, 而不会对设备树的解析和处理造成影响。

字符串块是设备树中用于存储属性名称的部分。 它由字符串连接而成, 并通过偏移量在结构块中进行引用。 字符串块的灵活位置使得设备树的表示更加紧凑和可扩展。

3. 总结

3.1 dtb文件结构图

通过以上分析,可以得到Device Tree文件结构如下图(与前面分析的设备树有所不同,这里是从网上找的图)所示。dtb的头部首先存放的是fdt_header的结构体信息,接着是填充区域,填充大小为off_dt_struct – sizeof(struct fdt_header),填充的值为0。接着就是struct fdt_property 结构体的相关信息。最后是 dt_string 部分。
img

Device Tree源文件的结构分为header、fill_area、dt_struct及dt_string四个区域。fill_area区域填充数值0。节点(node)信息使用struct fdt_node_header结构体描述。属性信息使用struct fdt_property结构体描述。各个结构体信息如下:

1
2
3
4
5
6
7
8
9
10
11
struct fdt_node_header {
fdt32_t tag;
char name[0];
};

struct fdt_property {
fdt32_t tag;
fdt32_t len;
fdt32_t nameoff;
char data[0];
};

struct fdt_node_header描述节点信息,tag是标识node的起始结束等信息的标志位,name指向node名称的首地址。tag的取值如下:

1
2
3
4
5
1. #define FDT_BEGIN_NODE 0x1 /* Start node: full name */
2. #define FDT_END_NODE 0x2 /* End node */
3. #define FDT_PROP 0x3 /* Property: name off, size, content */
4. #define FDT_NOP 0x4 /* nop */
5. #define FDT_END 0x9

FDT_BEGIN_NODEFDT_END_NODE标识node节点的起始和结束,FDT_PROP标识node节点下面的属性起始符,FDT_END标识Device Tree的结束标识符。因此,对于每个node节点的tag标识符一般为FDT_BEGIN_NODE,对于每个node节点下面的属性的tag标识符一般是FDT_PROP

描述属性采用struct fdt_property描述,tag标识是属性,取值为FDT_PROP;len为属性值的长度(包括‘\0’,单位:字节);nameoff为属性名称存储位置相对于off_dt_strings的偏移地址。

3.2 设备节点结构图

dt_struct在Device Tree中的结构如下图所示。节点的嵌套也带来tag标识符的嵌套。

image-20250221151342424

3.3 一个更简单的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/dts-v1/;
/ {
compatible = "hd,test_dts", "hd,test_xxx";
#address-cells = <0x1>;
#size-cells = <0x1>;
model = "HD test dts";

chosen {
stdout-path = "/ocp/serial@ffff";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
led1:led@2000000 {
compatible = "test_led";
#address-cells = <0x1>;
#size-cells = <0x1>;
reg = <0x200 0x4>;
};
};

我们会得到如下结构的dtb文件:

image-20250221152833425

整个dtb文件还是比较简单的,图中的红色框出的部分为header部分的数据,可以看到:

  • (1)fdt_header

第1个四字节对应magic,数据为 D00DFEED.

第2四字节对应totalsize,数据为 000001BC,可以由整张图片看出,这个dtb文件的大小由0x0~0x1bb,大小刚好是0x1bc

第3个四字节对应off_dt_struct,数据为00000038。

第4个四字节对应off_dt_strings,数据为00000174,可以由整张图片看到,从0x174开始刚好是字符串开始的地方

第5个四字节对应off_mem_rsvmap,数据为00000028

第6个四字节对应version,数据为00000011,十进制为17

第7个四字节对应last_comp_version,数据为00000010,十进制为16,表示兼容版本16

第8个四字节对应boot_cpuid_phys,数据为00000000,仅在版本2中使用,这里为0

第9个四字节对应size_dt_strings,数据为00000048,表示字符串总长。

第10个四字节对应size_dt_struct,数据为0000013c,表示struct部分总长度。

整个头部为40字节,16进制为0x28,从头部信息中off_mem_rsvmap部分可以得到,reserve memory起始地址为0x28,上文中提到,这一部分使用一个16字节的struct来描述,以一个全为0的struct结尾。

  • (2)reserve memory

后16字节全为0,可以看出,这里并没有设置reserve memory。

  • (3)dt_struct

偏移地址来到0x00000038(0x28+0x10),接下来8个字节为00000003,根据上述structure中的描述,这是OF_DT_PROP,即标示属性的开始。

接下来4字节为00000018,表明该属性的value部分size为24字节。

接下来4字节是当前属性的key在string 部分的偏移地址,这里是00000000,由头部信息中off_dt_strings可以得到,string部分的开始为00000174,偏移地址为0,所以对应字符串为”compatible”.

之后就是value部分,这部分的数据是字符串,可以直接从图片右侧栏看出,总共24字节的字符串”hd,test_dts”, “hd,test_xxx”,因为字符串之间以0结尾,所以程序可以识别出这是两个字符串。

可以看出,到这里,compatible = “hd,test_dts”, “hd,test_xxx”;这个属性就被描述完了。

按照固有的规律,接下来就是对#address-cells = <0x1>的解析,然后是#size-cells = <0x1>…

然后就是递归的子节点chosen,memory@80000000等等都是按照上文中提到的structure解析规则来进行解析,最后以00000002结尾。

与根节点不同的是,子节点有一个unit name,即chosen,memory@80000000这些名称,并非节点中的.name属性。

而整个结构的结束由00000009来描述。

  • dt_string

从0x00000174地址开始就是dt_string的内容了。

二、内核对设备树的处理

1. 设备树的处理过程

从源代码文件 dts 文件开始,设备树的处理过程为:

image-20250219094830737

(1)dts 在 PC 机上被编译为 dtb 文件;

(2)u-boot 把 dtb 文件传给内核;

(3)内核解析 dtb 文件,把每一个节点都转换为 struct device_node结构体;

(4)对于某些 struct device_node 结构体,会被转换为 struct platform_device结构体。

2. dtb 展开成 device_node

dtb 中每一个节点都会被转换为 device_node 结构体。

2.1 展开流程

dtb 展开流程图如下:

image-20250221160740859

(1) 设备树源文件编写: 根据设备树的基本语法和相关知识编写符合规范的设备树。

(2) 设备树编译: 设备树源文件经过设备树编译器(dtc) 进行编译, 生成设备树二进制文件(.dtb) 。 设备树编译器会检查源文件的语法和语义, 并将其转换为二进制格式, 以便内核能够解析和使用。

(3) boot.img 镜像生成: boot.img 是一个包含内核镜像、 设备树二进制文件和其他一些资源文件的镜像文件(目前只是适用于瑞芯微的 soc 上, 其他厂商的 soc 需要具体问题具体分析) 。 在生成 boot.img 时, 通常会将内核镜像、 设备树二进制文件和其他一些资源文件打包在一起。 这个过程可以使用特定的工具或脚本完成。但是这里其实可以内核和设备树分开,内核是内核,设备树是设备树,然后配置uboot为从tftp下载内核和设备树,然后加载内核和设备树,这样有利于我们调试。

(4) U-Boot 加载: U-Boot(Universal Bootloader) 是一种常用的开源引导加载程序, 用于引导嵌入式系统。 在系统启动过程中, U-Boot 会将 boot.img 中的内核和设备树的二进制文件加载到系统内存的特定地址。或者可以设置为从tftp服务器下载设备树和内核。

( 5) 内核初始化: U-Boot 将内核和设备树的二进制文件加载到系统内存的特定地址后,控制权会转交给内核。 在内核初始化的过程中, 会解析设备树二进制文件, 将其展开为内核可以识别的数据结构, 以便内核能够正确地初始化和管理硬件资源。

( 6) 设备树展开: 设备树展开是指将设备树二进制文件解析成内核中的设备节点(device_node) 的过程。 内核会读取设备树二进制文件的内容, 并根据设备树的描述信息,构建设备树数据结构, 例如设备节点、 中断控制器、 寄存器、 时钟等。 这些设备树数据结构将在内核运行时用于管理和配置硬件资源。

2.2 相关数据结构

dtb 中每一个节点都被转换为 struct device_node 结构体。根节点被保存在全局变量 of_root中(这个全局变量在__unflatten_device_tree()函数中使用),从 of_root 开始可以访问到任意节点。

2.2.1  struct device_node

struct device_node 结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct device_node {
const char *name;
const char *type;
phandle phandle;
const char *full_name;
struct fwnode_handle fwnode;

struct property *properties;
struct property *deadprops; /* removed properties */
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
//......
};

2.2.2 struct property

struct property 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct property {
char *name;
int length;
void *value;
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};

2.3 展开过程源码分析

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。 Linux 内核解析 DTB 文件的函数调用关系如下:

image-20250219163216978

可以看出,在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工作的函数为 unflatten_dt_nodes。

2.3.1 start_kernel()

首先来到源码目录下的 main.c - init/main.c 文件, 找到其中的 start_kernel() 函数, start_kernel() 函数是 Linux 内核启动的入口点, 它是 Linux 内核的核心函数之一, 负责完成内核的初始化和启动过程:

1
2
3
4
5
6
asmlinkage __visible void __init start_kernel(void)
{
// ......
setup_arch(&command_line);// 架构相关的初始化
// ......
}

其中跟设备树相关的函数为 setup_arch(&command_line);

2.3.2 setup_arch()

setup_arch()函数与架构有关,像RK3568平台是定义在 arch/arm64/kernel/setup.c 中,imx6ull的话是定义在 arch/arm64/kernel/setup.c 中:

1
2
3
4
5
6
7
8
void __init setup_arch(char **cmdline_p)
{
//......
mdesc = setup_machine_fdt(__atags_pointer);// 设置机器的 FDT(平台设备树)
//......
unflatten_device_tree();// 展开设备树
//......
}

setup_machine_fdt()函数是用于在内核启动过程中设置机器的设备树。 __atags_pointer是 dtb 二进制文件加载到内存的地址。具体是怎么传过来的,这里就没有深入研究了,这里知道是设备树的地址就可以了。可以简单看一下这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 初始化设置机器的设备树
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
//......
// phys_to_virt() 将设备树物理地址映射到内核虚拟地址空间
// early_init_dt_verify() 验证设备树有效性
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
return NULL;
// 匹配 machine_desc:遍历所有 machine_desc,与设备树根节点的 compatible 字符串进行匹配
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
//......
return mdesc;
}

具体的就没有再深入去了解了,以后有需要再说吧。这里主要还是要了解设备树的展开:unflatten_device_tree()

2.3.3 unflatten_device_tree()

unflatten_device_tree()函数用于解析设备树, 将紧凑的设备树数据结构转换为树状结构的设备树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* unflatten_device_tree - create tree of device_nodes from flat blob
*
* unflattens the device-tree passed by the firmware, creating the
* tree of struct device_node. It also fills the "name" and "type"
* pointers of the nodes so the normal device-tree walking functions
* can be used.
*/
void __init unflatten_device_tree(void)
{
// 解析设备树,解析后的设备树将使用 of_root 指针进行存储。
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
// 获取指向 "/chosen" 和 "/aliases" 节点的指针,并为它们分配内存。 以供全局使用
/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
of_alias_scan(early_init_dt_alloc_memory_arch);
// 运行设备树的单元测试
unittest_unflatten_overlay_base();
}

2.3.4 __unflatten_device_tree()

__unflatten_device_tree()函数的重点在两次设备树的扫描上, 第一遍扫描的目的是计算展开设备树所需的内存大小,第二遍扫描的目的是实际展开设备树, 并填充设备节点的名称、 类型和属性等信息。 我们来看一下这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void *__unflatten_device_tree(const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
// 第一遍扫描, 计算大小
/* First pass, scan for size */
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
if (size < 0)
return NULL;

size = ALIGN(size, 4);
pr_debug(" size is %d, allocating...\n", size);
// 为展开的设备树分配内存
/* Allocate memory for the expanded device tree */
mem = dt_alloc(size + 4, __alignof__(struct device_node));
//......
// 第二遍扫描, 实际展开设备树
/* Second pass, do actual unflattening */
unflatten_dt_nodes(blob, mem, dad, mynodes);
if (be32_to_cpup(mem + size) != 0xdeadbeef)
pr_warning("End of tree marker overwritten: %08x\n",
be32_to_cpup(mem + size));
//......
}

470 行:unflatten_dt_nodes()函数的作用是递归地遍历设备树数据块, 并计算展开设备树所需的内存大小。 它接受四个参数: blob(设备树数据块指针) 、 start(当前节点的起始地址,初始为 NULL) 、 dad(父节点指针) 和 mynodes(用于存储节点指针数组的指针, 初始为 NULL)。第一遍扫描完成后, unflatten_dt_nodes()函数会返回展开设备树所需的内存大小, 然后在对大小进行对齐操作, 并为展开的设备树分配内存。

489 行:再次调用了 unflatten_dt_nodes() 函数进行第二遍扫描。 通过这样的过程, 第二遍扫描会将设备树数据块中的节点展开为真正的设备节点, 并填充节点的名称、 类型和属性等信息。 这样就完成了设备树的展开过程。

2.3.5 unflatten_dt_nodes()

unflatten_dt_nodes() 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static int unflatten_dt_nodes(const void *blob,
void *mem,
struct device_node *dad,
struct device_node **nodepp)
{
struct device_node *root; // 根节点
int offset = 0, depth = 0, initial_depth = 0; // 偏移量、 深度和初始深度
#define FDT_MAX_DEPTH 64 // 最大深度
struct device_node *nps[FDT_MAX_DEPTH]; // 设备节点数组
void *base = mem; // 基地址, 用于计算偏移量
bool dryrun = !base; // 是否只是模拟运行, 不实际处理

if (nodepp)
*nodepp = NULL; // 如果指针不为空, 将其置为空指针

/*
* We're unflattening device sub-tree if @dad is valid. There are
* possibly multiple nodes in the first level of depth. We need
* set @depth to 1 to make fdt_next_node() happy as it bails
* immediately when negative @depth is found. Otherwise, the device
* nodes except the first one won't be unflattened successfully.
*/
// 如果 @dad 有效, 则表示正在展开设备子树。在第一层深度可能有多个节点。
//将 @depth 设置为 1, 以使 fdt_next_node() 正常工作。当发现负的 @depth 时, 该函数会立即退出。否则, 除第一个节点外的设备节点将无法成功展开。
if (dad)
depth = initial_depth = 1;

root = dad; // 根节点为 @dad
nps[depth] = dad; // 将根节点放入设备节点数组

for (offset = 0;
offset >= 0 && depth >= initial_depth;
offset = fdt_next_node(blob, offset, &depth)) {
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))
continue;
// 如果未启用 CONFIG_OF_KOBJ 并且节点不可用, 则跳过该节点
if (!IS_ENABLED(CONFIG_OF_KOBJ) &&
!of_fdt_device_is_available(blob, offset))
continue;
// 填充节点信息, 并将子节点添加到设备节点数组
if (!populate_node(blob, offset, &mem, nps[depth],
&nps[depth+1], dryrun))
return mem - base;
// 将子节点指针赋值给 @nodepp
if (!dryrun && nodepp && !*nodepp)
*nodepp = nps[depth+1];
// 如果根节点为空, 则将子节点设置为根节点
if (!dryrun && !root)
root = nps[depth+1];
}

if (offset < 0 && offset != -FDT_ERR_NOTFOUND) {
pr_err("Error %d processing FDT\n", offset);
return -EINVAL;
}

/*
* Reverse the child list. Some drivers assumes node order matches .dts
* node order
*/

// 反转子节点列表。 一些驱动程序假设节点顺序与 .dts 文件中的节点顺序一致
if (!dryrun)
reverse_nodes(root);

return mem - base; // 返回处理的字节数
}

393 行:fdt_next_node()函数用来遍历设备树的节点。 从偏移量为 0 开始, 只要偏移量大于等于 0且深度大于等于初始深度, 就执行循环。 循环中的每次迭代都会处理一个设备树节点。在每次迭代中, 首先检查深度是否超过了最大深度 FDT_MAX_DEPTH, 如果超过了, 则跳过该节点。如果未启用 CONFIG_OF_KOBJ 并且节点不可用( 通过 of_fdt_device_is_available() 函数判断) , 则跳过该节点。

401 行:调用 populate_node() 函数填充节点信息,并将子节点添加到设备节点数组 nps 中。 populate_node() 函数定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static bool populate_node(const void *blob,
int offset,
void **mem,
struct device_node *dad,
struct device_node **pnp,
bool dryrun)
{
struct device_node *np; // 设备节点指针
const char *pathp; // 节点路径字符串指针
unsigned int l, allocl; // 路径字符串长度和分配的内存大小

pathp = fdt_get_name(blob, offset, &l); // 获取节点路径和长度
if (!pathp) {
*pnp = NULL;
return false;
}

allocl = ++l; // 分配内存大小为路径长度加一, 用于存储节点路径字符串

np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
__alignof__(struct device_node)); // 分配设备节点内存
if (!dryrun) {
char *fn;
of_node_init(np); // 初始化设备节点
np->full_name = fn = ((char *)np) + sizeof(*np); // 设置设备节点的完整路径名

memcpy(fn, pathp, l); // 将节点路径字符串复制到设备节点的完整路径名中

if (dad != NULL) {
np->parent = dad; // 设置设备节点的父节点
np->sibling = dad->child;// 设置设备节点的兄弟节点
dad->child = np; // 将设备节点添加为父节点的子节点
}
}

populate_properties(blob, offset, mem, np, pathp, dryrun);// 填充设备节点的属性信息
if (!dryrun) {
np->name = of_get_property(np, "name", NULL);// 获取设备节点的名称属性
np->type = of_get_property(np, "device_type", NULL);// 获取设备节点的设备类型属性

if (!np->name)
np->name = "<NULL>";// 如果设备节点没有名称属性, 则设置为"<NULL>"
if (!np->type)
np->type = "<NULL>";// 如果设备节点没有设备类型属性, 则设置为"<NULL>"
}

*pnp = np;// 将设备节点指针赋值给*pnp
return true;
}

populate_node() 函数中首先会调用第 299 行的 unflatten_dt_alloc()函数分配设备节点内存。分配的内存大小 为 sizeof(struct device_node) + allocl 字 节 , 并 使 用 __alignof__(struct device_node) 对齐。 然后调用 populate_properties()函数填充设备节点的属性信息。 该函数会解析设备节点的属性, 并根据需要分配内存来存储属性值。

3. device_node 转换成 platform_device

在上一节中, 我们学习了 dtb 二进制文件展开成 device_node 的具体流程, 而 device_node 这时候还并不能跟内核中的 platform_driver进行对接, 而为了让操作系统能够识别和管理设备, 需要将设备节点转换为平台设备。

3.1 转换规则

在之前学习的平台总线模型中, device 部分是用 platform_device 结构体来描述硬件资源的, 所以内核最终会将内核认识的 device_node 树转换 platform_device, 但是并不是所有的 device_node 都会被转换成 platform_ device, 只有满足要求的才会转换成 platform_device,转换成 platform_device 的节点可以在 /sys/bus/platform/devices 下查看, 那 device_node 节点要满足什么要求才会被转换成 platform_device 呢?

  • 规则 1:根节点下包含 compatible 属性的子节点

对于每个根节点下含有 compatible 属性的子节点, 创建一个对应的 platform_device

  • 规则 2:含有特定compatible 属性的节点的子节点

节点中 compatible 属性含有 “simple-bus”、 “simple-mfd” 或 “isa” 的节点,如果它们的子节点包含 compatible 属性值,则会为这个子节点创建一个对应的 platform_device

  • 规则 3:节点的 compatible 属性包含 “arm” 或 “primecell”, 则不将该节点转换platform_device, 而是将其识别为 AMBA 设备。

  • 规则4:总线 I2C、 SPI 节点下的子节点, 不转换platform_device 。某个总线下的子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device

3.2 转换示例

3.2.2 示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/{
mytest {
compatile = "mytest", "simple-bus";
mytest@0 {
compatile = "mytest_0";
};
};
i2c {
compatile = "samsung,i2c";
at24c02 {
compatile = "at24c02";
};
};
spi {
compatile = "samsung,spi";
flash@0 {
compatible = "winbond,w25q32dw";
spi-max-frequency = <25000000>;
reg = <0>;
};
};
}
  • /mytest 会被转换为 platform_device, 因为它兼容”simple-bus”;它的子节点/mytest/mytest@0 也会被转换为 platform_device
  • /i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的 platform_driver;
  • /i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 i2c_client。
  • 类似的也有/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为platform_device, 在内核中有对应的 platform_driver;
  • /spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver 决定, 一般是被创建为一个 spi_device。

3.2.3 示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};
aliases {
led1 = "/gpio@22020101";
};
node1 {
#address-cells = <1>;
#size-cells = <1>;
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};
node2 {
node1-child {
pinnum = <01234>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

总共有 chosen、 cpu1: cpu@1、 aliases、 node1、 node2、 gpio@22020101这六个节点, 其中前五个节点都没有 compatible 属性, 所以并不会被转换为 platform_device,而最后一个 gpio@22020101 节点符合规则一, 在根节点下, 且有 compatible 属性, 所以最后会转换为 platform_device。

3.2.4 示例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};
aliases {
led1 = "/gpio@22020101";
};
node1 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
gpio@22020102 {
reg = <0x20220102 0x40>;
};
};
node2 {
node1-child {
pinnum = <01234>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

相较于示例 2 的设备树, 这里在 node1 节点中添加了 compatible 属性, 但是这个 compa tible 属性值为 simple-bus, 我们需要继续看他的子节点, 子节点 gpio@22020102 并没有 comp atible 属性值, 所以这里的 node1 节点不会被转换。

3.2.4 示例4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
};
aliases {
led1 = "/gpio@22020101";
};
node1 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
gpio@22020102 {
compatible = "gpio";
reg = <0x20220102 0x40>;
};
};
node2 {
node1-child {
pinnum = <01234>;
};
};
gpio@22020101 {
compatible = "led";
reg = <0x20220101 0x40>;
status = "okay";
};
};

相较于示例 3 的设备树, 这里在 node1 节点的子节点 gpio@22020102 中添加了 compati ble 属性, node1 节点的 compatible 属性值为 simple-bus, 然后需要继续看他的子节点, 子节点 gpio@22020102 的 compatible 属性值为 gpio, 所以这里的 gpio@22020102 节点会被转换成platform_device。

3.2.5 示例5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/dts-v1/;
/ {
model = "This is my devicetree!";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
cpul: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a35", "arm,armv8";
reg = <0x0 0x1>;
amba {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges;
dmac_peri: dma-controller@ff250000 {
compatible = "arm,p1330", "arm,primecell";
reg = <0x0 0xff250000 0x0 0x4000>;
interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,p1330-periph-burst;
clocks = <&cru ACLK DMAC_PERI>;
clock-names = "apb_pclk";
};
dmac_bus: dma-controller@ff600000 {
compatible = "arm,p1330", "arm,primecell";
reg = <0x0 0xff600000 0x0 0x4000>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>;
arm,pl330-broken-no-flushp;
arm,pl330-periph-burst;
clocks = <&cru ACLK_DMAC_BUS>;
clock-names = "apb_pclk";
};
};
};
};

amba 节点的 compatible 值为 simple-bus, 不会被转换为 platform_device, 而是作为父节点用于组织其他设备, 所以需要来查看他的子节点。

dmac_peri: dma-controller@ff250000 节点: 该节点的 compatible 属性包含 “arm,p1330”和 “arm,primecell”, 根据规则 3, 该节点不会被转换为 platform_device, 而是被识别为 AMBA设备。

dmac_bus: dma-controller@ff600000 节点: 该节点的 compatible 属性包含 “arm,p1330” 和”arm,primecell”, 根据规则 3, 该节点不会被转换为 platform_device, 而是被识别为 AMBA 设备。

3.3 转换流程分析

3.3.1 arch_initcall_sync()

事实上,如果从C语言的开始函数start_kernel()进行追溯,是找不到platform device这一部分转换的源头的,事实上,这个转换过程的函数是of_platform_default_populate_init(),它被调用的方式是这样一个声明:

1
arch_initcall_sync(of_platform_default_populate_init);

这行在 platform.c - drivers/of/platform.c 中,先来看 arch_initcall_sync() 函数,它是一个宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

#define arch_initcall_sync(fn) __define_initcall(fn, 3s)

最终是调用__define_initcall(fn, id),这个 id 代表系统启动时被调用的优先级,数字越小,优先级越低,用这一系列宏声明一个新的函数就是将这个函数指针放入内存中一个指定的段(即放入到”.init”中,n代表优先级,当系统启动时,会依次调用这些段中的函数)内。

它用于在内核初始化过程中执行架构相关的初始化函数。 它属于内核的初始化调用机制, 用于确保在系统启动过程中适时地调用特定架构的初始化函数。

在 Linux 内核的初始化过程中, 各个子系统和架构会注册自己的初始化函数。 这些初始化函数负责完成特定子系统或架构相关的初始化工作, 例如初始化硬件设备、 注册中断处理程序、设置内存映射等。 而 arch_initcall_sync() 函数则用于调用与当前架构相关的初始化函数。

3.3.2 of_platform_default_populate_init()

当内核启动,在初始化过程中,arch_initcall_sync() 函数会被调用, 以确保所有与当前架构相关的初始化函数按照正确的顺序执行。 这样可以保证在启动过程中, 特定架构相关的初始化工作得到正确地完成。

of_platform_default_populate_init() 函数的作用是在内核初始化过程中自动解析设备树,并根据设备树中的设备节点创建对应的 platform_device 结构。 它会遍历设备树中的设备节点,并为每个设备节点创建一个对应的 platform_device 结构, 然后将其注册到内核中, 使得设备驱动程序能够识别和操作这些设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int __init of_platform_default_populate_init(void)
{
struct device_node *node;
// 检查设备树是否已经被填充。 如果设备树尚未填充, 则返回错误码
if (!of_have_populated_dt())
return -ENODEV;

/*
* Handle certain compatibles explicitly, since we don't want to create
* platform_devices for every node in /reserved-memory with a
* "compatible",
*/
// 遍历设备树中与 reserved_mem_matches 匹配的节点。 这些节点是 /reserved-memory 中具有 "compatible" 属性的节点。
//reserved-memory 中匹配的节点创建 platform_device 结构。 不会为每个具有“compatible”的节点节点都创建 platform_device, 而是根据需要进行显式处理。
for_each_matching_node(node, reserved_mem_matches)
of_platform_device_create(node, NULL, NULL);
// 查找节点 "/firmware"
node = of_find_node_by_path("/firmware");
if (node) {
// 使用找到的节点填充设备树中的平台设备。 这些节点可能包含与固件相关的设备。
of_platform_populate(node, NULL, NULL, NULL);
of_node_put(node);
}
// 填充其他设备
/* Populate everything else. */
of_platform_default_populate(NULL, NULL, NULL);

return 0;
}
arch_initcall_sync(of_platform_default_populate_init);

3.3.3 of_platform_default_populate()

我们来看一下 of_platform_default_populate_init() 函数的 542 行,这里调用了of_platform_default_populate(),这个函数定义如下:

1
2
3
4
5
6
7
int of_platform_default_populate(struct device_node *root,
const struct of_dev_auxdata *lookup,
struct device *parent)
{
return of_platform_populate(root, of_default_bus_match_table, lookup,
parent);
}

该函数的作用是调用 of_platform_populate()函数来填充设备树中的平台设备, 并使用默认的设备匹配表 of_default_bus_match_table, 设备匹配表内容如下所示:

1
2
3
4
5
6
7
8
9
const struct of_device_id of_default_bus_match_table[] = {
{ .compatible = "simple-bus", },
{ .compatible = "simple-mfd", },
{ .compatible = "isa", },
#ifdef CONFIG_ARM_AMBA
{ .compatible = "arm,amba-bus", },
#endif /* CONFIG_ARM_AMBA */
{} /* Empty terminated list */
};

上述的设备匹配表就是我们第 2 条规则, 函数将自动根据设备树节点的属性匹配相应的设备驱动程序, 并填充内核的平台设备列表。

3.3.4 of_platform_populate()

接下来看 of_platform_populate() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int of_platform_populate(struct device_node *root,
const struct of_device_id *matches,
const struct of_dev_auxdata *lookup,
struct device *parent)
{
struct device_node *child;
int rc = 0;
// 如果 root 不为空, 则增加 root 节点的引用计数; 否则, 在设备树中根据路径查找 root 节点
root = root ? of_node_get(root) : of_find_node_by_path("/");
if (!root)
return -EINVAL;

pr_debug("%s()\n", __func__);
pr_debug(" starting at: %pOF\n", root);
// 遍历设备树节点的子节点, 查找与平台设备相关的节点。 这些节点通常具有 compatible 属性, 用于匹配设备驱动程序。
for_each_child_of_node(root, child) {
// 创建平台设备并添加到设备树总线,对于每个找到的平台设备节点, 创建一个 platform_device 结构, 并根据设备树节点的属性设置该结构的各个字段。
rc = of_platform_bus_create(child, matches, lookup, parent, true);
if (rc) {
of_node_put(child);//将创建的 platform_device 添加到内核的平台设备列表中, 以便设备驱动程序能够识别和操作这些设备。
break;
}
}
// 设置 root 节点的 OF_POPULATED_BUS 标志
of_node_set_flag(root, OF_POPULATED_BUS);
// 释放 root 节点的引用计数
of_node_put(root);
return rc;
}
EXPORT_SYMBOL_GPL(of_platform_populate);

3.3.5 of_platform_bus_create

接下来看一下 of_platform_bus_create() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
static int of_platform_bus_create(struct device_node *bus,
const struct of_device_id *matches,
const struct of_dev_auxdata *lookup,
struct device *parent, bool strict)
{
const struct of_dev_auxdata *auxdata;
struct device_node *child;
struct platform_device *dev;
const char *bus_id = NULL;
void *platform_data = NULL;
int rc = 0;

/* Make sure it has a compatible property */
// 确保设备节点具有 compatible 属性
if (strict && (!of_get_property(bus, "compatible", NULL))) {
pr_debug("%s() - skipping %pOF, no compatible prop\n",
__func__, bus);
return 0;
}

/* Skip nodes for which we don't want to create devices */
// 跳过不想创建设备的节点
if (unlikely(of_match_node(of_skipped_node_table, bus))) {
pr_debug("%s() - skipping %pOF node\n", __func__, bus);
return 0;
}

if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
pr_debug("%s() - skipping %pOF, already populated\n",
__func__, bus);
return 0;
}

auxdata = of_dev_lookup(lookup, bus);
if (auxdata) {
bus_id = auxdata->name;
platform_data = auxdata->platform_data;
}

if (of_device_is_compatible(bus, "arm,primecell")) {
/*
* Don't return an error here to keep compatibility with older
* device tree files.
*/
// 在此处不返回错误以保持与旧设备树文件的兼容性。
of_amba_device_create(bus, bus_id, platform_data, parent);
return 0;
}

dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
if (!dev || !of_match_node(matches, bus))
return 0;

for_each_child_of_node(bus, child) {
pr_debug(" create child: %pOF\n", child);
rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);
if (rc) {
of_node_put(child);
break;
}
}
of_node_set_flag(bus, OF_POPULATED_BUS);
return rc;
}

365 行: 如果 strict 为真且设备节点 bus 没有兼容性属性, 则输出调试信息并返回 0。 这个条件判断确保设备节点具有 compatible 属性, 因为 compatible 属性用于匹配设备驱动程序,对应第 1 条规则。

372 行: 如果设备节点 bus 在被跳过的节点表中, 则输出调试信息并返回 0。 这个条件判断用于跳过不想创建设备的节点。

377 行: 如果设备节点 bus 的 OF_POPULATED_BUS 标志已经设置, 则输出调试信息并返回 0。 这个条件判断用于避免重复创建已经填充的设备节点。

383 行: 使用 lookup 辅助数据结构查找设备节点 bus 的特定配置信息, 并将其赋值给变量 bus_id 和 platform_data。 这个步骤用于获取设备节点的特定配置信息, 以便在创建平台设备时使用, 由于这里传入的参数为 NULL, 所以下面的条件判断并不会被执行。

389 行: 如果设备节点 bus 兼容于 “arm,primecell”, 则调用 of_amba_device_create()函数创建 AMBA 设备, 并返回 0, 对应第 3 条规则。

398 行: 调用 of_platform_device_create_pdata()函数创建平台设备, 并将其赋值给变量dev。 然后, 检查设备节点 bus 是否与给定的匹配表 matches 匹配。 如果平台设备创建失败或者设备节点不匹配, 那么返回 0。

402 行 - 第 409 行: 遍历设备节点 bus 的每个子节点 child, 并递归调用 of_platform_bus_create()函数来创建子节点的平台设备。

3.3.6  of_platform_device_create_pdata()

再来看一下 of_platform_device_create_pdata() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np,
const char *bus_id,
void *platform_data,
struct device *parent)
{
struct platform_device *dev;
// 检查设备节点是否可用或已填充
if (!of_device_is_available(np) ||
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;
// 分配平台设备结构体
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;
// 设置平台设备的一些属性
dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
if (!dev->dev.dma_mask)
dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
dev->dev.bus = &platform_bus_type;
dev->dev.platform_data = platform_data;
of_msi_configure(&dev->dev, dev->dev.of_node);
// 将平台设备添加到设备模型中
if (of_device_add(dev) != 0) {
platform_device_put(dev);
goto err_clear_flag;
}

return dev;

err_clear_flag:
//清除设备节点的已填充标志
of_node_clear_flag(np, OF_POPULATED);
return NULL;
}

180 行: 函数会检查设备节点的可用性, 即检查设备树对应节点的 status 属性。 如果设备节点不可用或已经被填充, 则直接返回 NULL。

184 行: 函数调用 of_device_alloc 分配一个平台设备结构体, 并将设备节点指针、 设备标识符和父设备指针传递给它。 如果分配失败, 则跳转到 err_clear_flag 标签处进行错误处理。

188 - 193行, 函数设置平台设备的一些属性。 它将 coherent_dma_mask 属性设置为 32 位的DMA 位掩码, 并检查 dma_mask 属性是否为 NULL。 如果 dma_mask 为 NULL, 则将其指向 coherent_dma_mask。 然后, 函数设置平台设备的总线类型为 platform_bus_type, 并将平台数据指针存储在 platform_data 属性中。 接着, 函数调用 of_msi_configure 来配置设备的 MSI 信息。

195 行: 函数调用 of_device_add 将平台设备添加到设备模型中。 如果添加失败, 则释放已分配的平台设备, 并跳转到 err_clear_flag 标签处进行错误处理。

3.3.7 总结

image-20250225104457278

参考资料:

Device Tree (二) - dtb格式_dtb文件-CSDN博客

设备树处理之——device_node转换成platform_device【转】 - Sky&Zhang - 博客园

linux的initcall机制 - 牧野星辰 - 博客园