3. 设备树(DT)基础与3.x到5.x的变迁:从板级文件(board-*.c)到设备树(.dts)的迁移

3.1 为什么需要设备树?从“硬编码”到“数据驱动”

在 Linux 3.x 时代,ARM 架构的板级支持包(BSP)依赖大量的 board-*.c 文件。这些文件在 C 代码中硬编码了中断号、GPIO、内存地址、时钟频率等硬件资源信息。这种方式的弊端随着 ARM SoC 的爆发式增长而暴露无遗:

  • 代码冗余:同一 SoC 的不同板卡需要复制大量相似的 board-*.c 文件。
  • 内核与硬件耦合:更换一个 GPIO 或调整内存分区,必须重新编译内核。
  • 主线化困难:社区维护者难以接受大量重复的板级代码进入主线。

设备树(Device Tree, DT)的引入,将硬件描述从内核源码中剥离,变成一份独立的数据文件(.dts)。内核在启动时解析这份数据,动态创建平台设备、注册资源,实现了“一套内核二进制,适配多种板卡”的目标。

核心变迁: 从 arch/arm/mach-xxx/board-xxx.c 中的 platform_device 和 resource 结构体,迁移到 arch/arm/boot/dts/xxx.dts 中的节点与属性。

3.2 设备树核心语法速览

设备树源文件(.dts)采用树形结构描述硬件。每个硬件外设对应一个节点(node),节点内包含若干属性(property)。

// 一个典型的 I2C 控制器节点
i2c1: i2c@40012000 {
    compatible = "st,stm32f4-i2c", "st,stm32f7-i2c";
    reg = <0x40012000 0x400>;
    interrupts = <31 2>;
    clocks = <&rcc 0 0 43>;
    status = "okay";

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
    };
};
语法元素 说明 3.x 对应物
compatible 驱动匹配的关键字符串,格式为“厂商,型号” .name 字段 + platform_match
reg 地址与长度对,描述寄存器基址和范围 struct resource 中的 .start / .end
interrupts 中断号与触发类型,依赖 interrupt-parent platform_get_irq() 硬编码
clocks 时钟句柄,引用 clocks 控制器节点 clk_get() 通过设备名查找
status 设备启用状态,"okay" 或 "disabled" 无对应,直接注册

3.3 从 board-*.c 到 .dts 的迁移路径

迁移工作并非简单的“翻译”,而是对硬件描述逻辑的重新梳理。以下是一个典型的 I2C 外设迁移对照:

迁移原则: 将 C 代码中的 platform_device 注册、resource 数组、i2c_board_info 结构体,全部替换为 .dts 中的节点和属性。驱动侧则通过 of_match_table 匹配,并使用 of_* API 读取资源。
// 3.x 时代:board-xxx.c
static struct resource i2c_resources[] = {
    [0] = DEFINE_RES_MEM(0x40012000, 0x400),
    [1] = DEFINE_RES_IRQ(31),
};

static struct platform_device i2c_device = {
    .name = "stm32-i2c",
    .id = 1,
    .resource = i2c_resources,
    .num_resources = ARRAY_SIZE(i2c_resources),
};

static struct i2c_board_info __initdata eeprom_info = {
    I2C_BOARD_INFO("24c02", 0x50),
    .platform_data = &eeprom_pdata,
};

