06. OF API 验证——在内核源码中寻找真相
前言:为什么我们需要验证API
在前一章里,我们一口气介绍了十几个 OF API 函数。你可能跟着代码敲了一遍,也可能只是大概浏览了一下。不管怎样,当你准备自己写驱动的时候,一个问题迟早会冒出来:这些 API 真的存在吗?
这个问题不是在开玩笑。嵌入式开发有个特点:你手头的内核版本可能比最新的主线内核落后好几年,芯片厂商(比如 NXP)又会在自己的内核里加一些私货。你在网上看到的教程代码、在某个开源项目里看到的 API 调用,到你自己的内核里可能就编译不过了——要么函数签名不一样,要么头文件路径不对,最惨的是这个 API 根本就不存在。
更糟糕的是,有些 API 的行为在不同内核版本间会有变化。你以为传个 NULL 没问题,结果在旧内核里直接 panic。你以为这个函数会自动处理引用计数,结果内存泄漏得一塌糊涂。
所以,作为一名负责任的驱动开发者,我们需要养成一个习惯:在正式使用某个 API 之前,先在内核源码里验证它的存在性和正确性。这听起来很繁琐,但比起在生产环境踩坑,这点时间成本绝对是值得的。
这一章我们就来聊聊怎么验证 OF API。我们会在主线内核和 IMX 内核里分别查找这些 API 的定义,对比它们之间的差异,最后给你一些自动化的验证思路。相信我,这套流程掌握之后,你再用任何新 API 都会更有底气。
验证方法论:grep的艺术
在正式动手之前,我们先明确一下验证的目标。我们想知道三件事:
- 这个 API 函数到底定义在哪个头文件里?
- 它的函数签名是什么?参数类型、返回值分别是什么?
- 在不同内核版本间,这个 API 有没有变化?
要回答这些问题,我们手里的武器就是 grep 和源码阅读能力。这里先说句实话:内核源码搜索这件事,用对工具是关键。
很多人习惯用 grep -r 递归搜索整个内核目录,但这种方式效率很低。内核源码动辄几十万文件,搜索一个常见函数名会返回几千个结果,你根本看不过来。更好的方式是:
- 只搜索
include/目录,因为 API 的声明都在头文件里 - 使用
git grep而不是普通的grep,前者更快且支持正则表达式 - 搜索时加上
-n参数显示行号,方便定位
我们来实际操作一下。假设我们想验证 of_find_node_by_path 这个 API:
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)这里有两行结果,为什么同一个函数声明会出现两次?仔细看行号,你会发现它们分别在不同的代码块里。第一个在 282 行,第二个在 526 行。这实际上是内核的头文件保护机制:第一处是真正的函数声明,第二处是当 CONFIG_OF 未定义时的空实现。
这就是为什么我们在搜索时需要看上下文,而不是只看函数名本身。接下来我们逐个验证我们在前一章用到的核心 API。
主线内核验证:逐个验证核心API
我们现在来系统验证一下前一章介绍的所有核心 API。我们的验证环境是主线内核(linux_mainline),这是 Linux 内核的"官方版本"。所有芯片厂商的内核都是基于这个版本修改的,所以主线内核的 API 定义是最"正统"的参考。
of_find_node_by_path
这个函数用于通过路径查找设备树节点。我们先在主线内核里搜索它的定义:
$ git grep -B3 -A3 "of_find_node_by_path" include/linux/of.h输出结果(行 279-285):
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。这个设计的意图是提供一个简化接口,当你不需要处理选项(options)时,直接传路径就行了。
函数签名很清晰:
- 参数:
const char *path——设备树节点的路径字符串,比如"/imx_aes_led" - 返回值:
struct device_node *——找到的节点指针,如果没找到返回NULL
头文件位置:include/linux/of.h
of_property_read_string
这个函数用于从设备树节点读取字符串属性。我们搜索它的定义:
$ git grep -B3 -A3 "^extern int of_property_read_string" include/linux/of.h输出结果(行 350-352):
extern int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);函数签名分析:
- 参数
np:要读取的设备树节点 - 参数
propname:属性名字,比如"status"或"compatible" - 参数
out_string:输出参数,函数会把找到的字符串指针写到这里 - 返回值:
int类型,0 表示成功,负值表示失败
头文件位置:include/linux/of.h
这里有个细节需要注意:out_string 是一个指向指针的指针(const char **)。这意味着函数不会复制字符串内容,而是直接指向设备树里存储的原始数据。你不需要手动释放这个字符串,它的生命周期由设备树节点管理。
of_property_read_u32_array
这个函数用于从设备树读取 32 位整数数组。搜索定义:
$ git grep -B15 -A5 "static inline int of_property_read_u32_array" include/linux/of.h输出结果(行 1373-1393):
/**
* of_property_read_u32_array - Find and read an array of 32 bit integers
* from a property.
*
* @np: device node from which the property value is to be read.
* @propname: name of the property to be searched.
* @out_values: pointer to return value, modified only if return value is 0.
* @sz: number of array elements to read
*
* Return: 0 on success, -EINVAL if the property does not exist,
* -ENODATA if property does not have a value, and -EOVERFLOW if the
* property data is smaller than sz elements.
*/
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz)这个函数的注释非常详细,值得好好读一下。关键信息:
- 参数
out_values:指向u32数组的指针,函数会把读取到的数据写进去 - 参数
sz:你想读取多少个元素(不是字节数!) - 返回值:0 成功,
-EINVAL表示属性不存在,-ENODATA表示属性没有值,-EOVERFLOW表示属性数据比你想要的要小
头文件位置:include/linux/of.h
of_iomap
这个函数用于映射设备树里的寄存器地址到虚拟地址空间。搜索定义:
$ git grep -B3 -A3 "of_iomap" include/linux/of_address.h输出结果(行 65):
extern void __iomem *of_iomap(struct device_node *device, int index);函数签名:
- 参数
device:设备树节点 - 参数
index:reg属性中的索引(从 0 开始),对应第几组地址 - 返回值:
void __iomem *——映射后的虚拟地址,失败返回NULL
头文件位置:include/linux/of_address.h
注意这个头文件路径:它不在 of.h 里,而是在单独的 of_address.h 里。这意味着当你使用 of_iomap 时,需要额外包含这个头文件。我们在驱动代码里就是这么做的:
#include <linux/of.h>
#include <linux/of_address.h> /* 专门为地址映射 API */of_node_put
这个函数用于释放设备树节点的引用计数。搜索定义:
$ git grep -B3 -A3 "of_node_put" include/linux/of.h | head -20输出结果(行 127-136):
#ifdef CONFIG_OF_DYNAMIC
extern struct device_node *of_node_get(struct device_node *node);
extern void of_node_put(struct device_node *node);
#else /* CONFIG_OF_DYNAMIC */
/* Dummy ref counting routines - to be implemented later */
static inline struct device_node *of_node_get(struct device_node *node)
{
return node;
}
static inline void of_node_put(struct device_node *node) { }
#endif /* !CONFIG_OF_DYNAMIC */这里有个有趣的发现:of_node_put 的实现取决于内核配置选项 CONFIG_OF_DYNAMIC。如果这个选项没开启,of_node_put 就是一个空函数——什么都不做。这意味着在某些内核配置下,你可以不调用 of_node_put,但为了代码的可移植性,我们还是应该始终调用它。
头文件位置:include/linux/of.h
IMX内核验证:对比验证
主线内核是参考标准,但我们实际用的是 NXP 的 IMX 内核。芯片厂商在移植内核时,可能会做一些修改:有的是添加新 API,有的是调整现有 API 的行为,还有的可能引入一些 bug。
我们现在在 IMX 内核里搜索同样的 API,看看有没有差异。
of_find_node_by_path 在 IMX 内核中
$ cd /home/charliechen/imx-forge/third_party/linux-imx
$ git grep -B3 -A3 "of_find_node_by_path" include/linux/of.h输出结果(行 282-284):
static inline struct device_node *of_find_node_by_path(const char *path)
{
return of_find_node_opts_by_path(path, NULL);
}对比主线内核:完全一致。这很好,说明 IMX 内核在这个 API 上没有做修改。
of_iomap 在 IMX 内核中
$ git grep -B3 -A3 "of_iomap" include/linux/of_address.h输出结果(行 64):
extern void __iomem *of_iomap(struct device_node *device, int index);对比主线内核:函数签名完全一致。头文件位置也一样。
of_property_read_u32_array 在 IMX 内核中
$ git grep -B15 -A5 "static inline int of_property_read_u32_array" include/linux/of.h输出结果(行 1339-1359):
/**
* of_property_read_u32_array - Find and read an array of 32 bit integers
* from a property.
* [注释内容与主线内核完全一致]
*/
static inline int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, size_t sz)对比主线内核:函数签名和注释完全一致。
验证结论
经过对比,我们发现 IMX 内核在这些核心 OF API 上与主线内核保持一致。这意味着我们可以放心使用这些 API,不用担心兼容性问题。但这并不意味着所有 API 都是这样——我们在使用一些高级功能(比如中断、时钟、GPIO 子系统)时,还是需要仔细验证。
API差异分析:兼容性考量
虽然我们验证的这几个 API 在两个内核间是一致的,但现实中确实存在 API 差异的情况。我们这里总结一下常见的差异类型,以及应对策略。
函数签名变化
最常见的变化是参数类型或数量的调整。比如某个早期版本的 API 可能是这样的:
/* 旧版本 */
int of_property_read_u32(struct device_node *np,
const char *propname,
u32 *out_value);新版本可能添加了额外的检查参数:
/* 新版本 */
int of_property_read_u32(struct device_node *np,
const char *propname,
u32 *out_value,
bool strict);应对策略:写代码前先确认你的内核版本。最可靠的方式是直接看头文件里的定义。
头文件路径变化
有些 API 在不同内核版本里被移到了不同的头文件。比如 of_gpio.h 在较新的内核里被重构了,一些函数移到了 linux/gpio/consumer.h。
应对策略:编译不过时,看报错信息。编译器会告诉你哪个符号找不到,然后用 grep 搜索它现在在哪个头文件里。
行为变化
这是最隐蔽的差异。函数签名没变,但行为变了。比如某个早期版本的 API 会自动处理某些默认值,新版本不处理了,需要你显式指定。
应对策略:看内核的 git log。如果你怀疑某个 API 的行为变了,可以用 git log -p --all -S "function_name" 查看这个函数的历史修改。
验证脚本:自动化工具
每次用新 API 都手动 grep 一遍确实很繁琐。我们这里提供一个简单的验证脚本思路,你可以根据自己的需求扩展。
基本验证脚本
#!/bin/bash
# verify_of_api.sh - 验证 OF API 是否存在于内核中
KERNEL_DIR=$1
API_NAME=$2
if [ -z "$KERNEL_DIR" ] || [ -z "$API_NAME" ]; then
echo "Usage: $0 <kernel_dir> <api_name>"
exit 1
fi
echo "Searching for $API_NAME in $KERNEL_DIR..."
echo "=========================================="
# 在头文件中搜索定义
echo "Definitions in header files:"
git -C "$KERNEL_DIR" grep -rn "$API_NAME" include/ --include="*.h" | grep -E "(extern|static inline).*$API_NAME"
# 在源文件中搜索实现
echo ""
echo "Implementations in source files:"
git -C "$KERNEL_DIR" grep -rn "$API_NAME" --include="*.c" drivers/ of/ | grep -E "^.*\.c:[0-9]+:.*$API_NAME.*\(" | head -5使用方式:
./verify_of_api.sh /home/charliechen/imx-forge/third_party/linux_mainline of_iomap批量验证脚本
如果你有一堆 API 要验证,可以写一个批量脚本:
#!/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 验证的流程。你可能觉得这些步骤很繁琐,但相信我,一旦你养成习惯,它会成为你写代码时的"安全带"。
我的建议是:每次你看到一个不熟悉的 API,先花一分钟验证一下。一分钟的时间可以避免后续几小时的调试痛苦,这绝对是值得的。
验证的时候,记住这几个关键点:
- 确认头文件位置——知道该
include哪个文件 - 确认函数签名——知道参数类型和返回值
- 确认是否有版本差异——如果你在多个内核版本间移植代码
- 看看函数注释——内核开发者写的注释通常很有参考价值
还有一个建议:把你验证过的 API 记录下来。可以是一个简单的 markdown 文件,也可以是你自己的笔记工具。当你下次再用到这个 API 时,就不用重复验证了。我的个人笔记里就有这样一张表,记录了我常用的 OF API 及其验证信息,这给我节省了很多时间。
下一步
现在我们已经掌握了 OF API 的验证方法,你知道如何在内核源码中确认 API 的存在性和正确性了。这给你提供了在使用新 API 时的信心和安全感。
接下来的章节,我们会把这些知识应用到实际的驱动开发中。我们会看看设备树是如何与 Linux 驱动模型配合工作的,特别是平台设备驱动(platform device driver)这一块。你会发现,设备树和驱动模型的结合,让硬件抽象变得非常优雅。
继续阅读: 07. 驱动代码对比 了解设备树如何与驱动模型配合工作。