【Linux驱动开发】第13天:Linux内核设备树解析 OF API 超详细全解

Linux内核设备树解析 OF API 超详细全解(驱动开发必备)

OF(Open Firmware)系列API是驱动与设备树之间的唯一桥梁,所有驱动从设备树中读取硬件信息的操作,都通过这些API完成。


一、OF API 概述

1. 什么是OF API?

OF API是Linux内核提供的一套设备树解析函数集,专门用于驱动代码中:

  • 访问设备树节点
  • 读取节点的属性值
  • 获取硬件资源(寄存器地址、中断号、GPIO、时钟等)
  • 遍历设备树结构

2. 核心头文件

所有OF API都定义在以下头文件中:

c 复制代码
#include <linux/of.h>              // 基础OF API
#include <linux/of_address.h>      // 地址相关API
#include <linux/of_irq.h>          // 中断相关API
#include <linux/of_gpio.h>         // GPIO相关API
#include <linux/of_clk.h>          // 时钟相关API

3. 核心数据结构

所有OF API的核心是struct device_node结构体,它代表设备树中的一个节点:

c 复制代码
struct device_node {
    const char *name;       // 节点名
    const char *full_name;  // 节点完整路径名
    struct property *properties;  // 节点的属性链表
    struct device_node *parent;   // 父节点
    struct device_node *child;    // 第一个子节点
    struct device_node *sibling;  // 下一个兄弟节点
    // ... 其他内核内部字段
};

在platform驱动中,我们通过pdev->dev.of_node获取当前设备对应的设备树节点指针:

c 复制代码
static int my_probe(struct platform_device *pdev)
{
    // 获取当前设备的设备树节点指针
    struct device_node *node = pdev->dev.of_node;
    
    if (!node) {
        dev_err(&pdev->dev, "设备树节点不存在\n");
        return -ENODEV;
    }
    
    // 后续所有OF API都使用这个node指针
    return 0;
}

二、节点操作API

1. 检查节点是否可用

