1. Linux设备树基础概念解析
设备树(Device Tree)是Linux内核中用于描述硬件配置的一种数据结构,它通过一种名为DTS(Device Tree Source)的文本格式定义硬件信息,然后编译成DTB(Device Tree Blob)二进制文件供内核使用。设备树的核心价值在于实现了硬件描述与驱动代码的分离,使得同一份内核镜像可以支持多种硬件平台。
在嵌入式Linux开发中,设备树已经成为标准配置方式。它主要解决了传统ARM架构中"board file"带来的问题:每当硬件变更时都需要重新编译内核。通过设备树,我们只需修改DTS文件并重新编译DTB,无需改动内核代码。
设备树的基本组成单元是节点(node),每个节点可以包含:
- 属性(property):键值对形式,描述硬件特性
- 子节点(child node):描述硬件组件的层次结构
一个典型的设备树节点示例如下:
c复制/* 示例设备树节点 */
example_device: example@12340000 {
compatible = "mycompany,example-v2";
reg = <0x12340000 0x1000>;
interrupts = <5 IRQ_TYPE_LEVEL_HIGH>;
status = "okay";
child-node {
child-property = "value";
};
};
2. 设备树核心操作函数分类
Linux内核提供了一系列以of_开头的函数来操作设备树,这些函数主要定义在include/linux/of.h头文件中。根据功能不同,我们可以将这些函数分为以下几类:
2.1 节点操作函数
节点是设备树的基本组成单元,内核提供了丰富的节点操作函数:
c复制// 获取子节点
struct device_node *of_get_child_by_name(const struct device_node *node, const char *name);
// 遍历子节点
#define for_each_child_of_node(parent, child) \
for (child = of_get_next_child(parent, NULL); child != NULL; \
child = of_get_next_child(parent, child))
// 获取父节点
struct device_node *of_get_parent(const struct device_node *node);
// 通过phandle获取节点
struct device_node *of_find_node_by_phandle(phandle handle);
注意事项:使用
of_get_child_by_name等主动获取节点的函数后,必须调用of_node_put()释放节点引用,而for_each_child_of_node宏会自动管理引用计数,无需手动释放。
2.2 属性操作函数
属性是节点中存储具体硬件信息的地方,内核提供了多种属性读取函数:
c复制// 读取字符串属性
int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
// 读取32位整数属性
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
// 读取数组属性
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz);
// 检查属性是否存在
bool of_property_read_bool(const struct device_node *np, const char *propname);
2.3 地址转换函数
设备树中经常需要处理内存映射和地址转换:
c复制// 获取寄存器地址和大小
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags);
// 地址转换
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr);
// 直接映射到内核虚拟地址
void __iomem *of_iomap(struct device_node *np, int index);
2.4 中断相关函数
设备树中的中断信息有专门的解析函数:
c复制// 获取中断号
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
// 获取中断父控制器
struct device_node *of_irq_find_parent(struct device_node *child);
// 解析中断属性
int of_irq_parse_one(struct device_node *device, int index, struct of_phandle_args *out_irq);
3. 设备树操作实战解析
3.1 基础节点与属性操作
让我们通过一个完整示例来演示如何在内核驱动中使用设备树函数:
c复制#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
static int example_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *node = dev->of_node;
const char *name;
u32 id, reg[2];
int ret;
/* 检查compatible属性 */
if (!of_device_is_compatible(node, "mycompany,example")) {
dev_err(dev, "Incompatible device\n");
return -ENODEV;
}
/* 读取字符串属性 */
ret = of_property_read_string(node, "name", &name);
if (ret) {
dev_err(dev, "Failed to read name: %d\n", ret);
return ret;
}
/* 读取32位整数属性 */
ret = of_property_read_u32(node, "id", &id);
if (ret) {
dev_err(dev, "Failed to read id: %d\n", ret);
return ret;
}
/* 读取数组属性 */
ret = of_property_read_u32_array(node, "reg", reg, 2);
if (ret) {
dev_err(dev, "Failed to read reg: %d\n", ret);
return ret;
}
dev_info(dev, "Device %s (id=%u) at 0x%08x, size 0x%x\n",
name, id, reg[0], reg[1]);
return 0;
}
static const struct of_device_id example_of_match[] = {
{ .compatible = "mycompany,example" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, example_of_match);
static struct platform_driver example_driver = {
.probe = example_probe,
.driver = {
.name = "example",
.of_match_table = example_of_match,
},
};
module_platform_driver(example_driver);
对应的设备树节点:
c复制example@12340000 {
compatible = "mycompany,example";
name = "sample-device";
id = <1>;
reg = <0x12340000 0x1000>;
};
3.2 字节序处理技巧
设备树中所有数值默认采用大端字节序存储,而现代CPU大多是小端架构,因此需要特别注意字节序转换:
c复制u32 value = be32_to_cpu(*prop_value);
在读取数组属性时,可以这样处理字节序:
c复制const __be32 *prop;
int len, i;
prop = of_get_property(node, "clock-rates", &len);
if (!prop)
return -EINVAL;
len /= sizeof(u32);
for (i = 0; i < len; i++) {
u32 rate = be32_to_cpu(prop[i]);
pr_info("Clock rate %u: %u Hz\n", i, rate);
}
经验分享:在调试时,如果发现读取的数值异常大(如0x64000000),很可能是忘记进行字节序转换导致的。设备树中的0x00000064在小端CPU上会被解释为0x64000000。
3.3 父节点与子节点操作
设备树中的父子节点关系是硬件层次结构的体现,内核提供了相应的操作函数:
c复制/* 获取父节点属性示例 */
static int get_parent_properties(struct device *dev, struct device_node *node)
{
struct device_node *parent;
u32 parent_id;
const char *parent_name;
int ret;
parent = of_get_parent(node);
if (!parent)
return -ENODEV;
ret = of_property_read_u32(parent, "parent-id", &parent_id);
if (ret) {
dev_err(dev, "Failed to read parent-id\n");
goto out;
}
ret = of_property_read_string(parent, "parent-name", &parent_name);
if (ret) {
dev_err(dev, "Failed to read parent-name\n");
goto out;
}
dev_info(dev, "Parent device: %s (id=%u)\n", parent_name, parent_id);
out:
of_node_put(parent);
return ret;
}
/* 遍历子节点示例 */
static void iterate_children(struct device *dev, struct device_node *node)
{
struct device_node *child;
dev_info(dev, "Children of %pOF:\n", node);
for_each_child_of_node(node, child) {
const char *name = of_node_get_name(child);
dev_info(dev, "- %s (%pOF)\n", name, child);
}
}
4. 高级设备树操作技巧
4.1 节点引用与phandle处理
设备树中经常��要引用其他节点,这通过phandle实现:
c复制/* 设备树示例 */
clocks: clock-controller@12350000 {
compatible = "mycompany,clocks";
reg = <0x12350000 0x1000>;
#clock-cells = <1>;
};
device@12340000 {
compatible = "mycompany,device";
reg = <0x12340000 0x1000>;
clocks = <&clocks 5>; /* 引用时钟控制器,使用第5个时钟 */
};
驱动中解析phandle引用:
c复制struct clk *clk;
struct device_node *clk_node;
int index = 0;
clk_node = of_parse_phandle(node, "clocks", index);
if (!clk_node)
return -ENODEV;
clk = of_clk_get_from_provider(clk_node);
if (IS_ERR(clk)) {
of_node_put(clk_node);
return PTR_ERR(clk);
}
of_node_put(clk_node);
4.2 设备树与平台设备
内核会自动将设备树中的某些节点转换为平台设备。要使节点被识别为平台设备,需要满足以下条件之一:
- 节点有compatible属性
- 父节点有
simple-bus或simple-mfd兼容性
c复制/* 设备树示例 */
parent@10000000 {
compatible = "mycompany,parent", "simple-bus";
reg = <0x10000000 0x1000>;
#address-cells = <1>;
#size-cells = <1>;
child@0 {
compatible = "mycompany,child";
reg = <0x0000 0x100>;
};
};
4.3 设备树覆盖(Overlay)技术
设备树覆盖允许在运行时动态修改设备树,常用于支持可插拔硬件模块:
c复制/* 加载设备树覆盖 */
echo example-overlay.dtbo > /sys/kernel/config/device-tree/overlays/load
/* 卸载设备树覆盖 */
rmdir /sys/kernel/config/device-tree/overlays/load
内核中处理覆盖的API:
c复制#include <linux/of_overlay.h>
int of_overlay_fdt_apply(void *fdt, u32 fdt_size, int *ovcs_id);
int of_overlay_remove(int ovcs_id);
int of_overlay_remove_all(void);
5. 设备树调试技巧
5.1 常用调试方法
-
查看设备树:
bash复制# 查看完整设备树 cat /proc/device-tree/* # 查看特定节点 ls /proc/device-tree/soc -
内核日志:
c复制dev_info(dev, "Node name: %pOF\n", node); // 打印节点全名 dev_info(dev, "Node address: %pOFn\n", node); // 打印节点名称 -
设备树反编译:
bash复制
dtc -I fs /proc/device-tree -O dts -o full.dts
5.2 常见问题排查
-
驱动未加载:
- 检查compatible字符串是否匹配
- 确认节点status是否为"okay"
- 检查父节点是否有
simple-bus兼容性
-
属性读取失败:
- 确认属性名称拼写正确
- 检查属性是否存在:
of_property_read_bool() - 确认属性类型是否正确(字符串/整数/数组)
-
内存映射问题:
- 检查reg属性格式是否正确
- 确认#address-cells和#size-cells设置正确
- 使用
of_iomap()后检查返回值
6. 设备树最佳实践
-
命名规范:
- 节点命名:
name@address格式 - 属性命名:小写字母加连字符(如
clock-frequency)
- 节点命名:
-
兼容性字符串:
- 使用"vendor,model"格式
- 按从具体到通用的顺序排列:
c复制compatible = "mycompany,device-v2", "mycompany,device", "generic-device";
-
属性使用:
- 标准属性优先使用内核定义的标准名称
- 自定义属性添加公司前缀:
mycompany,special-feature
-
文档注释:
c复制/* * @vendor: MyCompany * @description: Sample device node * @usage: Required properties - reg, interrupts */ device@12340000 { compatible = "mycompany,device"; reg = <0x12340000 0x1000>; }; -
测试验证:
- 使用
dtc编译时开启警告选项:-Wno-unit_address_vs_reg - 通过
uboot的fdt命令验证设备树 - 在内核启动参数中添加
dump-dtb选项保存实际使用的设备树
- 使用
掌握Linux内核中的设备树操作是嵌入式Linux开发的核心技能之一。通过合理使用设备树,可以大大提高驱动的可移植性和可维护性。在实际开发中,建议结合具体硬件平台反复练习,积累调试经验,逐步掌握设备树的精髓。
