设备树展开源码解析---基于imx6ull
&ecspi3 {
pinctrl-names = "default";
pinctrl-0 = <&my_pinctrl_ecspi3>;
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
status = "okay";
spidev: icm20608@0{
compatible = "my-icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
};
};
上面是SPI的icm20608芯片的设备树文件,下面研究一下他是如何展开,然后与驱动适配的?
1. 设备树展开流程
-
根据所学知识,我们所编写的
dts加上DTSI源文件,使用DTC工具编译成DTB二进制文件,最后打包为boot.img 烧录在板子上 -
在uboot阶段放在内存某个地址上
-
在内核初始化的时候会将dtb文件展开为可以识别的设备树文件(device_node结构体)
-
根据转换规则把展开的设备树中的节点转换成platform_device

1.1 device_node:
struct device_node {
const char *name; /* 设备节点的名称。通常是从设备树源文件中的 "node-name@unit-address" 提取出的 "node-name" 部分 */
const char *type; /* 设备的类型。对应设备树中的 "device_type" 属性(在现代的扁平设备树 FDT 中已不推荐使用,但在旧系统中仍可见) */
phandle phandle; /* 节点的唯一句柄(标识符)。当设备树中的其他节点需要引用此节点时(例如中断父节点或时钟源),就会使用这个 phandle 值 */
const char *full_name; /* 节点的完整路径名,例如 "/soc/serial@12340000" */
struct fwnode_handle fwnode; /* 统一固件节点句柄。内核用来抽象不同固件类型(如 Device Tree, ACPI, 软件节点)的通用接口 */
struct property *properties; /* 指向该节点下包含的所有属性(properties)单向链表的表头 */
struct property *deadprops; /* 指向被移除的属性链表。主要用于支持设备树的动态修改和热插拔(Device Tree Overlays) */
struct device_node *parent; /* 指向该节点的父节点 */
struct device_node *child; /* 指向该节点的第一个子节点 */
struct device_node *sibling; /* 指向该节点的下一个兄弟节点(与该节点拥有相同父节点的节点) */
struct kobject kobj; /* 基础内核对象。用于内核的引用计数机制,并且负责将该设备树节点在用户空间的 sysfs 中呈现(通常在 /sys/firmware/devicetree/ 下) */
unsigned long _flags; /* 节点的状态标志位,例如 OF_DYNAMIC(表示节点是动态分配的)、OF_DETACHED(表示节点已从树中分离)等 */
void *data; /* 通用指针,供特定的子系统或驱动程序附加私有数据使用 */
#if defined(CONFIG_SPARC) /* 以下是专供 SPARC 架构使用的遗留/特定字段 */
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
struct device_node全景拓扑图
▲
│ parent (指向上一级总线节点,如 aips-bus)
│
┌───────────────────────────┴───────────────────────────┐
│ struct device_node (代表 &ecspi3 节点) │
├───────────────────────────────────────────────────────┤
│ name = "spi" │
│ full_name = "/soc/aips-bus@30000000/spi@.../ecspi3" │
│ │
│ properties ─────┐ │
│ │ │
│ sibling ────────┼─────────────────► (指向同级设备,如 &uart1 或 &i2c1 的 device_node)
│ │ │
│ child ────┐ │ │
└───────────┼─────┼─────────────────────────────────────┘
│ │
│ │ ┌────────────────────────────────┐ ┌────────────────────────────────┐
│ └──►│ struct property │ next │ struct property │
│ ├────────────────────────────────┤──────►├────────────────────────────────┤───► ... ──► NULL
│ │ name = "pinctrl-names" │ │ name = "status" │
│ │ length = 8 │ │ length = 5 │
│ │ value = "default\0" │ │ value = "okay\0" │
│ └────────────────────────────────┘ └────────────────────────────────┘
│
│ (指向该总线下的第一个从设备)
▼
┌───────────────────────────┴───────────────────────────┐
│ struct device_node (代表 spidev: icm20608@0 子节点) │
├───────────────────────────────────────────────────────┤
│ name = "icm20608" │
│ full_name = "/soc/.../spi@.../icm20608@0" │
│ │
│ parent ───────────► (指回上方的 ecspi3 节点的首地址) │
│ │
│ child ────────────► NULL (传感器是叶子节点,无子节点) │
│ │
│ sibling ──────────► NULL (ecspi3下仅挂载了这1个设备) │
│ │
│ properties ─────┐ │
└─────────────────┼─────────────────────────────────────┘
│
│ ┌────────────────────────────────┐ ┌────────────────────────────────┐
└──►│ struct property │ next │ struct property │
├────────────────────────────────┤──────►├────────────────────────────────┤───► (指向 "reg" 属性) ──► NULL
│ name = "compatible" │ │ name = "spi-max-frequency" │
│ length = 12 │ │ length = 4 │
│ value = "my-icm20608\0" │ │ value = 0x00 0x7A 0x12 0x00 │
└────────────────────────────────┘ └────────────────────────────────┘
-
在include/linux/of.h
-
parent、child和sibling这三个指针共同构建了整个设备树在内存中的树状多叉拓扑结构。-
向下遍历 (SPI 主机驱动探测设备):当 SPI 控制器驱动(如
spi-imx.c)加载时,它会拿到ecspi3的device_node,然后顺着child指针往下找,发现了icm20608节点。 -
横向遍历 (多设备挂载):如果你的
ecspi3下面不仅挂了 ICM20608,还在片选 1 (reg = <1>) 上挂了另一个设备(比如你在&ecspi1里注释掉的spidev1),那么icm20608节点的sibling指针就不会是 NULL,而是指向那个新设备的device_node。 -
向上回溯 (子设备获取主设备资源):
icm20608的驱动可能需要知道挂在哪个 SPI 总线上,它可以通过parent指针轻松找到ecspi3节点,进而获取 SPI 控制器的操作句柄。
-
-
-
properties(属性链表): 只会显示同个{}内的,子{}不算-
icm20608@0包含三个属性:compatible、spi-max-frequency和reg。-
内核分配这三个
struct property的内存后,通过它们的next成员串接在一起。 -
链表的头部交给了
icm_node->properties。 -
驱动调用
of_property_read_u32(np, "spi-max-frequency", &val)时,内核实际上就是在跑一个while循环,顺着这个next链表一直找,直到对比发现某个节点的name == "spi-max-frequency",然后把它的value提取出来。
-
-
-
name和full_name:-
name:会被解析为"icm20608"(即@符号之前的部分)。 -
full_name:包含节点的绝对路径,例如"/soc/aips-bus@30000000/spi@30820000/icm20608@0"(具体路径取决于&ecspi3在整个设备树中的位置)。这在内核打印 Debug 日志或通过 sysfs (/sys/firmware/devicetree/base/...) 查看节点时非常有用。
-
1.2 property
struct property {
char *name; /* 属性的名称(键),例如 "compatible", "reg", "spi-max-frequency", "status" 等。通常是一个以 null 结尾的字符串。 */
int length; /* 属性值(value)的长度,以字节(bytes)为单位。由于设备树中的值可以是任意类型(字符串、32位整数、数组或为空),内核需要知道具体的字节数来正确读取。 */
void *value; /* 指向属性实际数据的指针。
* 注意:设备树中的数据(尤其是数字)通常以大端序(Big-Endian)存储。
* 驱动程序在读取时,必须使用 of_property_read_u32() 等内核提供的专用 API,
* 这些 API 会自动处理数据类型的转换和大小端对齐。 */
struct property *next; /* 指向下一个属性的指针。一个设备树节点(device_node)下的所有属性,就是通过这个指针串联成一个单向链表,链表头挂在 device_node 的 properties 成员上。 */
unsigned long _flags; /* 属性的状态标志位。类似于 device_node 的 _flags,用于标记该属性是否是动态分配的(OF_DYNAMIC)、是否已分离等,主要用于设备树重载(DT Overlays)时的内存管理。 */
unsigned int unique_id; /* 内核为该属性分配的全局唯一标识符(通常用于特定的架构或内核内部的哈希追踪)。 */
struct bin_attribute attr; /* 二进制 sysfs 属性。
* 这个成员非常关键:它负责将该属性导出到用户空间。
* 你在 Linux 终端中查看 /sys/firmware/devicetree/base/... 目录下的文件时,
* 每一个文件(如 compatible)底层就是由这个 attr 支撑的,允许你通过 cat 命令或 hexdump 读取出 value 中的原始二进制数据。 */
};
icm20608@0 节点的内存全景图
[struct device_node: icm_node]
├── name = "icm20608"
├── full_name = "/.../spi@.../icm20608@0"
└── properties ─────┐
│
▼
[struct property: P1]
├── name = "compatible"
├── length = 12
├── value = "my-icm20608\0"
└── next ──────┐
│
▼
[struct property: P2]
├── name = "spi-max-frequency"
├── length = 4
├── value = 0x00 0x7A 0x12 0x00 (8000000的大端序)
└── next ──────┐
│
▼
[struct property: P3]
├── name = "reg"
├── length = 4
├── value = 0x00 0x00 0x00 0x00
└── next = NULL (链表结束)
-
compatible
name: 指向字符串"compatible"。length:12。因为"my-icm20608"有 11 个字符,加上字符串结尾的\0,总共占用 12 个字节。value: 指向一段 12 字节大小的内存区域,里面存着"my-icm20608\0"。next: 指向下一个属性(spi-max-frequency的结构体首地址)。 -
spi-max-frequency:
name: 指向字符串"spi-max-frequency"。length:4。因为设备树中的< >语法代表一个 32 位的整数(u32),占用 4 个字节。value: 指向一段 4 字节的内存。里面存的是十进制 8000000(十六进制 0x007A1200)的大端序(Big-Endian)原始二进制数据。next: 指向下一个属性(reg的结构体首地址)。 -
reg:
name: 指向字符串"reg"。length:4。(同样是一个 32 位整数)。value: 指向一段 4 字节的内存,里面存的是0x00000000。next:NULL。假设这是最后一个属性,链表到此结束。
2. 设备树二进制文件展开过程源码分析
2.1 setup_arch
-
dtb文件最后会展开成device_node这个文件格式,那么他是如何展开的?
-
先看
start_kernel(), 他是内核启动的入口函数-
init/main.c
-
找到API:
setup_arch(&command_line); //架构相关的初始化:-
它的核心任务之一就是完成设备树的早期解析和展开
-
arch/arm64/kernel/setup.c
-
setup_machine_fdt(__atags_pointer); // 设置平台设备树
-
中间进行内存分配、分页初始化、命令行解析
-
unflatten_device_tree(); // 展开设备树
-
void __init setup_arch(char **cmdline_p) { const struct machine_desc *mdesc; setup_processor(); /* * 【设备树核心 API 1:早期扁平设备树解析】 * U-Boot 会将设备树编译后的二进制文件(DTB)的物理地址存放在某个寄存器中(如 r2), * 内核启动时将其保存在 __atags_pointer 变量里。 * setup_machine_fdt() 会去该地址检查 DTB 的魔数(Magic Number, 0xd00dfeed), * 如果确认是合法的 DTB,它会扫描 DTB 的 root 节点,读取 "compatible" 属性, * 并在内核编译的 machine_desc 数组中寻找最匹配的单板描述结构体(mdesc)。 */ mdesc = setup_machine_fdt(__atags_pointer); if (!mdesc) /* 如果上面的 DTB 校验失败,退回到使用旧的 ATAGS(Tag 列表)方式启动 */ mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type); machine_desc = mdesc; machine_name = mdesc->name; dump_stack_set_arch_desc("%s", mdesc->name); // ... [省略中间的内存分配、分页初始化、命令行解析等代码] ... /* * 【设备树核心 API 2:设备树的“展开” (Unflattening)】 * 这是设备树生命周期中最核心的一步! * 在此之前,设备树仅仅是内存中的一块连续二进制数据(Flattened Device Tree, FDT)。 * unflatten_device_tree() 函数会解析这块二进制内存,并动态分配内存, * 构建我们在前面讨论过的 struct device_node 树和 struct property 链表。 * 执行完这个函数后,内核中的设备树拓扑结构才真正建立起来。 */ unflatten_device_tree(); /* * 【设备树特定功能 API 3:初始化 CPU 拓扑】 * 在设备树展开成 device_node 树之后,这个函数会去遍历设备树中的 "cpus" 节点。 * 它通过读取 "cpus" 节点下的子节点(cpu@0, cpu@1 等)及其 "reg" 属性, * 获知当前 SoC 有几个 CPU 核心,以及它们的物理 ID (MPIDR),从而初始化内核的 CPU 映射图。 */ arm_dt_init_cpu_maps(); /* * 【设备树特定功能 API 4:初始化 PSCI 电源管理】 * 解析设备树中的 "psci" 节点(Power State Coordination Interface)。 * 提取其中的 method(如 "smc" 或 "hvc"),以便后续内核可以通过这些接口 * 调用 TrustZone/Secure Monitor 来实现 CPU 的休眠、唤醒、热插拔等电源管理操作。 */ psci_dt_init(); #ifdef CONFIG_SMP // ... [SMP 对称多处理器的初始化逻辑] ... #endif // ... [省略后续的 IRQ 和 Console 初始化代码] ... if (mdesc->init_early) mdesc->init_early(); } -
根据上面的分析,设备树展开的核心API是setup_machine_fdt 和 unflatten_device_tree, 下面来讲解分析
2.2 setup_machine_fdt
mdesc = setup_machine_fdt(__atags_pointer);
2.2.1 __atags_pointer传参
-
__atags_pointer: 保存的是U-Boot 传递给 Linux 内核的 DTB在系统 内存 中的物理首地址。 -
在arch/arm/kernel/head-common.S 文件中对__atags_pointer进行赋值
-
arch/arm/kernel/head.S 对其验证合法性
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
-
uboot进入内核之前,将 DTB 文件在内存中的物理首地址强制存入 CPU 的 r2 寄存器
-
在head.S文件中,验证合法性:代码会调用
__vet_atags函数,去读取r2指向内存的前 4 个字节,判断是否等于设备树的魔数(Magic Number:0xd00dfeed)。校验通过后,r2寄存器继续保留该地址。 -
在head-common.S,进行查表: 内核建立好初始页表并开启 MMU 后,跳转到
__mmap_switched执行。汇编代码会读取定义在文件末尾的__mmap_switched_data数据表。这个表是由链接器生成的,里面准确记录了 C 语言全局变量__atags_pointer的内存地址。代码将这个变量的内存地址加载到r6寄存器中。
__mmap_switched:
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
- dtb的赋值
r2:保存着 U-Boot 传来的 DTB 物理地址。
r6:保存着全局变量 __atags_pointer 的内存地址。
汇编代码执行一条指令:str r2, [r6]。这直接把 r2 里的 DTB 地址值,硬写入了 __atags_pointer 所在的 内存 空间。
当后续代码跳转到 C 语言环境(如
start_kernel->setup_arch)时,C 代码直接读取__atags_pointer变量,拿到的就是完整的 DTB 地址
2.2.2 setup_machine_fdt源码
-
phys_to_virt(): 将设备树的物理地址转换成虚拟地址 -
of_flat_dt_match_machine:-
读取刚传入的 DTB 文件的根节点
-
提取出根节点的 “compatible” 属性(例如你的 i.MX6ULL 里的 “fsl,imx6ull-14x14-evk”)。
-
然后,拿着这个字符串,遍历内核源码中所有通过 MACHINE_START / DT_MACHINE_START 注册的单板结构体,看看哪个结构体的 dt_compat 列表里包含这个字符串。找到了就返回那个结构体。
-
-
early_init_dt_scan_nodes:-
此时内存分配器还没就绪,不能完整展开整棵树。这里只会“扫描”几个关乎内核能否继续启动的特殊节点:
-
/chosen 节点:提取 bootargs 命令行参数(比如 console=ttymxc0,115200 root=/dev/mmcblk1p2)。
-
/ 根节点:提取 #address-cells 和 #size-cells。
-
/memory 节点:获取物理内存的起始地址和大小,存入 memblock,为下一步初始化内存分配器做准备。
-
-
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
const struct machine_desc *mdesc, *mdesc_best = NULL;
#if defined(CONFIG_ARCH_MULTIPLATFORM) || defined(CONFIG_ARM_SINGLE_ARMV7M)
/* * 1. 准备通用备选方案:
* 现代 ARM 内核通常支持“多平台(Multi-platform)”编译(即一个内核镜像可以跑在不同厂家的板子上)。
* 这里定义了一个通用的 machine_desc,作为后续匹配的默认 fallback(备胎)。
*/
DT_MACHINE_START(GENERIC_DT, "Generic DT based system")
.l2c_aux_val = 0x0,
.l2c_aux_mask = ~0x0,
MACHINE_END
mdesc_best = &__mach_desc_GENERIC_DT;
#endif
/* * 2. 核心校验:
* 如果 U-Boot 传来的物理地址为空,或者早期设备树校验失败(找不到 0xd00dfeed 魔数),直接返回 NULL。
* 注意:此时 MMU 刚开启,phys_to_virt() 用于将 DTB 的物理地址转换为早期的虚拟地址,以便 C 语言指针读取。
*/
if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
return NULL;
/* * 3. 核心匹配:
* 这是最关键的一步!它会去读取刚传入的 DTB 文件的根节点(即 DTS 文件中的 "/"),
* 提取出根节点的 "compatible" 属性(例如你的 i.MX6ULL 里的 "fsl,imx6ull-14x14-evk")。
* 然后,拿着这个字符串,遍历内核源码中所有通过 MACHINE_START / DT_MACHINE_START 注册的单板结构体,
* 看看哪个结构体的 dt_compat 列表里包含这个字符串。找到了就返回那个结构体。
*/
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
/* * 4. 匹配失败的死机处理:
* 如果遍历了内核里所有的单板,都没找到匹配的,说明当前内核不支持 U-Boot 传来的这块板子。
*/
if (!mdesc) {
const char *prop;
int size;
unsigned long dt_root;
early_print("\nError: unrecognized/unsupported "
"device tree compatible list:\n[ ");
/* 打印出当前 DTB 根节点的 compatible 字符串,告诉开发者是哪个板子不被支持 */
dt_root = of_get_flat_dt_root();
prop = of_get_flat_dt_prop(dt_root, "compatible", &size);
while (size > 0) {
early_print("'%s' ", prop);
size -= strlen(prop) + 1;
prop += strlen(prop) + 1;
}
early_print("]\n\n");
/* 打印内核当前支持的所有单板列表,然后死机挂起(does not return) */
dump_machine_table();
}
/* * 5. 执行特定单板的补丁钩子:
* 有些早期的 Bootloader 给出的设备树可能有 Bug,
* 如果匹配到的单板结构体定义了 dt_fixup 函数,就在解析前先调用它修复一下设备树数据。
*/
if (mdesc->dt_fixup)
mdesc->dt_fixup();
/* * 6. 早期节点扫描(极度重要):
*
*/
early_init_dt_scan_nodes();
/* * 7. 兼容旧时代:
* 将解析到的新式 device tree 对应的机器号,赋值给旧式 ATAGS 时代的全局变量。
*/
__machine_arch_type = mdesc->nr;
/* 返回最匹配的单板描述结构体给 setup_arch() */
return mdesc;
}
2.3 unflatten_device_tree
-
__unflatten_device_tree: 解析设备树-
initial_boot_params: dtb的虚拟地址
-
of_root: device_node的根节点
-
early_init_dt_alloc_memory_arch: 为device_node和property分配地址
-
void __init unflatten_device_tree(void)
{
/* * 【核心步骤 1:浴火重生,展开成树】
* * - initial_boot_params: 这个全局指针指向的就是 U-Boot 传进来的、
* 被 early_init_dt_verify() 验证过的原始 DTB 内存区。
* - &of_root: 这是整个 Linux 设备树系统的“万物之源”(指向 '/' 根节点)。
* 展开成功后,这个全局变量被赋值。以后不论是 SPI 核心层还是 I2C 核心层,
* 在遍历总线寻找设备时,归根结底都是顺着 of_root 往下找的。
* - early_init_dt_alloc_memory_arch: 内存分配回调函数。因为现在内存系统建好了,
* 内核终于可以肆无忌惮地用它来为成百上千的
* device_node 和 property 结构体申请内存了。
*/
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
/* * 【核心步骤 2:建立全局快捷方式 (Aliases)】
* * 树虽然建好了,但节点的绝对路径(full_name)通常非常长,
* 比如 "/soc/aips-bus@30000000/spi@30820000"。
* 这个函数会专门去扫描设备树里的 "/aliases" 节点,把长路径映射为短别名。
* 这样,SPI 或 I2C 驱动在注册控制器时,就能直接通过 "spi3" 或 "i2c1"
* 这样的别名,快速自动分配到对应的总线编号(Bus Number 3 或 1),而不需要硬编码。
*/
of_alias_scan(early_init_dt_alloc_memory_arch);
}
2.3.1 __unflatten_device_tree
-
第一遍扫描,计算大小
- unflatten_dt_nodes收到的第二个参数传的是 NULL。只会计算展开dtb需要的内存
-
为展开的设备树分配内存
-
第二遍扫描,实际展开设备树
- 把 device_node 和 property 结构体挨个填入,并建立好 parent/child 等指针联系
static void *__unflatten_device_tree(const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
int size;
void *mem;
pr_debug(" -> unflatten_device_tree()\n");
/* 1. 安全检查:确认 U-Boot 传来的 dtb 指针是否为空 */
if (!blob) {
pr_debug("No device tree pointer\n");
return NULL;
}
pr_debug("Unflattening device tree:\n");
pr_debug("magic: %08x\n", fdt_magic(blob));
pr_debug("size: %08x\n", fdt_totalsize(blob));
pr_debug("version: %08x\n", fdt_version(blob));
/* 2. 头部校验:验证魔数 0xd00dfeed 以及版本号等信息 */
if (fdt_check_header(blob)) {
pr_err("Invalid device tree blob header\n");
return NULL;
}
/* *
* 【核心步骤 1:第一遍扫描 (First pass) - 只算账不花钱】
* 注意这里的第二个参数传的是 NULL。
* 当 unflatten_dt_nodes 收到 NULL 的内存指针时,它不会真的去创建树,
* 而是遍历一遍完整的 DTB,精确计算出:如果要把这个设备树全部展开,
* 一共需要多少字节的内存(包括所有的 device_node、property 以及字符串)。
* 返回的 size 就是总需求量。
*/
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
if (size < 0)
return NULL;
/* 内存按 4 字节对齐 */
size = ALIGN(size, 4);
pr_debug(" size is %d, allocating...\n", size);
/* *
* 【核心步骤 2:一次性分配整块内存】
* 拿着刚才算出的 size,向系统的内存分配器(比如 memblock 早期分配器)
* 一次性申请一整大块连续的内存。
* 注意:这里多申请了 4 个字节 (size + 4),用于后面的安全校验。
*/
mem = dt_alloc(size + 4, __alignof__(struct device_node));
if (!mem)
return NULL;
/* 清空这块刚申请到的内存 */
memset(mem, 0, size);
/* *
* 【核心技巧:设置“内存金丝雀” (Canary) 防越界】
* 在申请到的总内存的最末尾 4 个字节,写上一个特殊的魔数 0xdeadbeef。
* 如果后面的代码在填充数据时写越界了,这个标志就会被覆盖掉。
*/
*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
pr_debug(" unflattening %p...\n", mem);
/* *
* 【核心步骤 3:第二遍扫描 (Second pass) - 真正入驻内存】
* 再次调用 unflatten_dt_nodes,这次把刚刚申请到的整块大内存的首地址 mem 传了进去。
* 函数会再次遍历 DTB,并在这个大内存块里“切蛋糕”,
* 把 device_node 和 property 结构体挨个填入,并建立好 parent/child 等指针联系。
* 最终,完整的树就构建在这块大内存里了。
*/
unflatten_dt_nodes(blob, mem, dad, mynodes);
/* *
* 【越界检查】
* 树建完了,检查一下内存最后那 4 个字节。
* 如果不是 0xdeadbeef 了,说明上面的分配算法算漏了,或者填充逻辑有 Bug,
* 导致内存写溢出了,打印严重警告。
*/
if (be32_to_cpup(mem + size) != 0xdeadbeef)
pr_warning("End of tree marker overwritten: %08x\n",
be32_to_cpup(mem + size));
/* * 处理设备树动态覆盖 (Overlay) 的特殊情况。
* 如果是动态加载的局部设备树,打上 OF_DETACHED 标志,说明它暂时还没挂到全局树上。
*/
if (detached && mynodes) {
of_node_set_flag(*mynodes, OF_DETACHED);
pr_debug("unflattened tree is detached\n");
}
pr_debug(" <- unflatten_device_tree()\n");
return mem;
}
2.3.2 unflatten_dt_nodes
-
fdt_next_node: 遍历所获取的设备树的每一个节点,
-
populate_node: 填充节点信息,构造每个device_node下的property
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
/* fpsizes 用于记录每一层节点的 full_name 字符串长度,方便在分配内存时精确计算 */
unsigned int fpsizes[FDT_MAX_DEPTH];
/* nps (Node Pointers) 是一个极其精妙的设计:
* 它像一个栈,记录着从根节点到当前所处深度的所有父节点指针。
* 当解析到一个新节点时,nps[depth] 就是它的亲生父亲,直接挂上去即可。
*/
struct device_node *nps[FDT_MAX_DEPTH];
void *base = mem;
/* * 【核心判决】:dryrun (空跑/演习模式)
* 如果 mem 为 NULL(第一遍扫描),dryrun 为 true,全程只计算大小,不写内存。
* 如果 mem 有值(第二遍扫描),dryrun 为 false,真刀真枪地向 mem 指向的地址填充数据。
*/
bool dryrun = !base;
if (nodepp)
*nodepp = NULL;
/*
* 处理设备树片段(Device Tree Overlay)的情况。
* 如果传入了 dad (父节点),说明这不是在一张白纸上建树,而是在已有的树枝上长新芽。
* 将 depth 设为 1,是为了配合下面 fdt_next_node 函数的退出机制。
*/
if (dad)
depth = initial_depth = 1;
root = dad;
/* 初始化栈顶元素(根节点或传入的父节点) */
fpsizes[depth] = dad ? strlen(of_node_full_name(dad)) : 0;
nps[depth] = dad;
/* * 【遍历整个扁平设备树 (FDT) 的主循环】
* fdt_next_node() 是 libfdt 库提供的方法,它按顺序读取 FDT 二进制块,
* 自动跳到下一个节点,并自动更新当前的深度 (depth)。
*/
for (offset = 0;
offset >= 0 && depth >= initial_depth;
offset = fdt_next_node(blob, offset, &depth)) {
/* 防止设备树层级太深(超过64层)导致内核栈溢出 */
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))
continue;
/* * 【干活的主力:populate_node】
* 这个函数负责处理当前偏移量 (offset) 处的这一个节点及其包含的所有属性。
* 如果是 dryrun 模式,它只是把所需的字节数累加到 mem 指针上(此时 mem 被当成了计数器)。
* 如果不是 dryrun,它就实打实地从 mem 取一块内存,填入 struct device_node,
* 并将 nps[depth+1] 指向刚建好的新节点,为下一层的子节点准备好“父亲”。
*/
fpsizes[depth+1] = populate_node(blob, offset, &mem,
nps[depth],
fpsizes[depth],
&nps[depth+1], dryrun);
/* 如果返回大小为0,说明解析出错,直接返回已计算的大小 */
if (!fpsizes[depth+1])
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;
}
/*
* 【链表倒序:保持 DTS 源码顺序】
* 为什么需要反转?
* 在 populate_node 中挂载子节点时,用的是经典的“头插法”构建单向链表。
* 这会导致最后解析的节点反而排在 child 链表的最前面(LIFO 后进先出)。
* 为了保证内核遍历节点的顺序和我们写 .dts 源文件时的顺序绝对一致(很多陈旧驱动强依赖这个顺序),
* 这里必须做一次链表的翻转。
*/
if (!dryrun)
reverse_nodes(root);
/* 返回本次扫描总共需要(或消耗)的字节数 */
return mem - base;
}
2.3.3 populate_node
-
unflatten_dt_alloc: 分配设备节点内存
-
populate_properties: 填充设备节点的属性信息
static unsigned int populate_node(const void *blob,
int offset,
void **mem,
struct device_node *dad,
unsigned int fpsize,
struct device_node **pnp,
bool dryrun)
{
struct device_node *np;
const char *pathp;
unsigned int l, allocl;
int new_format = 0;
/* 1. 从 FDT 二进制块中获取当前节点的原始名称及长度 */
pathp = fdt_get_name(blob, offset, &l);
if (!pathp) {
*pnp = NULL;
return 0;
}
/* allocl 用于记录全路径字符串需要占用的内存大小(+1 是为了字符串结尾的 '\0') */
allocl = ++l;
/* *
* 【核心逻辑 1:计算节点全路径 (full_name) 的长度】
* 在现代设备树版本(v16及以上)中,FDT 存储的是紧凑的相对节点名(如 "icm20608@0"),
* 而不是完整的绝对路径(如 "/soc/aips-bus@.../spi@.../icm20608@0")。
* 为了在内存中还原完整的全路径,这里利用 fpsize 累加父节点的路径长度,
* 提前计算出拼接出完整 full_name 所需要的精确字节数。
*/
if ((*pathp) != '/') {
new_format = 1;
if (fpsize == 0) {
/* 根节点 '/' 的特殊处理 */
fpsize = 1;
allocl = 2;
l = 1;
pathp = "";
} else {
/* 子节点:父节点路径长度 + 当前节点名长度 */
fpsize += l;
allocl = fpsize;
}
}
/* *
* 【核心技巧 2:内存合并分配 (Memory Layout Trick)】
* 这里非常精妙!内核不仅为 device_node 结构体本身分配了内存,
* 还把刚才算好的全路径字符串长度 (allocl) 直接加在了后面。
* 也就是说,device_node 结构体和它的 full_name 字符串在内存中是绝对连续的。
* 如果是 dryrun 模式,这个函数只是把需要的总大小加到 mem 计数器上。
*/
np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
__alignof__(struct device_node));
if (!dryrun) {
char *fn;
of_node_init(np); /* 初始化 kobject 等基础成员 */
/* 让 full_name 指针直接指向 device_node 结构体末尾紧挨着的那块内存 */
np->full_name = fn = ((char *)np) + sizeof(*np);
if (new_format) {
/* 重建全路径:先拷贝父节点的全路径,再拼接当前节点名 */
if (dad && dad->parent) {
strcpy(fn, dad->full_name);
#ifdef DEBUG
if ((strlen(fn) + l + 1) != allocl) {
pr_debug("%s: p: %d, l: %d, a: %d\n",
pathp, (int)strlen(fn),
l, allocl);
}
#endif
fn += strlen(fn);
}
*(fn++) = '/';
}
memcpy(fn, pathp, l); /* 拷贝当前节点的名字 */
/* *
* 【核心逻辑 3:构建父子与兄弟的指针网络(头插法)】
* 如果当前节点有父节点 (dad),把它挂载到父节点下。
* 注意这里的逻辑:
* 1. 当前节点的 sibling 指向父节点“原先的”第一个孩子 (dad->child)。
* 2. 父节点的 child 刷新为指向当前节点。
* 这意味着最新解析的节点,总是占据父节点 child 链表的第一个位置(后进先出 LIFO)。
* 这也完美解释了为什么在上一层的 unflatten_dt_nodes 结束时,必须调用 reverse_nodes() 翻转链表!
*/
if (dad != NULL) {
np->parent = dad;
np->sibling = dad->child;
dad->child = np;
}
}
/* *
* 【核心逻辑 4:解析并挂载属性 (Properties)】
* 调用专门的函数,解析这个节点下的大括号 {} 内所有的键值对
* (比如 `compatible = "my-icm20608"`, `reg = <0>` 等),
* 并在内部为它们分配 struct property,串联到 np->properties 链表上。
* 这一步极其关键,后续总线在匹配诸如 SPI 设备驱动时,完全依赖这里挂载的属性数据。
*/
populate_properties(blob, offset, mem, np, pathp, dryrun);
if (!dryrun) {
/* 从刚刚挂载好的属性链表中,单独把 "name" 和 "device_type" 提取出来,
* 赋值给 device_node 的专属快捷指针,方便内核其他地方快速读取。
*/
np->name = of_get_property(np, "name", NULL);
np->type = of_get_property(np, "device_type", NULL);
/* 容错处理:确保即使 FDT 中没有这些属性,指针也不会引发空指针异常 */
if (!np->name)
np->name = "<NULL>";
if (!np->type)
np->type = "<NULL>";
}
/* 将构建好的节点指针通过入参 pnp 返回给上层调用者 */
*pnp = np;
/* 返回累加后的路径深度大小,供解析下一层子节点时使用 */
return fpsize;
}
最后,DTB 展开后,数据是存储在
device_node和property这两个核心结构体共同交织形成的网络里的。
3. device_node转换成platform_device
dtb展开device_node后还不能给内核的platform_driver使用,为了能使用,需要将其转换为platform_device
3.1 转换规则
- 转换规则在我别的博客里面,或者读者请自行查找,本博客主要谈论源码分析
3.2 转换流程源码分析
3.2.1 of_platform_default_populate_init()
-
device_node转换成platform_device的入口函数
-
遍历设备树中的设备节点,并为每个符合条件的设备节点创建一个对应的platform_device结构体
static int __init of_platform_default_populate_init(void)
{
struct device_node *node;
/* * 【核心防线】
* 检查系统是否真的通过了前面说的 unflatten_device_tree 解析。
* 如果 of_root 为空(没有设备树),直接退出,什么也不做。
*/
if (!of_have_populated_dt())
return -ENODEV;
/*
* 【特例处理:处理崩溃日志内存区 (ramoops)】
* ramoops 是一种机制,用于在内核 Panic 死机时,把崩溃日志写到某块保留的 RAM 里,
* 这样重启后还能读出来查问题。
* * 为什么它要特事特办?
* 因为在你的 DTS 中,它通常被定义在 "/reserved-memory" 节点下。
* 按照 Linux 的默认规则,父节点如果没有 "compatible" 属性(就像 /reserved-memory),
* 内核的自动遍历机制就会直接跳过它,不去扫描它的子节点。
* 所以这里必须手动把它“捞”出来,通过 of_platform_device_create() 为它强制生成一个平台设备。
*/
node = of_find_node_by_path("/reserved-memory");
if (node) {
node = of_find_compatible_node(node, NULL, "ramoops");
if (node)
of_platform_device_create(node, NULL, NULL);
}
/* * 【宇宙大爆炸:全自动实例化平台设备】
* 这是整个函数、甚至整个内核设备树初始化中最震撼的一句代码!
* 它会从设备树的根节点('/')开始,向下遍历整棵 device_node 树。
* * 它的默认扫描规则非常清晰:
* 1. 扫描根节点下的所有直系子节点(一级设备),为它们分配并注册 platform_device。
* 2. 扫描那些带有特定 "compatible" 属性(如 "simple-bus", "simple-mfd" 等)的总线节点。
* 如果发现某个父节点是一条“简单总线”,它就会继续深入,去扫描挂在这条总线上的所有子节点,
* 并为它们也生成 platform_device。
*/
of_platform_default_populate(NULL, NULL, NULL);
return 0;
}
3.2.2 of_platform_default_populate
- of_platform_default_populate: 内核启动扫描, 携带一张至关重要的白名单匹配表of_default_bus_match_table
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 */
};
int of_platform_default_populate(struct device_node *root,
const struct of_dev_auxdata *lookup,
struct device *parent)
{
/* *
* 【核心调用:平台设备大批量孵化器】
* - root: 起始扫描的设备树节点。传 NULL 默认代表从根节点 '/' 开始扫描。
* - lookup: 辅助数据。早年 ARM 架构从平台文件向设备树过渡时,用来把旧的
* 时钟、调节器等数据强行绑到新设备树节点上的,现在新架构基本传 NULL。
* - parent: 父设备指针。传 NULL 表示这些新挂载的设备直接挂在 sysfs 的顶级目录。
*
* 【绝对核心:of_default_bus_match_table】
* 它是内核内置的一张“白名单匹配表”。这也是 default_populate 存在的唯一意义!
*/
return of_platform_populate(root, of_default_bus_match_table, lookup,
parent);
}
3.2.3 of_platform_populate
- 只遍历第一层子节点, 将任务交给of_platform_bus_create
/*
* 【1号函数:入口指挥官】
* 它的任务很简单:找到起始节点(通常是根节点 '/'),
* 然后遍历它的所有“第一层子节点”,把它们派发给巡逻兵。
*/
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;
/* 获取根节点。传 NULL 就默认从 '/' 开始 */
root = root ? of_node_get(root) : of_find_node_by_path("/");
if (!root)
return -EINVAL;
pr_debug("%s()\n", __func__);
pr_debug(" starting at: %s\n", root->full_name);
/* *
* 【核心动作】:只遍历第一层子节点!
* 这里只是一个扁平的 for 循环,它把第一层的子节点挨个交给了 of_platform_bus_create。
* 至于要不要继续往深层子节点挖,交给了下一个函数去判断。
*/
for_each_child_of_node(root, child) {
rc = of_platform_bus_create(child, matches, lookup, parent, true);
if (rc) {
of_node_put(child);
break;
}
}
/* 标记根节点所在的“总线”已经被扫描过了 */
of_node_set_flag(root, OF_POPULATED_BUS);
of_node_put(root);
return rc;
}
3.2.4 of_platform_bus_create
-
检查:
-
必须有 compatible 属性:如果没有这个属性,驱动根本无法与之匹配,生成设备毫无意义。
-
必须“未被处理过”:检查 OF_POPULATED_BUS 标志,防止重复创建。
-
特殊情况拦截:如果是 ARM AMBA 总线设备(arm,primecell),则特殊处理生成 amba_device,提前退出流程。
-
/*
* 【2号函数:递归巡逻兵】
* 这是整个流程中最具有智慧的函数。它负责“创建设备”以及“决定是否要向下递归”。
*/
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;
/* * 【防线 1:身份审查】
* 只有带有 "compatible" 属性的节点才配生成设备。
* 因为如果没有 compatible,驱动根本无法和它匹配,生成设备毫无意义。
*/
if (strict && (!of_get_property(bus, "compatible", NULL))) {
pr_debug("%s() - skipping %s, no compatible prop\n",
__func__, bus->full_name);
return 0;
}
if (of_node_check_flag(bus, OF_POPULATED_BUS)) {
pr_debug("%s() - skipping %s, already populated\n",
__func__, bus->full_name);
return 0;
}
/* 处理一些遗留的附加数据(给旧架构用的,现代 ARM 可忽略) */
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")) {
/* ARM AMBA 总线的特殊处理,生成 amba_device 而不是 platform_device */
of_amba_device_create(bus, bus_id, platform_data, parent);
return 0;
}
/* *
* 【核心动作 1:呼叫制造厂】
* 为当前正在遍历的这个节点,真正分配并注册一个 platform_device!
*/
dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
/* *
* 【核心动作 2:白名单审查与递归截断】
* 1. 如果 dev 没创建成功,或者...
* 2. 如果当前节点不在 matches 白名单(也就是上一问提到的 simple-bus 等)里!
* -> 直接 return 0,停止向下扫描!
* 这就是为什么 I2C 节点下的设备不会被误当成平台设备的原因。
*/
if (!dev || !of_match_node(matches, bus))
return 0;
/* *
* 【核心动作 3:递归深挖】
* 如果代码能走到这里,说明当前节点不仅生成了设备,而且它本身还是一条“白名单里的总线”(如 simple-bus)。
* 那么,继续遍历它的子节点,自己调用自己(递归),并把刚才为总线生成的 dev 当作子节点的 parent!
*/
for_each_child_of_node(bus, child) {
pr_debug(" create child: %s\n", child->full_name);
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;
}
3.2.5 of_platform_device_create_pdata
-
首先检查节点的 status 属性。如果设为 “disabled”,直接打回,不分配内存。只有状态为 okay 或可用的节点才能继续。
-
内核使用
kzalloc(通过of_device_alloc)为struct platform_device分配内存,并将其强制归类到 Linux 的platform总线上 (platform_bus_type)。 -
配置好 DMA 和中断后,调用
of_device_add(dev)将设备正式注册进内核的 kobject 体系。- 这一步会在
/sys/devices/platform/生成目录,并立刻触发 platform 总线的匹配机制。如果暗号(compatible)对上了,就会直接拉起对应驱动的probe()函数
- 这一步会在
/*
* 【3号函数:设备制造厂】
* 这里是真正的“产线”,负责分配内核内存,并将设备挂载到 Linux 驱动总线上。
*/
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;
/* *
* 【生死一刀:status 属性检查】
* of_device_is_available() 会去读设备树里这个节点的 status 属性。
* 如果 status 被写成了 "disabled",这里直接返回 NULL,不创建设备!
* 这也就是为什么我们在 DTS 里经常通过写 status = "okay" 来激活硬件。
*/
if (!of_device_is_available(np) ||
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;
/* 真的去 kzalloc 申请一个 struct platform_device 的内存 */
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;
/* * 将这个新设备的归属,强行绑定到 Linux 的 "platform" 总线上
*/
dev->dev.bus = &platform_bus_type;
dev->dev.platform_data = platform_data;
/* 配置硬件直接内存访问 (DMA) 和 消息信号中断 (MSI) */
of_dma_configure(&dev->dev, dev->dev.of_node);
of_msi_configure(&dev->dev, dev->dev.of_node);
/* *
* 【万物苏醒:注册到内核】
* 把这个组装好的 platform_device 正式塞进 Linux 的设备驱动模型 (kobject体系)。
* 这一步会:
* 1. 在 /sys/devices/platform/ 目录下为你生成对应的文件夹。
* 2. 唤醒 platform 总线,拿着它的 compatible 去配对所有的 platform_driver。
* 3. 一旦对上暗号,直接拉起驱动的 probe() 函数。硬件点亮!
*/
if (of_device_add(dev) != 0) {
of_dma_deconfigure(&dev->dev);
platform_device_put(dev);
goto err_clear_flag;
}
return dev;
err_clear_flag:
of_node_clear_flag(np, OF_POPULATED);
return NULL;
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)