c 复制代码
bool of_device_is_available(const struct device_node *node);
  • 作用 :检查节点的status属性是否为okay
  • 参数node - 设备树节点指针
  • 返回值true表示节点可用,false表示节点被禁用(status = "disabled"
  • 使用场景:probe函数开头必须检查,避免操作被禁用的设备

代码示例

c 复制代码
if (!of_device_is_available(node)) {
    dev_err(&pdev->dev, "设备被禁用\n");
    return -ENODEV;
}

2. 检查节点兼容性

c 复制代码
bool of_device_is_compatible(const struct device_node *node, const char *compat);
  • 作用 :检查节点是否包含指定的compatible属性值
  • 参数
    • node - 设备树节点指针
    • compat - 要检查的兼容字符串
  • 返回值true表示包含该兼容字符串,false表示不包含
  • 使用场景:一个驱动支持多个兼容设备时,区分不同的硬件版本

代码示例

c 复制代码
if (of_device_is_compatible(node, "vendor,dev-v1")) {
    dev_info(&pdev->dev, "检测到硬件版本V1\n");
    // 执行V1版本的初始化
} else if (of_device_is_compatible(node, "vendor,dev-v2")) {
    dev_info(&pdev->dev, "检测到硬件版本V2\n");
    // 执行V2版本的初始化
}

3. 通过路径查找节点

c 复制代码
struct device_node *of_find_node_by_path(const char *path);
  • 作用:通过完整路径查找设备树节点
  • 参数path - 节点的完整路径,如"/soc/uart@12340000"
  • 返回值:成功返回节点指针,失败返回NULL
  • 注意 :使用完后需要调用of_node_put(node)释放节点引用计数

代码示例

c 复制代码
struct device_node *uart_node = of_find_node_by_path("/soc/uart@12340000");
if (uart_node) {
    dev_info(&pdev->dev, "找到UART节点\n");
    of_node_put(uart_node); // 释放引用
}

三、基础属性读取API(最常用)

1. 读取32位无符号整数

c 复制代码
int of_property_read_u32(const struct device_node *node,
                         const char *propname,
                         u32 *out_value);
  • 作用:读取节点中指定属性的32位无符号整数值
  • 参数
    • node - 设备树节点指针
    • propname - 属性名
    • out_value - 输出参数,保存读取到的整数值
  • 返回值
    • 0:成功
    • -EINVAL:属性不存在或格式错误
    • -ENODATA:属性值长度不足
  • 使用场景:读取寄存器地址、中断号、时钟频率、GPIO号等

代码示例

c 复制代码
u32 clock_freq;
int ret = of_property_read_u32(node, "clock-frequency", &clock_freq);
if (ret) {
    dev_err(&pdev->dev, "读取clock-frequency失败,ret=%d\n", ret);
    return ret;
}
dev_info(&pdev->dev, "时钟频率:%d Hz\n", clock_freq);

2. 读取32位整数数组

c 复制代码
int of_property_read_u32_array(const struct device_node *node,
                               const char *propname,
                               u32 *out_values,
                               size_t sz);
  • 作用:读取节点中指定属性的32位无符号整数数组
  • 参数
    • node - 设备树节点指针
    • propname - 属性名
    • out_values - 输出数组,保存读取到的整数
    • sz - 要读取的数组元素个数
  • 返回值:成功返回0,失败返回负数错误码

代码示例

dts 复制代码
// 设备树中定义
my-node {
    my-array = <1 2 3 4 5>;
};
c 复制代码
u32 arr[5];
int ret = of_property_read_u32_array(node, "my-array", arr, 5);
if (ret) {
    dev_err(&pdev->dev, "读取数组失败\n");
    return ret;
}
dev_info(&pdev->dev, "数组元素:%d %d %d %d %d\n", arr[0], arr[1], arr[2], arr[3], arr[4]);

3. 读取字符串

c 复制代码
int of_property_read_string(const struct device_node *node,
                            const char *propname,
                            const char **out_string);
  • 作用:读取节点中指定属性的字符串值
  • 参数
    • node - 设备树节点指针
    • propname - 属性名
    • out_string - 输出参数,保存字符串指针
  • 返回值:成功返回0,失败返回负数错误码
  • 注意:返回的字符串指针指向设备树内存,不需要释放

代码示例

c 复制代码
const char *model;
int ret = of_property_read_string(node, "model", &model);
if (ret) {
    dev_err(&pdev->dev, "读取model失败\n");
    return ret;
}
dev_info(&pdev->dev, "设备型号:%s\n", model);

4. 读取布尔属性

c 复制代码
bool of_property_read_bool(const struct device_node *node,
                           const char *propname);
  • 作用:检查节点是否包含指定的布尔属性
  • 参数
    • node - 设备树节点指针
    • propname - 属性名
  • 返回值true表示属性存在,false表示属性不存在
  • 说明:布尔属性没有值,只有存在和不存在两种状态

代码示例

dts 复制代码
// 设备树中定义
my-node {
    enable-power;
};
c 复制代码
if (of_property_read_bool(node, "enable-power")) {
    dev_info(&pdev->dev, "电源已启用\n");
    // 执行电源初始化
}

5. 读取任意长度的属性值

c 复制代码
int of_property_read_variable_u8_array(const struct device_node *node,
                                       const char *propname,
                                       u8 *out_values,
                                       size_t min_sz,
                                       size_t max_sz);
  • 作用:读取任意长度的字节数组属性
  • 参数
    • node - 设备树节点指针
    • propname - 属性名
    • out_values - 输出数组
    • min_sz - 最小读取长度
    • max_sz - 最大读取长度
  • 返回值:成功返回实际读取的字节数,失败返回负数错误码
  • 使用场景:读取MAC地址、固件版本等二进制数据

代码示例

dts 复制代码
// 设备树中定义
my-node {
    mac-address = [00 11 22 33 44 55];
};
c 复制代码
u8 mac[6];
int ret = of_property_read_variable_u8_array(node, "mac-address", mac, 6, 6);
if (ret != 6) {
    dev_err(&pdev->dev, "读取MAC地址失败\n");
    return -EINVAL;
}
dev_info(&pdev->dev, "MAC地址:%02x:%02x:%02x:%02x:%02x:%02x\n",
         mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

四、硬件资源获取API(驱动核心)

1. 获取reg资源(寄存器地址)

c 复制代码
struct resource *platform_get_resource(struct platform_device *pdev,
                                       unsigned int type,
                                       unsigned int num);
  • 作用:从platform_device中获取指定类型的资源
  • 参数
    • pdev - platform设备指针
    • type - 资源类型,常用值:
      • IORESOURCE_MEM:内存资源(寄存器基地址)
      • IORESOURCE_IRQ:中断资源
    • num - 资源索引,从0开始
  • 返回值:成功返回resource结构体指针,失败返回NULL

代码示例

c 复制代码
// 获取第0个内存资源
struct resource *mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem_res) {
    dev_err(&pdev->dev, "获取内存资源失败\n");
    return -ENODEV;
}

// 映射寄存器地址
void __iomem *base = devm_ioremap_resource(&pdev->dev, mem_res);
if (IS_ERR(base)) {
    dev_err(&pdev->dev, "映射寄存器失败\n");
    return PTR_ERR(base);
}

dev_info(&pdev->dev, "寄存器基地址:0x%llx,长度:0x%llx\n",
         (unsigned long long)mem_res->start,
         (unsigned long long)resource_size(mem_res));

2. 获取中断号

c 复制代码
int platform_get_irq(struct platform_device *pdev, unsigned int num);
  • 作用:从platform_device中获取中断号(推荐使用,比直接读reg更安全)
  • 参数
    • pdev - platform设备指针
    • num - 中断资源索引,从0开始
  • 返回值:成功返回中断号,失败返回负数错误码

代码示例

c 复制代码
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
    dev_err(&pdev->dev, "获取中断号失败,irq=%d\n", irq);
    return irq;
}
dev_info(&pdev->dev, "中断号:%d\n", irq);

