&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

    • parentchildsibling 这三个指针共同构建了整个设备树在内存中的树状多叉拓扑结构。

      • 向下遍历 (SPI 主机驱动探测设备):当 SPI 控制器驱动(如 spi-imx.c)加载时,它会拿到 ecspi3device_node,然后顺着 child 指针往下找,发现了 icm20608 节点。

      • 横向遍历 (多设备挂载):如果你的 ecspi3 下面不仅挂了 ICM20608,还在片选 1 (reg = <1>) 上挂了另一个设备(比如你在 &ecspi1 里注释掉的 spidev1),那么 icm20608 节点的 sibling 指针就不会是 NULL,而是指向那个新设备的 device_node

      • 向上回溯 (子设备获取主设备资源)icm20608 的驱动可能需要知道挂在哪个 SPI 总线上,它可以通过 parent 指针轻松找到 ecspi3 节点,进而获取 SPI 控制器的操作句柄。

  • properties (属性链表): 只会显示同个{}内的,子{}不算

    • icm20608@0 包含三个属性:compatiblespi-max-frequencyreg

      • 内核分配这三个 struct property 的内存后,通过它们的 next 成员串接在一起。

      • 链表的头部交给了 icm_node->properties

      • 驱动调用 of_property_read_u32(np, "spi-max-frequency", &val) 时,内核实际上就是在跑一个 while 循环,顺着这个 next 链表一直找,直到对比发现某个节点的 name == "spi-max-frequency",然后把它的 value 提取出来。

  • namefull_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
  1. uboot进入内核之前,将 DTB 文件在内存中的物理首地址强制存入 CPU 的 r2 寄存器

  2. 在head.S文件中,验证合法性:代码会调用 __vet_atags 函数,去读取 r2 指向内存的前 4 个字节,判断是否等于设备树的魔数(Magic Number: 0xd00dfeed)。校验通过后,r2 寄存器继续保留该地址。

  3. 在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_nodeproperty两个核心结构体共同交织形成的网络里的。

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;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