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属性名,如gpios、led-gpiosindex- 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。 - 注意 :循环内
break或return时,必须手动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); // 释放引用(必须)
}
六、常见错误与注意事项
-
必须检查所有OF API的返回值
设备树属性可能不存在或格式错误,不检查返回值会导致空指针访问和内核崩溃。
-
不要直接访问device_node结构体的内部字段
内核版本升级时,结构体内部字段可能会变化,必须使用官方提供的API访问。
-
使用devm系列API管理资源
优先使用
devm_ioremap_resource、devm_request_irq、devm_gpio_request等带devm_前缀的API,它们会在设备解绑时自动释放资源,避免内存泄漏。 -
of_node_put的使用
通过
of_find_node_by_path、of_get_next_child等函数获取的节点指针,使用完后必须调用of_node_put释放引用计数,否则会导致内存泄漏。 -
属性名的命名规范
设备树属性名使用小写字母和连字符
-,不要使用下划线_,这是内核的通用规范。
七、综合实战示例:完整的设备树解析驱动
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 |
⭐⭐⭐ |