// 申请中断
int ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
                          IRQ_TYPE_LEVEL_HIGH, pdev->name, pdev);
if (ret) {
    dev_err(&pdev->dev, "申请中断失败,ret=%d\n", ret);
    return ret;
}

3. 获取GPIO号

c 复制代码
int of_get_named_gpio(const struct device_node *node,
                      const char *propname,
                      int index);
  • 作用:从设备树中获取指定名称的GPIO号
  • 参数
    • node - 设备树节点指针
    • propname - GPIO属性名,如gpiosled-gpios
    • index - GPIO索引,从0开始
  • 返回值:成功返回GPIO号,失败返回负数错误码

代码示例

dts 复制代码
// 设备树中定义
led {
    compatible = "gpio-leds";
    led-gpios = <&gpioa 13 GPIO_ACTIVE_HIGH>;
};
c 复制代码
int gpio = of_get_named_gpio(node, "led-gpios", 0);
if (gpio < 0) {
    dev_err(&pdev->dev, "获取GPIO失败,gpio=%d\n", gpio);
    return gpio;
}
dev_info(&pdev->dev, "LED GPIO号:%d\n", gpio);

// 申请GPIO
int ret = devm_gpio_request(&pdev->dev, gpio, "led-gpio");
if (ret) {
    dev_err(&pdev->dev, "申请GPIO失败,ret=%d\n", ret);
    return ret;
}

// 设置GPIO为输出高电平
gpio_direction_output(gpio, 1);

4. 获取时钟

c 复制代码
struct clk *of_clk_get(struct device_node *node, int index);
struct clk *of_clk_get_by_name(struct device_node *node, const char *name);
  • 作用:从设备树中获取时钟
  • 参数
    • node - 设备树节点指针
    • index - 时钟索引,从0开始
    • name - 时钟名称
  • 返回值:成功返回clk结构体指针,失败返回ERR_PTR

代码示例

dts 复制代码
// 设备树中定义
uart@12340000 {
    compatible = "vendor,uart";
    reg = <0x12340000 0x1000>;
    clocks = <&rcc UART_CLK>;
    clock-names = "uart_clk";
};
c 复制代码
struct clk *clk = of_clk_get_by_name(node, "uart_clk");
if (IS_ERR(clk)) {
    dev_err(&pdev->dev, "获取时钟失败\n");
    return PTR_ERR(clk);
}

// 使能时钟
int ret = clk_prepare_enable(clk);
if (ret) {
    dev_err(&pdev->dev, "使能时钟失败,ret=%d\n", ret);
    clk_put(clk);
    return ret;
}

dev_info(&pdev->dev, "时钟频率:%lu Hz\n", clk_get_rate(clk));

五、高级API:遍历子节点

1、核心API:of_get_next_child

函数原型 (定义于 include/linux/of.h):

c 复制代码
struct device_node *of_get_next_child(
    const struct device_node *node,   // 父节点
    struct device_node *prev           // 上一个子节点,首次传NULL
);
关键特性
  • 作用 :迭代遍历父节点的直接子节点,返回下一个子节点指针。
  • 返回值 :成功返回子节点指针(引用计数+1 );遍历完返回NULL
  • 必须释放 :每个子节点用完后必须调用of_node_put(child)引用计数-1),否则内存泄漏。

2、简化宏:for_each_child_of_node

内核提供封装宏,自动处理迭代与释放,是驱动开发首选:

c 复制代码
#define for_each_child_of_node(parent, child) \
    for (child = of_get_next_child(parent, NULL); child; \
         child = of_get_next_child(parent, child))
  • 优势 :无需手动管理prev与循环,自动调用of_node_put
  • 注意 :循环内breakreturn时,必须手动of_node_put(child)

