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里的地址范围。

好了,设备树这部分就讲到这里。说白了,它就是硬件信息的配置文件,驱动通过它来获取资源,实现硬件无关化。掌握了这些,你就能写出更灵活、更可移植的驱动了。