3. 设备树编译与加载:DTC编译器、DTB二进制格式、内核解析流程

设备树从文本形式的 .dts 源文件到内核可识别的二进制 .dtb,再到内核解析并生成 struct device_node 树,经历了三个关键阶段:编译、加载、解析。理解这一完整链路,是进行寄存器配置分层重构的基础。

3.1 DTC编译器:从 .dts 到 .dtb

DTC(Device Tree Compiler)是设备树的官方编译器,负责将人类可读的 .dts 文本文件编译为紧凑的二进制 .dtb 文件。编译过程包含词法分析、语法解析、符号展开和二进制编码四个阶段。

关键命令示例:
# 编译 .dts 为 .dtb
dtc -I dts -O dtb -o my_board.dtb my_board.dts

# 反编译 .dtb 为 .dts(调试用)
dtc -I dtb -O dts -o my_board_decompiled.dts my_board.dtb

# 生成 .dtb 时保留符号信息(用于调试)
dtc -I dts -O dtb -@ -o my_board.dtb my_board.dts
编译优化提示: 生产环境中建议使用 -@ 选项保留符号,便于后续 overlay 动态加载;调试阶段可使用 -H epapr 指定头部格式。

DTC 编译流程的核心步骤:

  1. 预处理:展开 #include 和 /include/ 指令,合并所有包含的 .dtsi 文件。
  2. 词法/语法分析:解析节点、属性、值、标签等语法元素,构建抽象语法树(AST)。
  3. 符号解析:处理 &label 引用、phandle 分配、aliases 节点。
  4. 二进制编码:将 AST 转换为 DTB 格式,计算偏移量、填充对齐、生成 FDT 头部。

3.2 DTB二进制格式:结构解剖

DTB 文件采用 Flattened Device Tree(FDT)格式,由四个主要区域组成。理解其二进制布局,有助于在底层调试寄存器映射问题。

区域名称 偏移量 大小 内容说明
FDT Header 0x00 40 字节 魔数、总大小、各区域偏移量、版本号、boot_cpuid_phys
Memory Reservation Block header.off_mem_rsvmap 动态 保留内存区域列表(地址 + 大小),用于告知内核不要使用
Structure Block header.off_dt_struct 动态 节点树的核心数据:token(FDT_BEGIN_NODE / FDT_PROP / FDT_END_NODE 等)+ 内容
Strings Block header.off_dt_strings 动态 所有属性名的字符串池,属性值通过偏移量引用此处
对齐陷阱: Structure Block 中的每个 token 和属性值都必须 4 字节对齐。如果属性值长度不是 4 的倍数,DTC 会自动填充 0x00 字节。在手动构造或修改 DTB 时,忽略对齐会导致内核解析崩溃。

FDT Header 的关键字段:

  • magic:固定为 0xD00DFEED(大端序),用于校验文件合法性。
  • totalsize:DTB 文件总字节数。
  • off_dt_struct:Structure Block 相对于文件起始的偏移量。
  • off_dt_strings:Strings Block 的偏移量。
  • version 与 last_comp_version:当前版本(通常为 17)与兼容的最低版本(通常为 16)。
  • boot_cpuid_phys:引导 CPU 的物理 ID,SMP 系统使用。

3.3 内核解析流程:从 DTB 到 device_node

内核在启动早期(arch/arm/kernel/setup.c 或 drivers/of/fdt.c)完成 DTB 解析,主要分为三个阶段:

  1. 早期扫描(early_init_dt_scan):在 MMU 未开启或仅开启恒等映射时,直接扫描 DTB 内存镜像,提取 chosen 节点(bootargs、initrd)、memory 节点、reserved-memory 节点,用于建立早期内存映射。
  2. unflatten 展开(unflatten_device_tree):将扁平的 Structure Block 递归展开为内核的 struct device_node 树,分配内存、建立父子/兄弟链表、解析 phandle 为指针引用。
  3. 驱动匹配(of_platform_populate):遍历 device_node 树,对 compatible 属性匹配的节点创建 struct platform_device,触发驱动 probe。