3、完整代码示例

3.1. 设备树(DTS)节点
dts 复制代码
i2c@12341000 {
    compatible = "vendor,i2c";
    reg = <0x12341000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;

    eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; };
    sensor@51 { compatible = "vendor,temp-sensor"; reg = <0x51>; };
};
3.2. 内核驱动遍历代码
c 复制代码
#include <linux/of.h>          // 设备树OF函数
#include <linux/platform_device.h>

static int i2c_probe(struct platform_device *pdev)
{
    struct device_node *node = pdev->dev.of_node; // 父节点
    struct device_node *child;
    int i = 0;
    u32 addr;
    const char *compat;

    dev_info(&pdev->dev, "=== 遍历I2C子节点 ===\n");

    // 标准宏遍历(推荐)
    for_each_child_of_node(node, child) {
        // 读取reg属性
        of_property_read_u32(child, "reg", &addr);
        // 读取compatible属性
        of_property_read_string(child, "compatible", &compat);

        dev_info(&pdev->dev,
            "子节点%d: 名称=%s, 地址=0x%x, 兼容串=%s\n",
            i++, child->name, addr, compat);
    }

    return 0;
}

static struct platform_driver i2c_drv = {
    .probe = i2c_probe,
    .driver = {
        .name = "vendor-i2c",
        .of_match_table = of_match_ptr(
            (struct of_device_id[]){
                { .compatible = "vendor,i2c" },
                { /* 终止 */ }
            }),
    },
};
module_platform_driver(i2c_drv);

4、手动调用of_get_next_child(底层用法)

c 复制代码
struct device_node *child = NULL;
int i = 0;

dev_info(&pdev->dev, "=== 手动遍历 ===\n");
while ((child = of_get_next_child(node, child)) != NULL) {
    u32 addr;
    of_property_read_u32(child, "reg", &addr);
    dev_info(&pdev->dev, "子节点%d: %s@0x%x\n", i++, child->name, addr);
    
    of_node_put(child); // 释放引用(必须)
}

六、常见错误与注意事项

  1. 必须检查所有OF API的返回值

    设备树属性可能不存在或格式错误,不检查返回值会导致空指针访问和内核崩溃。

  2. 不要直接访问device_node结构体的内部字段

    内核版本升级时,结构体内部字段可能会变化,必须使用官方提供的API访问。

  3. 使用devm系列API管理资源

    优先使用devm_ioremap_resourcedevm_request_irqdevm_gpio_request等带devm_前缀的API,它们会在设备解绑时自动释放资源,避免内存泄漏。

  4. of_node_put的使用

    通过of_find_node_by_pathof_get_next_child等函数获取的节点指针,使用完后必须调用of_node_put释放引用计数,否则会导致内存泄漏。

  5. 属性名的命名规范

    设备树属性名使用小写字母和连字符-,不要使用下划线_,这是内核的通用规范。


七、综合实战示例:完整的设备树解析驱动

c 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <linux/io.h>
#include <linux/interrupt.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("设备树解析综合示例");

static const struct of_device_id my_dt_ids[] = {
    { .compatible = "mytest,dev" },
    { }
};
MODULE_DEVICE_TABLE(of, my_dt_ids);

// 设备私有数据结构体
struct my_dev {
    void __iomem *base;
    int irq;
    int led_gpio;
    u32 clock_freq;
    const char *model;
};

