ESC
输入关键词搜索文章标题和内容

嵌入式Linux设备树开发实战:从DTS编写到驱动匹配全链路

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式Linux设备树开发实战:从DTS编写到驱动匹配全链路

设备树把硬件描述从内核代码中剥离出来,驱动不再关心寄存器地址写死在哪,只管从DTB里读配置。本文覆盖DTS语法、编译流程、驱动匹配机制、内核API调用、调试技巧,给出兼容Linux 6.8的完整代码示例。

一、为什么需要设备树

Linux 3.x之前,ARM平台的硬件信息全部硬编码在arch/arm/mach-xxx/目录的board文件里。每加一块板子,就要往内核里塞一个新文件,把GPIO、I2C、SPI的寄存器地址和中断号逐个写死。到Linux 3.7的时候,arch/arm/下堆积了400多个board文件,维护成本失控制
设备树解决的核心问题硬件描述与驱动代码分支。板级信息放在独立的DTS文件里,编译成DTB二进制由bootloader传给内核,同一份内核镜像可以适配不同硬件配置。

flowchart LR A["DTS源文件<br/>.dts/.dtsi"] -->|"dtc编译"| B["DTB二进<br/>.dtb"] B -->|"bootloader传递 | C["内核解析<br/>unflatten_device_tree"] C -->|"of_match"| D["驱动匹配<br/>probe调用"]

整个流程:开发者编写DTS 。dtc编译器生成DTB 。U-Boot加载DTB到内存并传给内核 。内核解析DTB生成device_node。。Platform总线用compatible字符串匹配驱动。调用probe函数。

二、设备树核心概念

2.1 节点和属。

设备树的基本组成单元节点(node)属性(property),结构类似文件系统目录:

/ {                    // 根节。    node1 {            // 子节。        prop1 = "abc"; // 字符串属。        prop2 = <0x1>; // 整数属。    };
};
  • 节点:用{}包裹,描述一个硬件设备或逻辑分组,支持嵌。- *属:key = value形式,描述节点的具体参数

2.2 标准属。

属性 类型 用途 示例
compatible 字符串列表 驱动匹配标识,最关键的属性 "vendor,dev"
reg 整数数组 设备寄存器地址和长度 <0x1000 0x100>
#address-cells 整数 子节点reg中地址占几个u32 <1>
#size-cells 整数 子节点reg中长度占几个u32 <1>
interrupts 整数数组 中断号和触发方式 <0 29 4>
clocks 引用+整数 设备使用的时钟 <&clk 1>
status 字符串 设备状态 "okay" / "disabled"
phandle 整数 节点唯一标识,供其他节点引用 <1>

*compatible属是驱动匹配的核心。它是一个字符串列表,内核按从左到右的顺序尝试匹配:

compatible = "vendor,mydev", "generic,mydev-class";

驱动先尝试匹配"vendor,mydev",匹配不上再尝试"generic,mydev-class"。
*reg属的解读依赖父节点的#address-cells和#size-cells。

soc {
    #address-cells = <1>;  // 地址2个u32
    #size-cells = <1>;     // 长度2个u32
    mydev@1000 {
        reg = <0x1000 0x100>;  // 地址0x1000,长度0x100
    };
};

2.3 特殊节点

节点 用途 典型内容
/chosen 启动参数 bootargs、stdout-path
/aliases 节点别名 serial0 = &uart0
/memory 物理内存布局 reg = <0x0 0x40000000>
/cpus CPU拓扑 各CPU核的频率、缓存信息

2.4 phandle引用

当一个节点需要引用另一个节点时,用&符号加标签(label)实现:

clk: oscillator {
    compatible = "fixed-clock";
    clock-frequency = <24000000>;
};

uart0 {
    clocks = <&clk>;  // 引用oscillator节点
};

编译器自动为clk标签分配phandle值,<&clk>展开为<0x1>(phandle值)。

三、设备树语法详解

3.1 基本语法规则

/dts-v1/;                          // DTS版本声明,必须放在第一。
/ {                                // 根节。    // 这是注释
    string-prop = "hello";         // 字符串    int-prop = <42>;               // 32位整。    int-array = <1 2 3>;           // 整数数组
    string-list = "a", "b";       // 字符串列。    byte-array = [00 01 02];      // 字节数组
    bool-prop;                     // 布尔属性(存在即为true。    ref-prop = <&label>;          // phandle引用
};

3.2 数据类型

