3. 设备树详解:语法、编译与解析,驱动中如何获取节点信息
设备树,说白了就是描述硬件信息的配置文件。我刚开始接触Linux驱动时,总觉得这玩意儿有点多余——直接在代码里写死寄存器地址不香吗?后来维护过几个平台后才发现,没有设备树,代码简直没法看。
今天咱们就把它彻底讲透。从语法到编译,再到驱动里怎么用,一条龙搞定。
3.1 设备树的基本语法
设备树的文件后缀是.dts,编译后变成.dtb。语法其实很简单,就是树形结构,每个节点代表一个设备。
来看个最基础的例子:
/dts-v1/;
/ {
model = "MyBoard";
compatible = "vendor,myboard";
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0>;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>;
};
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
};
};
这里有几个关键点:
- 根节点:用
/表示,整棵树只有一个 - 节点名:格式是
名称@地址,比如uart0: serial@10000000 - 属性:键值对,值可以是字符串、数字、数组等
- 标签:
uart0:这种,方便其他地方引用
我的习惯:节点名里的地址最好和reg里的首地址一致。虽然语法上不强制,但这样读起来一目了然。我见过有人乱写,结果调试时找半天。
3.2 常用属性详解
有些属性是设备树规范里定义的,驱动开发者必须掌握。
| 属性名 | 含义 | 示例 |
|---|---|---|
compatible |
设备兼容性标识 | "vendor,device" |
reg |
寄存器地址和大小 | <0x10000000 0x1000> |
interrupts |
中断号、类型 | <0 33 4> |
status |
设备状态 | "okay" 或 "disabled" |
clocks |
时钟引用 | <&clkc 12> |
compatible 是最重要的属性。驱动就是靠它来匹配设备的。格式一般是"厂商,设备名",比如"ti,am335x-uart"。
我遇到过一个问题:有人把compatible写反了,写成"uart,ti",结果驱动死活匹配不上。嗯,这个顺序是有讲究的,厂商在前,设备在后。
3.3 设备树的编译
写好的.dts文件需要编译成.dtb才能被内核使用。工具是dtc(Device Tree Compiler)。
基本用法:
# 编译
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译(把dtb转回dts)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
# 检查语法
dtc -I dts -O dtb -o /dev/null myboard.dts
注意:编译时如果报错,多半是语法问题。最常见的是漏了分号、括号不匹配。我建议用-@选项生成符号表,方便后续overlay。
在内核编译时,设备树通常放在arch/arm/boot/dts/目录下。配置好CONFIG_OF后,make的时候会自动编译。
3.4 驱动中获取设备树信息
这部分是重点。驱动里怎么拿到设备树里定义的那些属性?
内核提供了丰富的API,都在include/linux/of.h里。我挑几个最常用的:
3.4.1 匹配设备
首先,驱动要声明自己支持哪些设备:
static const struct of_device_id my_dt_ids[] = {
{ .compatible = "vendor,mydevice", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_dt_ids);
static struct platform_driver my_driver = {
.probe = my_probe,
.driver = {
.name = "mydevice",
.of_match_table = my_dt_ids,
},
};
内核在probe时,会遍历of_match_table,找到匹配的compatible就调用probe。
3.4.2 读取属性
在probe函数里,我们可以拿到struct device_node *np,然后读取各种属性:
static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
u32 val;
int ret;
// 读取u32值
ret = of_property_read_u32(np, "clock-frequency", &val);
if (ret) {
dev_err(&pdev->dev, "failed to get clock-frequency\n");
return ret;
}
// 读取字符串
const char *str;
ret = of_property_read_string(np, "status", &str);
if (!ret)
dev_info(&pdev->dev, "status: %s\n", str);
// 读取数组
u32 reg[2];
ret = of_property_read_u32_array(np, "reg", reg, 2);
if (!ret)
dev_info(&pdev->dev, "base: 0x%x, size: 0x%x\n", reg[0], reg[1]);
return 0;
}
3.4.3 获取中断和内存资源
这部分我建议用platform_get_resource系列函数,更简洁:
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
int irq;
// 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 注册中断
ret = devm_request_irq(&pdev->dev, irq, my_isr, 0,
dev_name(&pdev->dev), dev);
if (ret)
return ret;
return 0;
}
避坑指南:我曾经在读取中断时直接用了of_irq_get,结果发现它和platform_get_irq的行为略有不同。前者不会处理irq domain的映射,后者会。所以,能用platform系列就别自己折腾of函数。
3.5 设备树解析的底层原理
你可能好奇,内核是怎么解析设备树的?
其实很简单。内核启动时,bootloader会把dtb的物理地址传给内核。内核在setup_arch里调用unflatten_device_tree,把扁平的dtb展开成树形结构——struct device_node的链表。
每个节点对应一个struct device_node,每个属性对应一个struct property。结构体定义大致如下:
struct device_node {
const char *name;
const char *type;
phandle phandle;
const char *full_name;
struct property *properties;
struct device_node *parent;
struct device_node *child;
struct device_node *sibling;
// ... 省略其他字段
};
struct property {
char *name;
int length;
void *value;
struct property *next;
// ...
};
说白了,就是一棵用指针串起来的树。我们用的那些of_xxx函数,本质上就是在这棵树上做遍历和查找。
3.6 实战:写一个完整的设备树驱动
咱们来个完整的例子。假设有个LED设备,设备树里这样定义:
myled: led@20000000 {
compatible = "vendor,myled";
reg = <0x20000000 0x10>;
interrupts = <0 45 4>;
led-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
brightness-max = <255>;
};
驱动里这样写:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/leds.h>
struct myled_priv {
struct device *dev;
void __iomem *base;
int irq;
int gpio;
int brightness_max;
};
static int myled_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct myled_priv *priv;
struct resource *res;
u32 val;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = dev;
// 获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
// 获取中断
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
// 获取GPIO
priv->gpio = of_get_named_gpio(dev->of_node, "led-gpios", 0);
if (!gpio_is_valid(priv->gpio)) {
dev_err(dev, "invalid gpio\n");
return -EINVAL;
}
// 获取亮度最大值
ret = of_property_read_u32(dev->of_node, "brightness-max", &val);
if (ret) {
dev_warn(dev, "use default brightness\n");
priv->brightness_max = 255;
} else {
priv->brightness_max = val;
}
// 请求GPIO
ret = devm_gpio_request(dev, priv->gpio, "myled");
if (ret)
return ret;
gpio_direction_output(priv->gpio, 0);
platform_set_drvdata(pdev, priv);
dev_info(dev, "myled probed, irq=%d, gpio=%d\n",
priv->irq, priv->gpio);
return 0;
}
static const struct of_device_id myled_of_match[] = {
{ .compatible = "vendor,myled", },
{ }
};
MODULE_DEVICE_TABLE(of, myled_of_match);
static struct platform_driver myled_driver = {
.probe = myled_probe,
.driver = {
.name = "myled",
.of_match_table = myled_of_match,
},
};
module_platform_driver(myled_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
这个例子涵盖了设备树驱动的大部分场景:内存映射、中断、GPIO、自定义属性。你想想看,如果没有设备树,这些信息都得硬编码在驱动里,换个板子就得改代码,多麻烦。
3.7 调试技巧
设备树出问题时,怎么查?
- 查看设备树是否被正确加载:
ls /proc/device-tree/,能看到展开后的节点 - 查看某个节点的属性:
cat /proc/device-tree/led@20000000/compatible - 查看驱动是否匹配:
dmesg | grep myled,看probe有没有被调用 - 查看设备树编译错误:
dtc -I dts -O dtb myboard.dts 2>&1
我曾经踩过的坑:有一次设备树里reg写错了大小,导致ioremap只映射了部分地址,读写寄存器时触发异常。排查了半天才发现是设备树的问题。所以,写设备树时一定要仔细核对datasheet里的地址范围。
好了,设备树这部分就讲到这里。说白了,它就是硬件信息的配置文件,驱动通过它来获取资源,实现硬件无关化。掌握了这些,你就能写出更灵活、更可移植的驱动了。