# 嵌入式Linux驱动开发 —— 从 DTS 到代码的桥梁与简单OF系列API
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!
前言:当设备树遇见驱动代码
前面我们聊了设备树的语法和编译原理,知道了 .dts 文件是如何被编译成 .dtb 然后被内核解析的。但说实话,这些只是"准备工作"。对于驱动开发者来说,真正的问题在于:我的驱动代码怎么去用这些设备树信息?
设备树里写着 reg = <0x020C406C 0x04>,但这只是个文本描述。驱动程序在运行时需要知道这个地址,需要把它映射成虚拟地址,然后才能去读写寄存器。中间缺了一个环节——需要有人在运行时去解析设备树,把那些 < > 里的数字提取出来,塞给 C 代码。
Linux 内核提供了这个环节,那就是一系列以 of_ 为前缀的 API 函数。你可以把它们理解为设备树和驱动代码之间的"翻译官"。
但这里有个历史遗留问题可能会困扰你:为什么叫 "OF" 而不是 "DT"?Device Tree 的缩写不是 DT 吗?这个问题的答案藏在设备树的历史里,我们稍后再说。现在先记住一点:当你看到 of_xxx() 这样的函数时,它们就是在操作设备树。
这一章我们会系统地介绍这些 API,看看它们是如何在实际驱动中使用的。我们还会拿 LED 驱动的代码做例子,看看那些在设备树里写的属性,是怎么一步步变成驱动里的寄存器地址的。
快速回顾:设备树的前世今生
在深入 API 之前,我们先快速过一遍设备树是怎么走到今天的。这段历史能帮你理解为什么内核里操作设备树的函数都叫 of_xxx(),以及设备树为什么被设计成现在这个样子。
从 PowerPC 到 ARM:一场被逼出来的变革
设备树最早不是 ARM 的发明。上世纪 90 年代,IBM 和苹果在 PowerPC 架构上制定了一个叫 Open Firmware 的固件标准,核心思想是让固件向操作系统提供一份完整的硬件描述,这样系统就不用为每块板子写专门的初始化代码了。设备树就是这个标准里定义的数据结构——用树状层次描述所有设备,每个节点包含寄存器地址、中断号、时钟频率等属性。
到了 2000 年代中后期,ARM 芯片爆发式增长,但 ARM Linux 处理硬件描述的方式极其原始:直接硬编码在 C 代码里。内核源码树里塞满了 arch/arm/mach-xxx 和 arch/arm/plat-xxx 目录,每个对应一种板子,重复率高达 90% 以上。维护成本高得离谱,代码膨胀到 arch/arm 的代码量比其他所有架构加起来还多。
2011 年,Linus Torvalds 终于爆发了:
"This whole ARM thing is a f*cking pain in the ass."
他明确拒绝继续合并这些垃圾代码。ARM 社区被迫改革,引入了 PowerPC 上已经成熟的设备树机制,经历了一个从可选到强制的演进过程。到了 2013 年左右,新的 ARM 板级代码几乎都使用了设备树;ARM64 更是从设计之初就强制要求设备树,不支持传统的板级 C 代码。
如今,设备树已成为 Linux 嵌入式领域描述硬件的通用机制,覆盖 ARM、ARM64、RISC-V、PowerPC、MIPS 等多个架构。
OF 命名的由来
因为设备树起源于 Open Firmware 标准,内核里操作设备树的函数就都叫 of_xxx()。后来 ARM 社区引入设备树时,为了复用已有的基础设施,也沿用了这个命名前缀。所以今天我们在 ARM Linux 里看到的设备树 API,依然叫 OF API,而不是 DT API。你可以把它理解为一种"历史遗产"——就像 C 语言的 printf 而不是 print。
那么 OF 和设备树是什么关系呢?设备树是数据结构,OF API 是操作这个数据结构的一套函数。就像 C 语言里有 struct 和操作 struct 的函数一样,设备树是"数据",OF API 是"操作数据的工具"。
在 Linux 内核的源码里,你会看到这样的头文件:
include/linux/of.h:核心 OF API 定义include/linux/of_address.h:地址映射相关函数include/linux/of_gpio.h:GPIO 相关函数include/linux/of_irq.h:中断相关函数
这些文件里定义的所有函数,都是我们这一章要讲的内容。
核心数据结构:device_node、property 和 resource
在讲具体的 API 之前,我们需要先了解一下内核是用什么数据结构来表示设备树的。毕竟 API 只是操作这些数据结构的工具,如果不了解数据结构本身,用起 API 来也是一头雾水。
struct device_node:节点的内核表示
struct device_node 是内核对设备树节点的描述。每个设备树节点在内核里都对应一个 device_node 结构体。这个结构体的定义在 include/linux/of.h 里,我们挑重点字段看:
struct device_node {
const char *name; /* 节点名字,比如 "gpio" */
const char *type; /* 设备类型,取自 device_type 属性 */
phandle phandle; /* 节点的 phandle 值 */
const char *full_name; /* 节点的全路径名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性链表头 */
struct property *deadprops; /* 已删除的属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点 */
struct device_node *sibling; /* 兄弟节点 */
struct kobject kobj;
unsigned long _flags;
void *data;
/* ... 更多平台特定字段 ... */
};
这个结构体设计得很巧妙。它不仅记录了节点的名字和类型,还通过 parent、child 和 sibling 三个指针把整棵树串了起来。这意味着你可以从任意一个节点出发,往上找父节点,往下找子节点,往旁边找兄弟节点——就像在真的树上爬一样。
properties 字段指向一个属性链表,所有的 property 结构体都挂在这个链表上。我们接下来看 property 结构体。
struct property:属性的内核表示
struct property {
char *name; /* 属性名字,比如 "reg" */
int length; /* 属性值的字节长度 */
void *value; /* 属性值,可以是任意数据 */
struct property *next; /* 指向下一个属性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
这里最关键的是 value 字段。它是个 void *,可以指向任意类型的数据。这是因为设备树里的属性值可以是各种类型:可能是单个整数,可能是字符串,可能是整数数组,甚至可能是任意字节序列。
内核怎么知道 value 里存的是什么类型呢?答案是:不知道。内核只知道这是一坨字节,具体怎么解释,要看属性的名字和上下文。比如 status 属性通常被解释为字符串,reg 属性被解释为整数数组,而 compatible 属性被解释为字符串数组。
所以当我们用 API 读取属性时,需要明确告诉内核我们想要什么类型的数据。这就是为什么有 of_property_read_u32()、of_property_read_string() 这样不同的函数。
struct resource:资源的统一描述
Linux 内核用 struct resource 来统一描述各种资源——不仅仅是内存映射 IO,还包括中断、DMA 通道等。这个结构体定义在 include/linux/ioport.h:
struct resource {
resource_size_t start; /* 资源起始地址/号 */
resource_size_t end; /* 资源结束地址/号 */
const char *name; /* 资源名称 */
unsigned long flags; /* 资源类型标志 */
struct resource *parent, *sibling, *child;
};
flags 字段说明这是什么类型的资源:
IORESOURCE_MEM:内存映射 IOIORESOURCE_IRQ:中断资源IORESOURCE_IO:端口 IO(x86 特有)IORESOURCE_DMA:DMA 通道
设备树里的 reg 属性可以通过 of_address_to_resource() 函数转换成 resource 结构体,这样驱动就可以用统一的方式来处理不同类型的资源了。
节点查找 API:如何在设备树中定位目标节点
有了数据结构基础,现在我们可以开始讲具体的 API 了。第一步是找到你要操作的节点。就像你想操作一个文件,得先找到它的路径一样,你想操作设备树里的某个节点,也得先定位到它。
内核提供了好几种查找节点的方法,适用于不同的场景。我们一个一个来看。
of_find_node_by_path:按路径查找
这是最直接的方法。如果你知道节点的完整路径,用这个函数最快:
struct device_node *of_find_node_by_path(const char *path);
参数 path 是节点的完整路径,比如 "/imx_aes_led"。返回值是找到的节点指针,如果没找到就返回 NULL。
这个函数在我们的 LED 驱动里用到了:
/* 从 /home/charliechen/imx-forge/driver/device_tree_try_03/alpha-board/led_hw.c */
static const char* kIMX_AES_LED = "/imx_aes_led";
led.device_tree_node = of_find_node_by_path(kIMX_AES_LED);
if (led.device_tree_node == NULL) {
pr_err("dtsled node can not found!\n");
return -EINVAL;
}
这里我们直接用路径 /imx_aes_led 去找节点。这个路径对应设备树里的定义:
/* 从 /home/charliechen/imx-forge/driver/device_tree/alpha-board/device_tree_try_03/imx6ull-aes-led.dts */
/ {
imx_aes_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = <...>;
};
};
of_find_node_by_path() 的好处是简单直接,缺点是你要知道确切的路径。如果你只是想找某个类型的设备(比如所有的 GPIO 控制器),这个方法就不太方便了。
of_find_node_by_name:按节点名查找
struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);
这个函数按节点名查找。注意节点名不是 compatible 属性,而是节点本身的名字。比如节点 gpio1 { ... } 的名字就是 "gpio1"。
from 参数指定从哪里开始找。如果传 NULL,就从根节点开始遍历整棵树。如果传一个具体的节点,就从那个节点之后继续找(这个设计允许你多次调用来遍历所有同名节点)。
这个函数在实际驱动里用得不多,因为节点名往往不够具体。同一个设备树上可能有很多叫 "gpio" 的节点,你很难确定找到的是哪一个。
of_find_compatible_node:按兼容性查找
这是驱动里最常用的查找函数:
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible);
参数说明:
from:起始节点,NULL表示从根开始type:device_type属性值,可以传NULL表示不检查compatible:要匹配的compatible属性字符串
这个函数会遍历设备树,找到第一个 compatible 属性包含指定字符串的节点。比如你可以用 "fsl,imx6ul-gpio" 来找 NXP 的 GPIO 控制器。
这里需要注意一点:compatible 属性可以包含多个字符串,用逗号分隔。of_find_compatible_node() 会检查所有这些字符串,只要有一个匹配就认为找到了。
of_find_matching_node_and_match:按匹配表查找
这是最强大的查找函数,它直接拿驱动里的 of_device_id 匹配表去过滤节点:
struct device_node *of_find_matching_node_and_match(
struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match);
matches 参数就是驱动里的 .of_match_table,比如:
static const struct of_device_id led_of_match[] = {
{ .compatible = "atkalpha-led", },
{ /* sentinel */ }
};
这个函数会遍历匹配表,找到第一个匹配的节点。match 是输出参数,告诉你具体匹配上了表里的哪一项。
在实际的 platform 驱动框架里,这个函数通常不需要你手动调用。驱动核心会自动帮你匹配。但如果你在写一些特殊逻辑(比如在驱动初始化时主动查找某个设备),这个函数就很有用了。
属性读取 API:如何从节点中提取信息
找到了节点,下一步就是读取它的属性。这是 OF API 的核心部分,也是驱动开发者用得最多的部分。
of_find_property:查找属性结构体
这是最底层的属性查找函数:
struct property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
参数说明:
np:设备节点name:属性名lenp:输出参数,返回属性值的字节长度
返回值是找到的 property 结构体指针,如果没找到就返回 NULL。
这个函数返回的是原始的 property 结构体,你可以直接访问它的 value 字段。但 value 是 void * 类型,你需要自己解释它的内容。
我们的 LED 驱动里用这个函数读取了 compatible 属性:
struct property* proper;
proper = of_find_property(led.device_tree_node, "compatible", NULL);
if (proper == NULL) {
pr_err("compatible property find failed\n");
} else {
pr_info("compatible = %s\n", (char*)proper->value);
}
这里我们知道 compatible 属性的值是个字符串,所以直接把 value 强转成 char * 来打印。但这种方法并不安全,因为 compatible 实际上是个字符串数组,可能包含多个以 null 结尾的字符串。更好的做法是用专门的字符串读取函数,我们稍后讲。
of_property_read_string:读取字符串属性
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string);
这个函数用于读取字符串类型的属性,比如 status、device_type 等。
参数说明:
np:设备节点propname:属性名out_string:输出参数,返回字符串指针
返回值是 0 表示成功,负值表示失败(-EINVAL 属性不存在,-ENODATA 属性值为空)。
我们的 LED 驱动用它来读取 status 属性:
const char* str;
int ret;
ret = of_property_read_string(led.device_tree_node, "status", &str);
if (ret < 0) {
pr_err("status read failed!\n");
} else {
pr_info("status = %s\n", str);
}
这里需要注意一个细节:如果属性里包含多个字符串(字符串数组),这个函数只会返回第一个。如果你想读取第 N 个字符串,可以用 of_property_read_string_index()。
of_property_read_u32:读取 32 位整数
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value);
这个函数用于读取单个 32 位整数属性。设备树里的 <0x12345678> 会被解析成一个 u32 值。
参数说明:
np:设备节点propname:属性名out_value:输出参数,返回读取的值
返回值是 0 表示成功,负值表示失败。
类似的还有读取 8 位、16 位、64 位整数的版本:
of_property_read_u8()of_property_read_u16()of_property_read_u64()
of_property_read_u32_array:读取整数数组
这个函数用于读取包含多个整数的属性,比如 reg 属性:
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz);
参数说明:
np:设备节点propname:属性名out_values:接收数据的数组指针sz:要读取的元素个数
返回值是 0 表示成功,负值表示失败。
我们的 LED 驱动用它来读取 reg 属性:
u32 regdata[10];
int ret;
ret = of_property_read_u32_array(led.device_tree_node, "reg", regdata, 10);
if (ret < 0) {
pr_err("reg property read failed!\n");
of_node_put(led.device_tree_node);
return -EINVAL;
}
pr_info("reg data:\n");
for (int i = 0; i < 10; i++) {
pr_cont("%#X ", regdata[i]);
}
pr_cont("\n");
这里我们预先知道 reg 属性有 10 个整数(5 组地址-长度对),所以直接读 10 个。在实际驱动里,你可能需要先用 of_property_count_elems_of_size() 来获取元素个数,动态分配内存。
of_property_count_elems_of_size:计算数组元素个数
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size);
这个函数返回指定属性里有多少个指定大小的元素。比如你想知道 reg 属性里有多少个 u32,可以这样做:
int count = of_property_count_elems_of_size(np, "reg", sizeof(u32));
返回值是元素个数,负值表示出错。
内存映射 API:如何将设备树地址转换为可访问的虚拟地址
我们前面讲了如何从设备树里读取地址值,但那些只是物理地址(或者总线地址)。驱动程序要访问这些地址,还需要把它们映射到内核虚拟地址空间。这一步通常用 ioremap() 来完成。
但 OF API 提供了更便捷的方法,把"读 reg 属性"和"ioremap"两步合成一步。
of_iomap:一步到位的地址映射
这是驱动里最常用的函数之一:
void __iomem *of_iomap(struct device_node *np,
int index);
参数说明:
np:设备节点index:reg属性的索引(从 0 开始)
返回值是映射后的内核虚拟地址,失败返回 NULL。
这个函数会自动完成以下步骤:
- 从
reg属性里读取第index组地址 - 处理地址转换(如果需要的话)
- 调用
ioremap()建立映射
我们的 LED 驱动用它来映射所有寄存器地址:
/* 5. 使用 of_iomap 进行寄存器地址映射 */
led.ccm_ccgr1 = of_iomap(led.device_tree_node, 0);
led.sw_mux_gpio = of_iomap(led.device_tree_node, 1);
led.sw_pad_gpio = of_iomap(led.device_tree_node, 2);
led.gpio_dr = of_iomap(led.device_tree_node, 3);
led.gpio_gdir = of_iomap(led.device_tree_node, 4);
if (!led.ccm_ccgr1 || !led.sw_mux_gpio || !led.sw_pad_gpio ||
!led.gpio_dr || !led.gpio_gdir) {
pr_err("ioremap failed!\n");
of_node_put(led.device_tree_node);
return -ENOMEM;
}
这里我们连续调用了 5 次 of_iomap(),每次传入不同的索引。这些索引对应 reg 属性里的 5 组地址:
reg = < 0X020C406C 0X04 /* 索引 0: CCM_CCGR1_BASE */
0X020E0068 0X04 /* 索引 1: SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* 索引 2: SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* 索引 3: GPIO1_DR_BASE */
0X0209C004 0X04 >; /* 索引 4: GPIO1_GDIR_BASE */
注意这里有个重要的错误处理:我们检查了所有映射是否成功,只要有一个失败就报错退出。这点很重要,因为部分成功会导致后续代码访问空指针,引发内核 panic。
of_get_address:获取地址原始数据
有时候你不想直接映射,而是想先拿到地址的原始数据,这时候可以用 of_get_address():
const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags);
参数说明:
dev:设备节点index:reg属性的索引size:输出参数,返回地址长度flags:输出参数,返回标志(比如IORESOURCE_MEM)
返回值是读取到的地址数据指针(大端格式的 u32 数组),失败返回 NULL。
这个函数返回的是设备树里的原始数据,可能还需要地址转换才能变成 CPU 物理地址。
of_translate_address:地址转换
设备树里的地址有时是总线地址,需要转换成 CPU 物理地址:
u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr);
参数说明:
dev:设备节点in_addr:从of_get_address()拿到的地址
返回值是转换后的物理地址,如果是 OF_BAD_ADDR 表示转换失败。
of_address_to_resource:转换成标准资源结构
Linux 内核用 struct resource 统一描述各种资源。这个函数把设备树里的 reg 直接转成 resource:
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r);
参数说明:
dev:设备节点index:reg属性的索引r:输出的resource结构体
返回值是 0 表示成功,负值表示失败。
这个函数在某些场景下很实用,比如你需要把地址信息传递给其他子系统时。但在简单的字符设备驱动里,直接用 of_iomap() 往往更方便。
资源管理 API:如何正确释放引用
到这里我们讲的都是"获取"资源的 API,但 Linux 内核编程有个黄金法则:有获取就必须有释放。OF API 也不例外。
of_node_put:释放节点引用
当你用 of_find_xxx() 系列函数获取了一个 device_node 指针后,你就有了对这个节点的引用。内核用引用计数来管理这些节点,当你用完后必须调用 of_node_put() 来释放引用:
void of_node_put(struct device_node *node);
参数 node 是你要释放的节点指针。
我们的 LED 驱动在出错处理和反初始化函数里都用到了它:
/* 出错处理 */
ret = of_property_read_u32_array(led.device_tree_node, "reg", regdata, 10);
if (ret < 0) {
pr_err("reg property read failed!\n");
of_node_put(led.device_tree_node); /* 释放节点引用 */
return -EINVAL;
}
/* 反初始化函数 */
void led_hw_deinit(void) {
/* ... 先 unmap 所有地址 ... */
if (led.device_tree_node) {
of_node_put(led.device_tree_node);
led.device_tree_node = NULL;
}
}
这里有个小技巧:我们在释放引用后把指针设为 NULL。这样即使 deinit() 函数被多次调用,也不会 double-free。
你可能会问:of_find_property() 需要配合 of_node_put() 吗?答案是:不需要。property 结构体是 device_node 的一部分,它的生命周期由节点管理。你只需要在用完整个节点后调用一次 of_node_put() 就行了。
实战示例:LED 驱动中的设备树使用
讲了这么多 API,现在我们把它们串起来,看看在实际驱动里是怎么用的。我们以 LED 硬件控制代码为例,完整走一遍流程。
第一步:查找节点
static const char* kIMX_AES_LED = "/imx_aes_led";
led.device_tree_node = of_find_node_by_path(kIMX_AES_LED);
if (led.device_tree_node == NULL) {
pr_err("dtsled node can not found!\n");
return -EINVAL;
}
pr_info("dtsled node has been found!\n");
这里我们用路径查找节点。如果没找到,直接返回错误。注意这里还没释放引用,因为后面还要用这个节点。
第二步:读取属性(调试用)
/* 读取 compatible 属性 */
proper = of_find_property(led.device_tree_node, "compatible", NULL);
if (proper == NULL) {
pr_err("compatible property find failed\n");
} else {
pr_info("compatible = %s\n", (char*)proper->value);
}
/* 读取 status 属性 */
ret = of_property_read_string(led.device_tree_node, "status", &str);
if (ret < 0) {
pr_err("status read failed!\n");
} else {
pr_info("status = %s\n", str);
}
这两步主要是为了调试,确认我们找到了正确的节点,并且节点状态是 "okay"。在实际生产代码里,这些调试信息可以去掉或改成 pr_debug()。
第三步:读取 reg 属性
ret = of_property_read_u32_array(led.device_tree_node, "reg", regdata, 10);
if (ret < 0) {
pr_err("reg property read failed!\n");
of_node_put(led.device_tree_node);
return -EINVAL;
}
pr_info("reg data:\n");
for (int i = 0; i < 10; i++) {
pr_cont("%#X ", regdata[i]);
}
pr_cont("\n");
这里我们读取 reg 属性的所有 10 个整数。注意出错处理里调用了 of_node_put(),避免内存泄漏。
第四步:映射寄存器地址
led.ccm_ccgr1 = of_iomap(led.device_tree_node, 0);
led.sw_mux_gpio = of_iomap(led.device_tree_node, 1);
led.sw_pad_gpio = of_iomap(led.device_tree_node, 2);
led.gpio_dr = of_iomap(led.device_tree_node, 3);
led.gpio_gdir = of_iomap(led.device_tree_node, 4);
if (!led.ccm_ccgr1 || !led.sw_mux_gpio || !led.sw_pad_gpio ||
!led.gpio_dr || !led.gpio_gdir) {
pr_err("ioremap failed!\n");
of_node_put(led.device_tree_node);
return -ENOMEM;
}
这里我们用 of_iomap() 一次性完成地址读取和映射。注意检查了所有映射是否成功,只要有一个失败就全部回滚。
第五步:硬件初始化
/* 使能 GPIO1 时钟 */
val = readl(led.ccm_ccgr1);
pr_info("CCGR1 raw value: 0x%08x\n Bits: ", val);
pr_bin_u32(val);
pr_cont("\n");
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, led.ccm_ccgr1);
/* 设置 GPIO1_IO03 复用功能为 GPIO */
writel(5, led.sw_mux_gpio);
/* 设置 GPIO1_IO03 电气属性 */
writel(0x10B0, led.sw_pad_gpio);
/* 设置 GPIO1_IO03 为输出功能 */
val = readl(led.gpio_gdir);
val &= ~(3 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, led.gpio_gdir);
/* 默认关闭 LED (高电平) */
val = readl(led.gpio_dr);
val |= (1 << 3);
writel(val, led.gpio_dr);
到这里,我们已经完成了从设备树读取配置到初始化硬件的完整流程。注意这里的寄存器操作(readl()/writel())操作的是映射后的虚拟地址,而不是设备树里的物理地址。
第六步:资源释放
void led_hw_deinit(void) {
pr_info("Deinit LED Hardware\n");
if (led.ccm_ccgr1) {
iounmap(led.ccm_ccgr1);
led.ccm_ccgr1 = NULL;
}
/* ... 其他 iounmap ... */
if (led.device_tree_node) {
of_node_put(led.device_tree_node);
led.device_tree_node = NULL;
}
}
卸载驱动时,我们释放所有映射的地址和节点引用。注意这里我们把指针设为 NULL,防止 double-free。
常见错误及处理方法
在实际使用 OF API 时,有几个常见的坑需要特别注意。
错误 1:忘记检查返回值
几乎所有 OF API 都有返回值,你必须检查它们:
/* 错误示例 */
struct device_node *node = of_find_node_by_path("/some-node");
/* 直接用 node,没检查 NULL */
of_property_read_u32(node, "some-prop", &val);
/* 正确示例 */
struct device_node *node = of_find_node_by_path("/some-node");
if (!node) {
pr_err("node not found\n");
return -ENODEV;
}
ret = of_property_read_u32(node, "some-prop", &val);
if (ret) {
pr_err("property read failed: %d\n", ret);
of_node_put(node);
return ret;
}
错误 2:忘记释放引用
这是内存泄漏的常见原因:
/* 错误示例 */
struct device_node *node = of_find_node_by_path("/some-node");
/* 用完后没有调用 of_node_put() */
/* 正确示例 */
struct device_node *node = of_find_node_by_path("/some-node");
/* ... 使用 node ... */
of_node_put(node);
错误 3:数组长度不匹配
用 of_property_read_u32_array() 时,确保你分配的数组足够大:
/* 危险示例 */
u32 data[5];
of_property_read_u32_array(node, "reg", data, 10); /* 数组越界! */
/* 安全示例 */
int count = of_property_count_elems_of_size(node, "reg", sizeof(u32));
u32 *data = kmalloc(count * sizeof(u32), GFP_KERNEL);
if (!data) return -ENOMEM;
of_property_read_u32_array(node, "reg", data, count);
/* ... 用完后 ... */
kfree(data);
错误 4:重复映射
不要对同一个地址调用多次 of_iomap():
/* 错误示例 */
void __iomem *addr1 = of_iomap(node, 0);
void __iomem *addr2 = of_iomap(node, 0); /* 重复映射! */
/* 正确做法 */
void __iomem *addr = of_iomap(node, 0);
/* 后续直接用 addr */
进阶:在内核源码中验证 API
上面我们介绍了十几个 OF API 函数。你可能已经跟着代码敲了一遍,也可能只是大概浏览了一下。不管怎样,当你准备自己写驱动的时候,一个问题迟早会冒出来:这些 API 真的存在吗?我的内核版本里能不能用?
这个问题不是在开玩笑。嵌入式开发有个特点:你手边的内核版本可能比最新的主线内核落后好几年,芯片厂商又会在自己的内核里加一些私货。你在网上看到的教程代码、在某个开源项目里看到的 API 调用,到你自己的内核里可能就编译不过了——要么函数签名不一样,要么头文件路径不对,最惨的是这个 API 根本就不存在。
所以,作为一名负责任的驱动开发者,我们需要养成一个习惯:在正式使用某个 API 之前,先在内核源码里验证它的存在性和正确性。这听起来很繁琐,但比起在生产环境踩坑,这点时间成本绝对是值得的。
验证方法论:grep 的艺术
验证 API 要回答三个问题:
- 这个 API 函数定义在哪个头文件里?
- 它的函数签名是什么?参数类型、返回值分别是什么?
- 在不同内核版本间,这个 API 有没有变化?
内核源码动辄几十万文件,用 grep -r 递归搜索效率很低。更好的方式是:
- 只搜索
include/目录——API 的声明都在头文件里 - 使用
git grep而不是普通的grep,前者更快 - 加上
-n参数显示行号,方便定位
比如验证 of_find_node_by_path:
cd /home/charliechen/imx-forge/third_party/linux_mainline
git grep -n "of_find_node_by_path" include/linux/of.h
输出:
include/linux/of.h:282:static inline struct device_node *of_find_node_by_path(const char *path)
include/linux/of.h:526:static inline struct device_node *of_find_node_by_path(const char *path)
同一个函数声明出现两次,是因为内核头文件的保护机制:第一处是真正的函数声明,第二处是当 CONFIG_OF 未定义时的空实现。所以搜索时需要看上下文,不能只看函数名。
主线内核验证:核心 API 一览
我们的验证环境是主线内核(linux_mainline),这是 Linux 内核的"官方版本",API 定义是最正统的参考。以下是我们上面用到过的核心 API 的验证结果:
of_find_node_by_path(定义在 include/linux/of.h):
extern struct device_node *of_find_node_opts_by_path(const char *path,
const char **opts);
static inline struct device_node *of_find_node_by_path(const char *path)
{
return of_find_node_opts_by_path(path, NULL);
}
有意思的发现:of_find_node_by_path 实际上是个 inline 函数,内部调用了 of_find_node_opts_by_path。参数是设备树节点的路径字符串,返回找到的节点指针(未找到则 NULL)。
of_property_read_string(定义在 include/linux/of.h):
extern int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);
注意 out_string 是指向指针的指针(const char **),函数不会复制字符串内容,而是直接指向设备树里存储的原始数据。你不需要手动释放这个字符串。
of_property_read_u32_array(定义在 include/linux/of.h):
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz)
参数 sz 是你想读取多少个元素(不是字节数!)。返回值 0 成功,-EINVAL 属性不存在,-ENODATA 属性没有值,-EOVERFLOW 属性数据比你想要的要小。
of_iomap(定义在 include/linux/of_address.h):
extern void __iomem *of_iomap(struct device_node *device, int index);
注意这个头文件路径:它不在 of.h 里,而是在单独的 of_address.h 里。使用时需要额外包含:
#include <linux/of.h>
#include <linux/of_address.h> /* 专门为地址映射 API */
of_node_put(定义在 include/linux/of.h):
#ifdef CONFIG_OF_DYNAMIC
extern void of_node_put(struct device_node *node);
#else
static inline void of_node_put(struct device_node *node) { }
#endif
有个有趣的发现:of_node_put 的实现取决于 CONFIG_OF_DYNAMIC。如果这个选项没开启,它就是个空函数。但为了代码的可移植性,我们还是应该始终调用它。
IMX 内核对比:确认兼容性
主线内核是参考标准,但我们实际用的是 NXP 的 IMX 内核。芯片厂商在移植时可能做修改。我们在 IMX 内核里搜索同样的 API:
cd /home/charliechen/imx-forge/third_party/linux-imx
git grep -B3 -A3 "of_find_node_by_path" include/linux/of.h
对比结果:上面验证的所有核心 API(of_find_node_by_path、of_property_read_string、of_property_read_u32_array、of_iomap、of_node_put),在主线内核和 IMX 内核中函数签名完全一致。我们可以放心使用这些 API,不用担心兼容性问题。
但这并不意味着所有 API 都是这样。使用高级功能(中断、时钟、GPIO 子系统)时,还是需要仔细验证。
API 差异的常见类型
虽然我们验证的这些核心 API 在两个内核间是一致的,但现实中确实存在差异。常见情况包括:
函数签名变化——最常见。旧版本可能只有 3 个参数,新版本加了第 4 个。应对策略:写代码前先确认你的内核版本对应的头文件。
头文件路径变化——有些 API 在不同版本里被移到了不同头文件。比如 of_gpio.h 在较新的内核里被重构了,一些函数移到了 linux/gpio/consumer.h。应对策略:编译不过时看报错信息,用 grep 搜索它现在在哪个头文件里。
行为变化——最隐蔽。函数签名没变,但行为变了。应对策略:用 git log -p --all -S "function_name" 查看函数的历史修改。
自动化验证脚本
每次手动 grep 确实繁琐。这里提供一个批量验证脚本思路:
#!/bin/bash
# batch_verify.sh - 批量验证多个 OF API
KERNEL_DIR=$1
shift
APIS=("$@")
echo "Verifying ${#APIS[@]} APIs in $KERNEL_DIR..."
echo "=============================================="
for api in "${APIS[@]}"; do
echo ""
echo "Checking: $api"
result=$(git -C "$KERNEL_DIR" grep -l "$api" include/ --include="*.h" 2>/dev/null)
if [ -n "$result" ]; then
echo " Found in: $result"
else
echo " NOT FOUND"
fi
done
使用方式:
./batch_verify.sh /home/charliechen/imx-forge/third_party/linux_mainline \
of_find_node_by_path \
of_property_read_string \
of_property_read_u32_array \
of_iomap \
of_node_put
建立验证习惯
我的建议是:每次你看到一个不熟悉的 API,先花一分钟验证一下。一分钟的时间可以避免后续几小时的调试痛苦。验证时记住这几个关键点:
- 确认头文件位置——知道该
include哪个文件 - 确认函数签名——知道参数类型和返回值
- 确认是否有版本差异——如果你在多个内核版本间移植代码
- 看看函数注释——内核开发者写的注释通常很有参考价值
还有一个建议:把你验证过的 API 记录下来。可以是一个简单的 markdown 文件,也可以是你自己的笔记工具。当你下次再用到这个 API 时,就不用重复验证了。
相关阅读
- 现代Qt开发教程(新手篇)1.15——正则与文本处理 - 相似度 58%