// 5.x 时代:xxx.dts
i2c1: i2c@40012000 {
    compatible = "st,stm32f4-i2c";
    reg = <0x40012000 0x400>;
    interrupts = <31 2>;
    status = "okay";

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

3.4 DT 绑定文档的重要性

设备树绑定文档(DT Binding Documentation)是连接硬件设计、内核驱动和设备树文件的“契约”。它定义了:

  • compatible 字符串:驱动必须声明的匹配值。
  • 必需属性(required):驱动正常工作必须提供的属性,如 reg、interrupts。
  • 可选属性(optional):用于配置特定功能,如 dmas、pinctrl-0。
  • 子节点规范:如 I2C 子节点必须包含 reg 属性表示地址。
常见陷阱: 在 3.x 到 5.x 的移植过程中,许多开发者直接复制旧板卡的 .dts 文件,却忽略了查阅对应驱动的最新绑定文档。这往往导致驱动无法 probe,因为 5.x 内核可能废弃了某些属性,或新增了必需属性(如 power-domains、resets)。

绑定文档位于内核源码的 Documentation/devicetree/bindings/ 目录下,按厂商和子系统分类。例如,STM32 I2C 控制器的绑定文档位于:

Documentation/devicetree/bindings/i2c/st,stm32-i2c.yaml

该 YAML 文件使用 json-schema 格式,不仅描述了属性,还能被 dt_binding_check 和 dtbs_check 工具自动验证,确保 .dts 文件的正确性。

3.5 设备树变迁时间线(3.x → 5.x)

设备树在 ARM 生态中的关键变迁节点 Linux 3.8 ARM DT 强制化 Linux 3.16 pinctrl 绑定标准化 Linux 4.4 YAML 绑定引入 Linux 4.18 dtbs_check 工具 迁移关键: 3.x 时代需手动编写 .dts 并确保 compatible 匹配;5.x 时代需遵循 YAML 绑定规范并通过 dtbs_check 验证。

3.6 驱动适配:从 platform_driver 到 of_match_table

驱动侧的变化同样显著。3.x 时代的驱动通常使用 platform_driver 的 .id_table 或直接硬编码设备名进行匹配。5.x 时代则必须使用 .of_match_table:

// 3.x 风格:依赖设备名
static struct platform_driver my_driver = {
    .driver = {
        .name = "stm32-i2c",
    },
    .probe = my_probe,
};

// 5.x 风格:依赖 compatible 匹配
static const struct of_device_id my_of_match[] = {
    { .compatible = "st,stm32f4-i2c" },
    { .compatible = "st,stm32f7-i2c" },
    { /* sentinel */ }
};

static struct platform_driver my_driver = {
    .driver = {
        .name = "stm32-i2c",
        .of_match_table = my_of_match,
    },
    .probe = my_probe,
};

在 probe 函数中,资源获取方式也从 platform_get_resource() 转变为 of_* API:

// 3.x 方式
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);

// 5.x 方式(推荐)
base = devm_platform_ioremap_resource(pdev, 0);
// 或更通用的 of_* 方式
struct device_node *np = pdev->dev.of_node;
of_property_read_u32(np, "clock-frequency", &freq);

3.7 实战:一个 I2C EEPROM 驱动的迁移清单

步骤 3.x 操作 5.x 操作
1. 硬件描述 在 board-xxx.c 中定义 i2c_board_info 在 .dts 中添加 EEPROM 子节点
2. 驱动匹配 i2c_driver.id_table 或 .driver.name i2c_driver.driver.of_match_table
3. 资源获取 client->irq 或 i2c_client.dev.platform_data of_property_read_u32() 或 device_property_read_*()
4. 时钟/电源 clk_get(NULL, "i2c_clk") devm_clk_get(&pdev->dev, NULL) 配合 clocks 属性
5. 验证 编译内核,烧录启动 make dtbs_check + 内核启动日志
推荐工具: 使用 dtc -I dtb -O dts 反编译设备树二进制(.dtb)为可读的 .dts,用于调试和对比。在 5.x 内核中,dtbs_check 和 dt_binding_check 是确保绑定正确性的必备工具。

3.8 本章小结

设备树的引入是嵌入式 Linux 内核架构的一次重大演进。从 3.x 到 5.x,开发者需要完成以下认知转变:

  • 描述方式:从 C 结构体到树形文本文件。
  • 匹配机制:从设备名到 compatible 字符串。
  • 资源获取:从 platform_get_resource 到 of_property_read_* 或 device_property_read_*。
  • 验证手段:从人工审查到自动化 schema 校验。