内核关键函数调用链:
start_kernel()
  └─ setup_arch()
       └─ early_init_dt_scan_nodes()
            ├─ early_init_dt_scan_chosen()   // 解析 bootargs
            ├─ early_init_dt_scan_root()     // 获取 #address-cells / #size-cells
            └─ early_init_dt_scan_memory()   // 解析 memory 节点
  └─ unflatten_device_tree()
       └─ __unflatten_device_tree()
            ├─ unflatten_dt_nodes()          // 递归展开节点
            └─ of_alias_scan()               // 扫描 aliases 节点
  └─ arch_initcall(of_platform_default_populate_init)
       └─ of_platform_populate()
            └─ of_platform_bus_create()      // 递归创建 platform_device

3.4 设备树编译与加载全流程 SVG 架构图

设备树编译与加载全流程架构 阶段一:编译阶段(Host PC) .dts 源文件 .dtsi 包含文件 DTC 编译器 词法/语法/符号/编码 .dtb 二进制 阶段二:加载阶段(Bootloader → 内核) Bootloader 加载到内存 DTB 物理地址传递 内核入口(start_kernel) r2 寄存器传递 DTB 地址 魔数校验 0xD00DFEED 阶段三:解析阶段(内核 OF 子系统) 早期扫描 chosen / memory / reserved unflatten 展开 device_node 树构建 驱动匹配 platform_device 创建 驱动 Probe 寄存器配置 关键数据结构转换: .dts 文本 → DTB 二进制(FDT 格式)→ struct device_node(树形链表)→ struct platform_device(驱动绑定) 寄存器配置在 probe 阶段通过 of_iomap() / of_property_read_u32() 从 device_node 中提取。

3.5 内核解析关键数据结构

内核 OF(Open Firmware)子系统使用以下核心数据结构表示设备树:

数据结构 定义位置 作用 关键字段
struct device_node include/linux/of.h 表示一个设备树节点 name, full_name, phandle, parent, children, sibling, properties
struct property include/linux/of.h 表示节点的一个属性 name, value, length, next
struct platform_device include/linux/platform_device.h 驱动匹配后的设备实例 dev, num_resources, resource, id
struct resource include/linux/ioport.h 表示寄存器地址范围 start, end, flags(IORESOURCE_MEM / IORESOURCE_IRQ)
寄存器配置提取示例:
// 驱动 probe 函数中获取寄存器基址和中断号
static int my_driver_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    int irq;

    // 获取第 0 个寄存器区域(reg 属性的第一个条目)
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 获取中断号(interrupts 属性的第一个条目)
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 读取寄存器值示例
    u32 val = readl(base + 0x10);
    dev_info(&pdev->dev, "REG[0x10] = 0x%08x\n", val);

    return 0;
}

3.6 常见问题与调试方法

常见问题清单:
  • DTB 魔数错误:内核打印 "OF: fdt: magic: 0x%08x",检查 Bootloader 是否正确加载 DTB。
  • 版本不兼容:内核只支持 last_comp_version <= 16 的 DTB,使用 dtc -V 检查版本。
  • 属性对齐错误:内核在 of_find_property 时崩溃,检查 DTB 中属性值是否 4 字节对齐。
  • phandle 冲突:两个节点使用相同 phandle,内核在 of_phandle_cache_update 时覆盖。
  • reg 格式不匹配:#address-cells / #size-cells 与父节点不一致,导致地址解析错误。
调试命令速查:
# 查看 DTB 结构(内核启动后)
cat /proc/device-tree/  # 遍历所有节点
ls /sys/firmware/devicetree/base/  # sysfs 接口

# 反编译运行时 DTB
dtc -I fs -O dts -o runtime.dts /sys/firmware/devicetree/base/

# 内核 DTB 解析日志(开启 DEBUG)
echo 'file drivers/of/fdt.c +p' > /sys/kernel/debug/dynamic_debug/control
echo 'file drivers/of/base.c +p' > /sys/kernel/debug/dynamic_debug/control

3.7 本章小结

设备树的编译与加载是驱动架构中承上启下的关键环节:

  • DTC 编译器将 .dts 文本转换为紧凑的 .dtb 二进制,完成符号解析和编码。
  • DTB 格式包含 Header、Memory Reservation、Structure Block、Strings Block 四个区域,内核通过魔数 0xD00DFEED 校验合法性。
  • 内核解析经历早期扫描、unflatten 展开、驱动匹配三个阶段,最终将寄存器配置传递给驱动。
  • 理解这一流程,有助于在寄存器配置分层重构中定位 DTB 生成、加载、解析各环节的问题。