4、内核设备树解析流程:OF API、platform_device与device_node的绑定

设备树(Device Tree)写好了,uboot也传过去了,内核到底怎么把它用起来?

说白了,内核启动时拿到的是一个扁平的二进制DTB。它得先解析成一棵内存里的树,再把这棵树上的每个节点,跟驱动模型里的platform_device挂上钩。这个过程,就是咱们今天要聊的核心。

我个人习惯把设备树解析分成三个阶段:DTB解包 → 树形结构建立 → 设备绑定。下面咱们一个一个拆开看。

4.1 从DTB到device_node:内核怎么“吃”设备树

uboot把DTB地址传给内核(通常是r2寄存器),内核在setup_arch()里拿到这个地址,然后调用unflatten_device_tree()。

这个函数干的事,就是把扁平的DTB“展开”成一棵由struct device_node组成的树。每个节点、每个属性,都被解析成内核可以操作的数据结构。

关键数据结构:

  • struct device_node:代表一个设备节点,包含name、type、phandle、full_name等字段。
  • struct property:代表节点下的一个属性,比如reg、interrupts、compatible。

我记得第一次调试时,打印过of_find_node_by_name()的返回值,发现节点名跟我在dts里写的不一样——原来内核会把节点名里的@和地址部分去掉,只保留前面的名字。嗯,这里要注意。

// 设备树展开的核心调用路径
start_kernel()
  → setup_arch()
    → unflatten_device_tree()
      → unflatten_dt_nodes()    // 递归解析每个节点
        → populate_node()       // 分配并填充device_node
        → set_node_props()      // 解析属性列表

4.2 OF API:操作设备树的瑞士军刀

树建好了,驱动怎么读里面的信息?内核提供了一套OF API(Open Firmware API),说白了就是一组函数,让你在驱动代码里查设备树。

函数 作用 我在项目中常用的场景
of_find_node_by_name() 按名字找节点 定位顶层节点
of_find_node_by_path() 按路径找节点 比如/soc/i2c@1234
of_get_property() 读取属性值 读reg、clock-frequency等
of_property_read_u32() 读32位整数 比of_get_property更安全
of_irq_parse_one() 解析中断 处理interrupts属性
of_iomap() 映射寄存器地址 直接拿到虚拟地址

我的习惯:能用of_property_read_*系列函数就别用of_get_property。前者做了类型检查和字节序转换,后者返回的是裸指针,容易踩坑。

举个例子,你想在驱动里读一个reg属性:

struct device_node *np;
u32 reg_base, reg_size;

np = of_find_node_by_path("/soc/uart@10000000");
if (!np) {
    pr_err("找不到节点\n");
    return -ENODEV;
}

// 读reg属性,假设是 <0x10000000 0x1000>
of_property_read_u32_index(np, "reg", 0, &reg_base);
of_property_read_u32_index(np, "reg", 1, &reg_size);

// 映射寄存器
void __iomem *base = of_iomap(np, 0);
if (!base) {
    pr_err("映射失败\n");
    return -ENOMEM;
}

4.3 platform_device与device_node的绑定:谁跟谁配对?

设备树解析完了,OF API也能查了,但驱动怎么知道该跟哪个节点配对?

这里有个关键机制:内核在启动时会遍历所有device_node,为每个节点创建一个platform_device。这个过程叫of_platform_populate()。

我曾经踩过的坑:如果一个节点在dts里写了status = "disabled",内核就不会为它创建platform_device。驱动probe根本进不去。排查了半天,最后发现是status没改回来。

绑定的核心逻辑,其实就是compatible属性匹配。驱动里有个of_match_table,里面列了一串compatible字符串。内核拿这个表跟节点的compatible属性比对,匹配上了就调用驱动的probe函数。

// 驱动中的匹配表
static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "vendor,my-device-v1" },
    { .compatible = "vendor,my-device-v2" },
    { /* 结束 */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);

// platform_driver注册
static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_driver_of_match,
    },
};
module_platform_driver(my_driver);

你想想看,驱动probe进来之后,怎么拿到设备树里的信息?

答案是通过struct platform_device里的dev.of_node指针。这个指针直接指向对应的device_node,你在probe里直接用OF API读属性就行了。

4.4 整体流程:一张图说清楚

下面这张图,把从DTB到驱动probe的完整路径画出来了。我建议你把它存下来,调试设备树问题时对照着看。

设备树解析与绑定完整流程 1. DTB解包 unflatten_device_tree() 2. 建立device_node树 内存中的树形结构 3. 创建platform_device of_platform_populate() device_node 树结构 /soc ├── uart@10000000 (compatible = "vendor,uart") ├── i2c@20000000 (compatible = "vendor,i2c") └── gpio@30000000 (compatible = "vendor,gpio") 4. compatible匹配 → 驱动probe of_match_table 比对 probe() 中通过OF API读属性

4.5 几个容易翻车的细节

  • 节点名 vs 全路径:of_find_node_by_name()只匹配节点名(不含地址部分),of_find_node_by_path()匹配完整路径。我建议优先用路径,更精确。
  • compatible的优先级:内核匹配时,先精确匹配,再尝试通配。如果你写了"vendor,device"和"vendor,device-v2",内核会按你在match table里写的顺序匹配。
  • reg属性的大小端:设备树里reg是大端(big-endian),而ARM/x86通常是little-endian。OF API会自动做转换,但如果你用of_get_property拿裸指针,就得自己调用of_read_number()。

调试小技巧:在/sys/firmware/devicetree/base/下,你可以看到内核解析后的设备树结构。每个文件对应一个属性,cat一下就能看到值。我调试时经常先看这里,确认内核解析对了,再查驱动代码。

嗯,设备树解析这块,说白了就是“uboot传DTB → 内核展开成树 → 创建platform_device → 驱动匹配probe”这么一条线。理解了这个流程,你写驱动时就知道该在哪个环节拿什么数据了。