// 中断处理函数
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    dev_info((struct device *)dev_id, "中断触发\n");
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct device_node *node = pdev->dev.of_node;
    struct my_dev *dev;
    struct resource *mem_res;
    int ret;

    // 1. 检查节点是否可用
    if (!of_device_is_available(node)) {
        dev_err(&pdev->dev, "设备被禁用\n");
        return -ENODEV;
    }

    // 2. 分配设备私有数据
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        return -ENOMEM;
    }
    dev_set_drvdata(&pdev->dev, dev);

    // 3. 读取基础属性
    ret = of_property_read_string(node, "model", &dev->model);
    if (ret) {
        dev_err(&pdev->dev, "读取model失败\n");
        return ret;
    }

    ret = of_property_read_u32(node, "clock-frequency", &dev->clock_freq);
    if (ret) {
        dev_err(&pdev->dev, "读取clock-frequency失败\n");
        return ret;
    }

    // 4. 获取寄存器资源并映射
    mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!mem_res) {
        dev_err(&pdev->dev, "获取内存资源失败\n");
        return -ENODEV;
    }

    dev->base = devm_ioremap_resource(&pdev->dev, mem_res);
    if (IS_ERR(dev->base)) {
        dev_err(&pdev->dev, "映射寄存器失败\n");
        return PTR_ERR(dev->base);
    }

    // 5. 获取中断号并申请中断
    dev->irq = platform_get_irq(pdev, 0);
    if (dev->irq < 0) {
        dev_err(&pdev->dev, "获取中断号失败\n");
        return dev->irq;
    }

    ret = devm_request_irq(&pdev->dev, dev->irq, my_irq_handler,
                          IRQ_TYPE_LEVEL_HIGH, pdev->name, dev);
    if (ret) {
        dev_err(&pdev->dev, "申请中断失败\n");
        return ret;
    }

    // 6. 获取GPIO并初始化
    dev->led_gpio = of_get_named_gpio(node, "led-gpios", 0);
    if (dev->led_gpio < 0) {
        dev_err(&pdev->dev, "获取GPIO失败\n");
        return dev->led_gpio;
    }

    ret = devm_gpio_request(&pdev->dev, dev->led_gpio, "led-gpio");
    if (ret) {
        dev_err(&pdev->dev, "申请GPIO失败\n");
        return ret;
    }

    gpio_direction_output(dev->led_gpio, 1); // 点亮LED

    // 7. 打印所有读取到的信息
    dev_info(&pdev->dev, "=== 设备树解析完成 ===\n");
    dev_info(&pdev->dev, "设备型号:%s\n", dev->model);
    dev_info(&pdev->dev, "时钟频率:%d Hz\n", dev->clock_freq);
    dev_info(&pdev->dev, "寄存器基地址:0x%llx\n", (unsigned long long)mem_res->start);
    dev_info(&pdev->dev, "中断号:%d\n", dev->irq);
    dev_info(&pdev->dev, "LED GPIO号:%d\n", dev->led_gpio);

    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_dev *dev = dev_get_drvdata(&pdev->dev);
    
    gpio_set_value(dev->led_gpio, 0); // 熄灭LED
    dev_info(&pdev->dev, "驱动卸载完成\n");
    return 0;
}

static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "mytest-driver",
        .of_match_table = my_dt_ids,
    },
};

module_platform_driver(my_driver);

对应的设备树节点:

dts 复制代码
mytest_dev@12340000 {
    compatible = "mytest,dev";
    status = "okay";
    model = "My Test Device";
    clock-frequency = <100000000>;
    reg = <0x12340000 0x1000>;
    interrupts = <5 IRQ_TYPE_LEVEL_HIGH>;
    led-gpios = <&gpioa 13 GPIO_ACTIVE_HIGH>;
};

八、总结:驱动开发必背OF API清单

功能 API 使用频率
检查节点是否可用 of_device_is_available ⭐⭐⭐⭐⭐
读取32位整数 of_property_read_u32 ⭐⭐⭐⭐⭐
读取字符串 of_property_read_string ⭐⭐⭐⭐⭐
读取布尔属性 of_property_read_bool ⭐⭐⭐⭐
获取寄存器资源 platform_get_resource ⭐⭐⭐⭐⭐
获取中断号 platform_get_irq ⭐⭐⭐⭐⭐
获取GPIO号 of_get_named_gpio ⭐⭐⭐⭐⭐
映射寄存器地址 devm_ioremap_resource ⭐⭐⭐⭐⭐
遍历子节点 for_each_child_of_node ⭐⭐⭐
相关推荐
没文化的阿浩9 小时前
【Linux系统】线程的同步与互斥(1)——互斥量mutex
linux·运维·jvm
t5y229 小时前
【Linux】组管理和权限管理
linux·服务器
j7~9 小时前
【Linux】 基础IO(动静态库的制作与使用)--万字详解
linux·运维·服务器·动态库·静态库
j_xxx404_9 小时前
Linux线程:核心机制与优雅的 C++ 封装实践|附源码
linux·运维·服务器·开发语言·c++·人工智能·ai
IMPYLH9 小时前
Linux 的 users 命令
linux·运维·服务器·前端·数据库·bash
xiaoye-duck9 小时前
【Linux:文件】Linux 动静态库详解:动态链接与动态库加载深度解析
linux
加油20199 小时前
嵌入式软件技术栈和学习路线详解
linux·arm开发·数据结构·mqtt·设计模式·嵌入式
Oj92q85H59 小时前
如何在Dev-C++中使用TDM-GCC编译项目
linux·开发语言·c++
行走的大喇叭9 小时前
计算机系统组成及常见概念
linux·运维·计算机网络