类型 语法 说明
字符串 "text" 以null结尾的字符串
32位整数 <0x1000> 大端序,尖括号包裹
字符串列表 "a", "b" 逗号分隔
字节数组 [00 01 ff] 两位十六进制
引用 <&label> phandle引用
布尔 prop-name; 无值,存在即真

3.3 完整DTS示例

下面是一个包含GPIO、I2C、SPI设备的板级描述:

/dts-v1/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

/ {
    model = "MyBoard Rev1.0";
    compatible = "vendor,myboard", "vendor,myboard-family";

    // 启动参数
    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
        stdout-path = &uart0;
    };

    // 物理内存。GB起始地址0x40000000
    memory@40000000 {
        device_type = "memory";
        reg = <0x40000000 0x40000000>;
    };

    // CPU配置
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu0: cpu@0 {
            compatible = "arm,cortex-a53";
            reg = <0>;
            clock-frequency = <1200000000>;
        };
    };

    // SoC内部总线
    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        // GPIO控制。        gpio0: gpio@2000000 {
            compatible = "vendor,gpio";
            reg = <0x2000000 0x1000>;
            gpio-controller;
            #gpio-cells = <2>;
            interrupt-controller;
            #interrupt-cells = <2>;
        };

        // I2C控制。        i2c0: i2c@3000000 {
            compatible = "vendor,i2c";
            reg = <0x3000000 0x1000>;
            interrupts = <0 25 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&clk_bus>;
            clock-frequency = <100000>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            // I2C从设备:温度传感。            temperature-sensor@48 {
                compatible = "ti,tmp102";
                reg = <0x48>;
            };
        };

        // SPI控制。        spi0: spi@4000000 {
            compatible = "vendor,spi";
            reg = <0x4000000 0x1000>;
            interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&clk_bus>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            // SPI从设备:显示例            display@0 {
                compatible = "vendor,lcd";
                reg = <0>;
                spi-max-frequency = <10000000>;
                reset-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
            };
        };

        // UART
        uart0: serial@5000000 {
            compatible = "vendor,uart";
            reg = <0x5000000 0x1000>;
            interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&clk_bus>;
            status = "okay";
        };
    };

    // 固定时钟
    clk_bus: clk-bus {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <200000000>;
    };

    // LED
    leds {
        compatible = "gpio-leds";

        led-heartbeat {
            label = "heartbeat";
            gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "heartbeat";
        };
    };
};

3.4 #include。dtsi文件组织

设备树文件分三层。
| 文件类型 | 后缀 | 用途 |
|:---------|:-----|:-----|
| DTS | .dts | 板级描述,包含具体硬件配置 |
| DTSI | .dtsi | SoC级公共定义,被DTS包含 |
| DTSI | .dtsi | 厂商级公共定义,被SoC DTSI包含 |

典型包含关系。

