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, ®_base);
of_property_read_u32_index(np, "reg", 1, ®_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的完整路径画出来了。我建议你把它存下来,调试设备树问题时对照着看。
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”这么一条线。理解了这个流程,你写驱动时就知道该在哪个环节拿什么数据了。