4、设备树在Linux内核中的表示:struct device_node、struct property、OF API简介

设备树源文件(.dts)经过编译生成二进制.dtb后,由引导加载程序传递给内核。内核在启动早期对.dtb进行解析,将其中的节点、属性和值展开为一套标准的内核数据结构。这套数据结构是后续所有设备驱动进行资源查询、匹配和获取的基础。本章深入剖析设备树在内核中的核心表示:struct device_node、struct property,以及操作它们的OF(Open Firmware)API。

4.1 核心数据结构:struct device_node

每一个设备树节点({ }包围的实体)在内核中都被抽象为一个struct device_node实例。该结构体定义在include/linux/of.h中,是驱动开发者最常接触的设备树对象。

成员 类型 说明
name const char * 节点名称,取自node-name部分,不含@unit-address
type const char * 设备类型,通常对应device_type属性值(PowerPC常用,ARM较少)
phandle phandle 节点的phandle值,用于节点间引用
full_name const char * 从根到当前节点的完整路径名,如/soc/i2c@7e205000
properties struct property * 指向该节点属性链表的头指针
parent struct device_node * 指向父节点
child struct device_node * 指向第一个子节点
sibling struct device_node * 指向下一个兄弟节点
_flags unsigned long 节点状态标志(如OF_DYNAMIC、OF_POPULATED)
data void * 平台私有数据指针,驱动可附加自定义数据

关键理解: device_node通过parent、child、sibling三个指针将整个设备树组织成一棵多叉树。驱动遍历设备树时,实际上就是在遍历这个指针网络。

4.2 属性表示:struct property

每个节点下的属性(key = value;)由struct property描述。属性是驱动获取硬件参数(寄存器地址、中断号、GPIO等)的直接来源。

成员 类型 说明
name char * 属性名称,如compatible、reg、interrupts
length int 属性值的字节长度
value void * 指向属性值的原始二进制数据
next struct property * 指向下一个属性,形成单链表

注意: value指向的是原始字节流。对于reg、interrupts等数值型属性,数据采用大端(Big-Endian)字节序存储。驱动使用OF API读取时,API会自动完成字节序转换。

4.3 设备树在内核中的整体结构

下图展示了device_node与property如何共同构成内核中的设备树视图:

内核设备树数据结构示意图 device_node (根节点) property: model property: compatible property: #address-cells device_node: soc compatible ranges device_node: uart@fe201000 compatible reg interrupts device_node: gpio@fe200000 compatible reg device_node property 父子关系 兄弟关系

4.4 OF API 分类与核心函数

OF API是驱动与设备树数据结构交互的唯一接口。根据功能,可将其分为五大类:

分类 核心函数 功能描述
节点查找 of_find_node_by_name()
of_find_node_by_type()
of_find_compatible_node()
of_find_node_by_phandle()
根据名称、类型、compatible或phandle遍历查找节点
属性读取 of_property_read_u32()
of_property_read_u64()
of_property_read_string()
of_property_read_u32_array()
从节点属性中读取整型、字符串、数组等数据
资源获取 of_iomap()
of_address_to_resource()
irq_of_parse_and_map()
将reg、interrupts属性转换为内核资源
匹配检查 of_match_node()
of_device_is_compatible()
检查节点是否与of_device_id匹配表匹配
遍历迭代 for_each_child_of_node()
for_each_property_of_node()
宏封装,方便遍历子节点或属性链表

4.5 典型使用模式:从节点读取资源

以下代码片段展示了一个platform驱动probe函数中,如何使用OF API从设备节点获取内存地址和中断号:

static int my_driver_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct resource *res;
    void __iomem *base;
    int irq, ret;
    u32 val;

    /* 1. 读取reg属性,获取IO内存资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(dev, "failed to get MEM resource\n");
        return -ENXIO;
    }
    base = devm_ioremap_resource(dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 2. 读取interrupts属性,获取中断号 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 3. 直接读取自定义属性 */
    ret = of_property_read_u32(np, "my-device,clock-frequency", &val);
    if (ret)
        dev_warn(dev, "clock-frequency not found, use default\n");

    /* 4. 检查compatible是否包含特定字符串 */
    if (of_device_is_compatible(np, "my-device,rev2"))
        dev_info(dev, "Running on rev2 hardware\n");

    return 0;
}

常见陷阱:

  • 不要直接访问device_node->properties链表:应始终使用OF API,避免破坏内核内部结构。
  • 注意引用计数:of_find_node_by_*()系列函数会增加节点引用计数,使用完毕后需调用of_node_put()释放。
  • platform_get_resource() vs of_iomap():在platform驱动中,优先使用platform_get_resource(),它内部已封装OF解析逻辑;直接使用of_iomap()会绕过platform资源管理。

4.6 设备树节点与platform_device的关联

在内核启动后期,of_platform_populate()函数遍历设备树中所有compatible属性不为空的节点,为每个节点创建一个struct platform_device。该platform_device的dev.of_node指针指向对应的device_node。这就是驱动probe函数中pdev->dev.of_node的来源。

核心关系链:
.dts节点 → device_node → platform_device.dev.of_node → OF API → 驱动获取资源