// board-v2.dts
/dts-v1/;
#include "soc-v1.dtsi"       // SoC公共定义
#include <dt-bindings/gpio/gpio.h>  // 内核头文件
/ {
    model = "MyBoard V2";
    // 覆盖DTSI中的默认配置
    &uart0 {
        status = "okay";     // DTSI中默认disabled,这里启。    };
};

&label语法可以在包含DTSI后,对已有节点进行追加或覆盖属性,这是板级定制的主要手段。

四、设备树编译流程

4.1 dtc编译。

dtc(Device Tree Compiler)是设备树的标准编译工具。

# DTS编译为DTB
dtc -I dts -O dtb -o board.dtb board.dts

# DTB反编译为DTS(调试常用)
dtc -I dtb -O dts -o board.dts board.dtb

4.2 Makefile自动编译

Linux内核源码中,设备树通过Makefile自动编译。

# arch/arm64/boot/dts/vendor/Makefile
dtb-$(CONFIG_ARCH_VENDOR) += board-v1.dtb
dtb-$(CONFIG_ARCH_VENDOR) += board-v2.dtb

编译命令。

来自 linuxros.cn · linuxROS
# 编译所有设备树
make dtbs

# 只编译指定目录的设备。make vendor/board-v1.dtb

4.3 反编译运行时DTB

在运行的系统上,可以直接读取内核实际使用的DTB。

# 反编译当前系统的设备。dtc -I dtb -O dts /sys/firmware/fdt 2>/dev/null

# 或者从proc读取
cat /proc/device-tree/compatible

4.4 /sys/firmware/devicetree/查看

内核把解析后的设备树导出到sysfs,每个节点对应一个目录,每个属性对应一个文件:

# 查看根节点compatible属。cat /sys/firmware/devicetree/base/compatible

# 查看某个节点的reg属性(二进制,用hexdump看)
hexdump -C /sys/firmware/devicetree/base/soc/i2c@3000000/reg

# 列出某节点下的所有子节点
ls /sys/firmware/devicetree/base/soc/

# 查看属性列。ls /sys/firmware/devicetree/base/soc/i2c@3000000/

五、驱动与设备树匹配机。

5.1 compatible字符串匹。

驱动通过of_device_id结构体声明自己支持的设备,compatible字段必须与DTS中的完全一致(区分大小写)。

static const struct of_device_id mydev_of_match[] = {
    { .compatible = "vendor,mydev", },
    { .compatible = "vendor,mydev-v2", .data = &mydev_v2_data },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

MODULE_DEVICE_TABLE(of, ...)宏的作用是让depmod工具在模块安装时生成modules.ofmap文件,这样modprobe可以根据设备树的compatible自动加载对应驱动模块。

5.2 匹配流程

flowchart TB A(["DTB解析<br/>unflatten_device_tree"]) --> B(["创建device_node"]) B --> C(["of_platform_populate<br/>创建platform_device"]) C --> D{"of_match_table<br/>compatible匹配?"} D -->|"匹配成功"| E(["调用driver.probe"]) D -->|"匹配失败"| F{"id_table匹配?"} F -->|"匹配成功"| E F -->|"匹配失败"| G{"driver.name<br/>== device.name?"} G -->|"匹配成功"| E G -->|"匹配失败"| H(["无匹<br/>驱动未加载"])

匹配优先级:of_match_table > id_table > name。在设备树场景下,of_match是主要匹配方式,后两种是遗留兼容机制。

5.3 驱动注册代码

static int mydev_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;

    dev_info(dev, "probe: compatible matched\n");
    // 后续从np读取设备树属。    return 0;
}

static int mydev_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove\n");
    return 0;
}

static struct platform_driver mydev_driver = {
    .probe  = mydev_probe,
    .remove = mydev_remove,
    .driver = {
        .name = "mydev",
        .of_match_table = mydev_of_match,
    },
};
module_platform_driver(mydev_driver);

六、驱动中获取设备树信息

内核提供一整套of_*函数,让驱动从设备树节点中读取配置。

6.1 常用API速查

API 功能 返回回调
of_find_property(np, "name", NULL) 查找属性是否存在 struct property *
of_property_read_u32(np, "name", &val) 读取32位整数 0成功,负数失败
of_property_read_string(np, "name", &str) 读取字符串 0成功,负数失败
of_property_read_u32_array(np, "name", arr, len) 读取整数数组 0成功,负数失败
of_get_named_gpio(np, "name-gpios", 0) 获取GPIO编号 GPIO号或负数错误
of_parse_phandle(np, "clocks", 0) 解析phandle引用 struct device_node *
of_address_to_resource(np, 0, &res) 地址转resource 0成功
of_iomap(np, 0) 映射寄存器地址 void __iomem *
of_property_count_u32_elems(np, "name") 统计整数数组元素个数 元素数或负数错误

6.2 读取基本类型

struct device_node *np = pdev->dev.of_node;
u32 freq;
const char *name;

// 读取整数
if (of_property_read_u32(np, "clock-frequency", &freq))
    freq = 100000;  // 默认。
// 读取字符号if (of_property_read_string(np, "label", &name))
    name = "unknown";

// 读取整数数组
u32 pins[4];
int count = of_property_count_u32_elems(np, "pins");
if (count > 0 && count <= 4)
    of_property_read_u32_array(np, "pins", pins, count);

6.3 获取GPIO

// 方式一:of_get_named_gpio(旧API。.8仍可用)
int gpio = of_get_named_gpio(np, "reset-gpios", 0);
if (!gpio_is_valid(gpio)) {
    dev_err(dev, "reset-gpios not found\n");
    return -EINVAL;
}

// 方式二:gpiod_get(推荐,基于描述符)
struct gpio_desc *reset_gpiod;
reset_gpiod = gpiod_get(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset_gpiod)) {
    dev_err(dev, "failed to get reset gpio\n");
    return PTR_ERR(reset_gpiod);
}

gpiod_get方式不需要写-gpios后缀,内核自动拼接。DTS中写reset-gpios = <&gpio0 5 0>,代码里传"reset"即可。

6.4 解析phandle引用

// 解析clocks引用
struct device_node *clk_np;
clk_np = of_parse_phandle(np, "clocks", 0);
if (clk_np) {
    u32 rate;
    of_property_read_u32(clk_np, "clock-frequency", &rate);
    of_node_put(clk_np);  // 用完必须释放
}

6.5 地址映射

// 方式一:of_iomap(一步完成地址查找和映射)
void __iomem *base;
base = of_iomap(np, 0);
if (!base) {
    dev_err(dev, "failed to map registers\n");
    return -ENOMEM;
}

// 方式二:of_address_to_resource + devm_ioremap_resource(推荐)
struct resource res;
if (of_address_to_resource(np, 0, &res))
    return -EINVAL;

base = devm_ioremap_resource(dev, &res);
if (IS_ERR(base))
    return PTR_ERR(base);

devm_ioremap_resource的优势是驱动卸载时自动解除映射,不需要手动调用iounmap。

6.6 完整代码示例

下面是一个完整的Platform驱动,从设备树读取配置并初始化设备:

// mydev_dt.c - 从设备树读取配置的Platform驱动(Linux 6.8。#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_address.h>
#include <linux/gpio/consumer.h>
#include <linux/io.h>
#include <linux/interrupt.h>

struct mydev_priv {
    void __iomem        *base;
    struct gpio_desc    *reset_gpio;
    struct gpio_desc    *irq_gpio;
    int                  irq;
    u32                  clock_freq;
    u32                  buffer_size;
};

static irqreturn_t mydev_irq_handler(int irq, void *dev_id)
{
    struct mydev_priv *priv = dev_id;
    pr_info("mydev: irq triggered\n");
    return IRQ_HANDLED;
}

static int mydev_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct mydev_priv *priv;
    struct resource res;
    int ret;

    if (!np) {
        dev_err(dev, "no device tree node\n");
        return -EINVAL;
    }

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    platform_set_drvdata(pdev, priv);

    /* 1. 读取寄存器地址并映*/
    ret = of_address_to_resource(np, 0, &res);
    if (ret) {
        dev_err(dev, "failed to get register resource\n");
        return ret;
    }
    priv->base = devm_ioremap_resource(dev, &res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 2. 获取GPIO(描述符方式*/
    priv->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
    if (IS_ERR(priv->reset_gpio)) {
        dev_err(dev, "failed to get reset gpio\n");
        return PTR_ERR(priv->reset_gpio);
    }

    /* 3. 读取整数属性(带默认值) */
    if (of_property_read_u32(np, "clock-frequency", &priv->clock_freq))
        priv->clock_freq = 100000000;  // 默认100MHz

    if (of_property_read_u32(np, "buffer-size", &priv->buffer_size))
        priv->buffer_size = 4096;

    /* 4. 获取中断 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0) {
        dev_err(dev, "failed to get irq\n");
        return priv->irq;
    }

    ret = devm_request_irq(dev, priv->irq, mydev_irq_handler,
                           IRQF_TRIGGER_FALLING, "mydev-irq", priv);
    if (ret) {
        dev_err(dev, "failed to request irq\n");
        return ret;
    }

    /* 5. 硬件复位 */
    gpiod_set_value(priv->reset_gpio, 1);
    msleep(10);
    gpiod_set_value(priv->reset_gpio, 0);
    msleep(50);

    dev_info(dev, "probe ok: freq=%u buf=%u irq=%d\n",
             priv->clock_freq, priv->buffer_size, priv->irq);
    return 0;
}

static int mydev_remove(struct platform_device *pdev)
{
    struct mydev_priv *priv = platform_get_drvdata(pdev);
    gpiod_set_value(priv->reset_gpio, 1);
    dev_info(&pdev->dev, "remove ok\n");
    return 0;
}

static const struct of_device_id mydev_of_match[] = {
    { .compatible = "vendor,mydev", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

static struct platform_driver mydev_driver = {
    .probe  = mydev_probe,
    .remove = mydev_remove,
    .driver = {
        .name = "mydev-dt",
        .of_match_table = mydev_of_match,
    },
};
module_platform_driver(mydev_driver);

MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Device Tree Platform Driver Demo");
MODULE_LICENSE("GPL");

对应的DTS节点。

mydev@6000000 {
    compatible = "vendor,mydev";
    reg = <0x6000000 0x1000>;
    interrupts = <0 30 4>;
    reset-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
    clock-frequency = <200000000>;
    buffer-size = <8192>;
};

七、设备树调试技。

7.1 dtc编译检查语。

# 编译时检查语法错。dtc -I dts -O dtb board.dts > /dev/null

# 带详细错误信息dtc -I dts -O dtb -o /dev/null board.dts 2>&1

7.2 /sys/firmware/devicetree/查看运行时设备树

# 查看compatible属。cat /sys/firmware/devicetree/base/compatible

# 查看节点是否存在
ls /sys/firmware/devicetree/base/mydev@6000000/

# 查看reg属性(二进制)
hexdump -C /sys/firmware/devicetree/base/mydev@6000000/reg

# 查看status属。cat /sys/firmware/devicetree/base/mydev@6000000/status

7.3 fdtput/fdtdump工具

# 修改DTB中的属性。fdtput -t s board.dtb /mydev status "okay"

# 读取DTB中的属性。fdtget board.dtb /mydev compatible

# 导出DTB为可读文件fdtdump board.dtb

7.4 内核启动参数指定DTB

# U-Boot中指定DTB文件
setenv fdt_addr_r 0x44000000
setenv fdtfile vendor/board-v1.dtb
booti ${kernel_addr_r} - ${fdt_addr_r}

# 或在extlinux.conf中指。fdt /boot/dtbs/vendor/board-v1.dtb

7.5 动态调试

# 开启设备树解析调试日志
echo 'file drivers/of/*.c +p' > /sys/kernel/debug/dynamic_debug/control

# 查看已注册的platform_device
ls /sys/bus/platform/devices/

# 查看驱动匹配状态cat /sys/bus/platform/drivers/mydev-dt/bind

八、常见问题

Q1:驱动probe不被调用。

*最常见原因:compatible不匹配置 检查以下几点:

  1. DTS中的compatible和驱动of_match_table中的字符串是否完全一致(区分大小写)
  2. DTS节点是否在正确的总线节点下(需要被of_platform_populate遍历到)
  3. 节点status是否为"okay"("disabled"的节点不会创建device。4. 驱动模块是否已加载(lsmod | grep mydev。
# 快速排查:查看内核是否识别到设备树节点
ls /sys/firmware/devicetree/base/ | grep mydev

# 查看驱动是否注册
ls /sys/bus/platform/drivers/ | grep mydev

Q2:of_property_read返回EINVAL。

*原因:属性名或类型不对

  1. 属性名拼写错误——DTS里写的是clock-frequency,代码里写成了clock_freq,下划线和横线不要搞。2. 类型不匹配——DTS中reg = <0x1000>是整数,用of_property_read_string读会返回EINVAL
  2. 属性不存在——用of_find_property先检查属性是否存。
// 安全的读取方。if (of_property_read_u32(np, "clock-frequency", &freq)) {
    dev_warn(dev, "clock-frequency not found, using default\n");
    freq = 100000000;
}

Q3:DTB修改后不生效。

*原因:bootloader加载的还是旧DTB

  1. 只修改DTS文件不够,必须重新编译成DTB
  2. 编译后的DTB必须更新到bootloader能找到的位置(boot分区或固件分区)
  3. 有些平台需要更新extlinux.conf中的fdt路径
  4. U-Boot环境变量可能缓存了旧的fdt地址,需要重启或清除环境变量
# 正确流程
dtc -I dts -O dtb -o board.dtb board.dts
cp board.dtb /boot/dtbs/vendor/
sync
reboot

九、总结

设备树的本质就是一份硬件配置清单,它把"板子上有什么?。驱动怎么。拆开。写DTS描述硬件,写驱动读DTS,内核负责中间的匹配和传递。
核心工作流:编写DTS 。dtc编译 bootloader加载 。内核解析 。compatible匹配 。probe中用of_* API读配置。

API速查看

操作 API 备注
检查属性存 of_find_property(np, "name", NULL) 返回非NULL表示存在
32位整数 of_property_read_u32(np, "name", &val) 失败返回负数
读字符串 of_property_read_string(np, "name", &str) 失败返回负数
读整数数 of_property_read_u32_array(np, "name", arr, len) 需预知长度
统计数组元素 of_property_count_u32_elems(np, "name") 返回元素个数
获取GPIO devm_gpiod_get(dev, "name", flags) 推荐描述符方
解析phandle of_parse_phandle(np, "name", index) 用完需of_node_put
地址转resource of_address_to_resource(np, index, &res) 搭配devm_ioremap_resource
直接映射寄存 of_iomap(np, index) 需手动iounmap
读布尔属 of_property_read_bool(np, "name") 存在返回true

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页