欢迎光临
我们一直在努力

初学Linux之设备树的使用| RK3399上实操

一、为什么需要设备树?—— 从平台总线的痛点说起

在前文的平台总线驱动开发中,我们通过手动编写dev_platform.c注册平台设备,实现了「硬件资源描述」和「驱动逻辑」的初步分离,但这种方式仍存在两个核心痛点:

  • 硬件修改仍需改动代码:更换按键 GPIO 引脚时,必须修改dev_platform.c,重新编译内核模块,无法做到「一次编译,适配多硬件」;
  • 内核板级代码冗余:在设备树出现之前,ARM 架构 Linux 内核中存在大量针对不同开发板的板级描述代码,每一款开发板都要在内核中添加对应的硬件描述文件,导致内核源码冗余臃肿。
  • 设备树(Device Tree) 就是为了解决这些问题诞生的:它是一种专门描述硬件的数据结构,将硬件资源描述完全独立于内核代码之外,通过单独的.dts文件编写,编译成二进制.dtb文件后,由 bootloader 传递给内核。内核启动时解析 dtb 文件,自动生成对应的平台设备、I2C 设备、SPI 设备等,无需我们再手动编写任何设备端代码。


    二、设备树核心基础概念

    先理清设备树生态中最核心的 6 个名词,对应文件的关系和作用如下表:

    缩写全称中文名称核心作用
    DT Device Tree 设备树 描述硬件数据结构的总称,树形结构组织硬件信息
    DTS Device Tree Source 设备树源文件 用 ASCII 码编写的硬件描述文件,对应一款开发板的硬件配置,存放于内核arch/xxx/boot/dts目录
    DTSI Device Tree Source Include 设备树头文件 类似 C 语言的.h头文件,可被多个 DTS 文件引用,通常存放同一款芯片的通用硬件配置
    DTC Device Tree Compiler 设备树编译器 内核自带的编译工具,将 DTS/DTSI 文件编译成二进制 DTB 文件
    DTB Device Tree Blob 设备树二进制文件 DTS 编译后的产物,bootloader 启动内核时,会将 DTB 加载到内存并把地址传递给内核,内核解析 DTB 生成硬件设备
    OF Open Firmware 开放固件 设备树的起源标准,内核中所有设备树相关的 API 都以of_前缀命名

    核心工作流程

  • 针对开发板编写 DTS 文件,引用芯片通用的 DTSI 文件,添加自定义硬件节点;
  • 用 DTC 将 DTS 编译为 DTB 二进制文件;
  • bootloader 启动内核时,将 DTB 加载到内存,把 DTB 的内存地址传递给内核;
  • 内核启动时解析 DTB 文件,根据节点内容自动生成对应的硬件设备,注册到对应总线(平台总线、I2C 总线等)。

  • 三、设备树语法规范与节点编写

    设备树是一个树形结构,由根节点、子节点、属性三部分组成,和电脑的文件夹结构完全一致:根节点是/,子节点是文件夹,属性是文件夹里的文件。

    3.1 设备树的基本结构

    一个最简的设备树文件结构如下:

    dts

    /dts-v1/; // 设备树版本,必须指定为v1,否则编译器视为过时的v0版本

    / { // 根节点,所有设备树文件有且只有一个根节点,DTSI的根节点编译时会和主DTS合并
    // 根节点属性:描述开发板和芯片信息
    model = "NanoPC-T4 RK3399 Development Board";
    compatible = "rockchip,nanopi4", "rockchip,rk3399";

    // 子节点1:自定义按键节点
    my_key: key_test {
    // 节点属性
    compatible = "my,key_test";
    status = "okay";
    gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
    debounce-interval = <20>;
    };

    // 子节点2:其他硬件节点(如GPIO、I2C、SPI控制器等)
    uart0: serial@ff180000 {
    compatible = "rockchip,rk3399-uart";
    reg = <0x0 0xff180000 0x0 0x100>;
    status = "okay";
    };
    };

    3.2 节点的编写规范

    节点的通用编写格式如下:

    dts

    [label:] node-name[@unit-address] {
    [properties definitions] // 属性定义
    [child nodes] // 子节点
    };

    各部分说明:

    • label::标签,可选,用于在其他位置通过&label引用该节点,修改节点内容;
    • node-name:节点名,必须以字母开头,只能包含数字、字母、下划线、破折号等字符,建议使用反映设备功能的通用名称(如key、led、gpio);
    • @unit-address:设备地址,可选,用于描述外设的寄存器起始地址,无地址的外设(如按键)可省略。

    3.3 常用属性类型

    属性是设备树描述硬件信息的核心,由「属性名 = 属性值」组成,内核中最常用的属性类型如下

    属性类型格式说明示例适用场景
    字符串类型 用双引号""包裹 compatible = "my,key_test"; 设备匹配标识、型号描述
    字符串列表 多个字符串用逗号分隔 compatible = "rockchip,nanopi4", "rockchip,rk3399"; 多兼容匹配标识
    32 位无符号整数 用尖括号<>包裹 debounce-interval = <20>; 引脚编号、延时时间、寄存器地址
    32 位整数数组 尖括号内多个数字用空格分隔 reg = <0x0 0xff180000 0x0 0x100>; 寄存器地址范围、多组数值
    二进制类型 用方括号[]包裹 mac-address = [00 11 22 33 44 55]; MAC 地址、二进制数据
    空属性 只有属性名,无属性值 gpio-key,wakeup; 布尔型功能开关,存在即表示开启

    3.4 驱动开发核心属性

    在平台总线驱动开发中,有两个属性是核心中的核心:

  • compatible属性:设备与驱动匹配的唯一标识,格式为厂商,设备名,必须和驱动中of_match_table里的字符串完全一致,否则无法匹配;
  • status属性:设备的使能状态,status = "okay"表示使能该设备,status = "disabled"表示禁用该设备。

  • 四、平台总线的设备树匹配规则(核心)

    在前文中我们讲过,平台总线的匹配逻辑在内核platform_match函数中实现,优先级从高到低共 4 种,设备树匹配的优先级高于手动 name 匹配,完整匹配顺序如下:

  • 驱动强制匹配:通过设备的driver_override字段强制匹配;
  • 设备树匹配(本文使用):通过驱动的of_match_table中的compatible属性,和设备树节点的compatible属性匹配,一致则匹配成功;
  • ID 表匹配:通过驱动的id_table数组匹配设备名称;
  • 名称匹配:对比设备的name和驱动的driver.name字符串。
  • 设备树匹配的完整工作流程

  • 内核启动时解析设备树 DTB 文件,根据节点内容生成platform_device平台设备,注册到平台总线的设备链表;
  • 我们加载驱动模块时,驱动注册到平台总线的驱动链表;
  • 平台总线自动遍历设备链表,对比驱动of_match_table中的compatible和设备树节点的compatible;
  • 字符串完全一致则匹配成功,总线自动触发驱动的probe函数,执行硬件初始化;
  • 驱动卸载时,总线自动触发remove函数释放资源。
  • 核心优势:硬件信息完全写在设备树中,更换硬件只需要修改 DTS 文件、重新编译 DTB,无需修改驱动代码、无需重新编译驱动模块,真正实现了「驱动代码与硬件描述完全解耦」。


    五、实战:将按键驱动改造为设备树匹配方式

    我们基于前文的平台总线按键驱动,完成设备树适配改造,改造后无需再编写和加载dev_platform.c设备端模块,所有硬件信息由设备树提供。

    5.1 步骤 1:编写按键设备树节点

    我们使用的是 NanoPC-T4 开发板(RK3399 芯片),按键使用 GPIO0_B5(对应 GPIO 编号 5),按下为低电平。

    操作步骤
  • 进入内核源码的设备树目录:

    cd linux-sdk/kernel/arch/arm64/boot/dts/rockchip/

  • 编辑通用 dtsi 文件(NanoPC-T4 的通用配置文件):

    vim rk3399-nanopi4-common.dtsi

  • 在根节点/下添加如下按键节点:

    dts

    /* 自定义按键设备树节点 */
    my_key: key_test {
    compatible = "my,key_test"; // 【匹配核心】必须和驱动中的compatible完全一致
    status = "okay"; // 使能该设备
    gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; // 按键GPIO:GPIO0_5,低电平有效
    debounce-interval = <20>; // 消抖时间20ms
    };

  • 保存退出,编译设备树,生成新的 DTB 文件:

    make ARCH=arm64 nanopi4-images -j8

  • 将新的 DTB 文件烧录到开发板,重启开发板。
  • 节点验证

    开发板重启后,执行以下命令,验证设备树节点是否正常生成:

    # 查看平台总线设备,能看到key_test节点,说明设备树解析成功
    ls /sys/bus/platform/devices/ | grep key_test
    # 查看设备树节点属性
    cat /sys/firmware/devicetree/base/key_test/compatible

    5.2 步骤 2:驱动代码核心改造点

    对比前文的手动匹配驱动,设备树适配的驱动有 3 个核心修改点:

  • 添加设备树匹配表of_match_table:定义compatible匹配标识,和设备树节点一致;
  • 移除硬编码 GPIO 获取:不再从platform_data获取 GPIO 号,改为通过of_系列 API 从设备树节点解析 GPIO;
  • 完善设备树节点合法性校验:在probe函数中校验设备树节点是否存在,属性是否合法。
  • 同时需要新增两个头文件:

    #include <linux/of.h> // 设备树核心API头文件
    #include <linux/of_gpio.h> // 设备树GPIO解析专用API头文件

    5.3 步骤 3:设备树适配版完整驱动代码

    #include <linux/module.h> // 模块编程核心头文件
    #include <linux/init.h>// 模块初始化/卸载头文件
    #include <linux/platform_device.h>// 平台总线驱动头文件
    #include <linux/fs.h>// 文件操作头文件
    #include <linux/miscdevice.h>// 杂项设备头文件
    #include <linux/gpio.h>// GPIO操作库头文件
    #include <linux/uaccess.h>// 内核/应用层数据交换头文件
    #include <linux/interrupt.h> // 中断处理头文件
    #include <linux/workqueue.h> // 工作队列(中断下半部)头文件
    #include <linux/delay.h> // 内核延时头文件
    #include <linux/atomic.h> // 原子操作头文件(并发安全)
    #include <linux/of.h> // 【设备树新增】设备树核心API头文件
    #include <linux/of_gpio.h> // 【设备树新增】设备树GPIO解析API头文件

    #define KEY_NAME "key_test" // 设备名称,用于GPIO申请和中断注册

    /* 全局变量优化:用原子变量替代普通int,解决中断下半部的并发安全问题 */
    atomic_t flag = ATOMIC_INIT(0); // 按键状态标记:0=松开,1=按下(原子初始化)

    /* 硬件相关全局变量(仅在驱动匹配成功后赋值,避免未初始化访问) */
    int key_gpio = 0; // 按键GPIO号(从设备树节点解析获取)
    int irq = 0; // 按键对应的中断号(通过GPIO号映射获取)
    int debounce_interval = 20; // 消抖时间(从设备树节点解析获取)

    /* 工作队列结构体:用于中断下半部处理消抖、电平判断等耗时操作 */
    struct work_struct key_work;

    /*
    * 中断服务函数(中断上半部:快进快出,禁止耗时操作)
    * 触发条件:按键按下/松开时,GPIO电平变化触发硬件中断
    */
    irqreturn_t key_irq_handler(int irq, void *arg)
    {
    /* 上半部只做最紧急的事:调度工作队列到下半部 */
    schedule_work(&key_work);
    return IRQ_HANDLED; // 告诉内核:中断已成功处理
    }

    /*
    * 工作队列处理函数(中断下半部:可安全执行延时、打印等耗时操作)
    * 触发条件:上半部调度后,内核在进程上下文中自动调用
    */
    void key_work_func(struct work_struct *workp)
    {
    u32 curr_level;

    /* 1. 临时关闭中断,防止按键抖动重复触发 */
    disable_irq(irq);

    /* 2. 读取当前GPIO电平,延时消抖,再次读取确认状态稳定 */
    curr_level = gpio_get_value(key_gpio);
    msleep(debounce_interval); // 使用从设备树获取的消抖时间

    if (curr_level == gpio_get_value(key_gpio)) {
    /* 3. 用原子操作读写flag,防止并发竞态 */
    if (curr_level && atomic_read(&flag)) {
    atomic_set(&flag, 0); // 原子设置:标记为松开
    pr_info("【按键事件】按键松开!!!\\n");
    } else if (curr_level == 0 && !atomic_read(&flag)) {
    atomic_set(&flag, 1); // 原子设置:标记为按下
    pr_info("【按键事件】按键按下!!!\\n");
    }
    }

    /* 4. 重新打开中断,等待下一次按键触发 */
    enable_irq(irq);
    }

    /*
    * 平台驱动的probe函数(匹配成功后自动执行)
    * 触发条件:平台总线设备树compatible匹配成功
    * 核心改动:从设备树节点解析硬件资源,替代之前的platform_data
    */
    int xxx_probe(struct platform_device *pdev)
    {
    struct device_node *np = pdev->dev.of_node; // 获取设备树节点
    int err;

    pr_info("【平台驱动】设备树匹配成功,开始执行probe初始化\\n");

    /* 【设备树校验】检查设备树节点是否存在 */
    if (!np) {
    pr_err("【平台驱动】错误:无对应的设备树节点\\n");
    return -EINVAL;
    }

    /* 【设备树核心】从设备树节点解析GPIO号 */
    key_gpio = of_get_named_gpio(np, "gpios", 0);
    if (!gpio_is_valid(key_gpio)) { // 校验GPIO号是否合法
    pr_err("【平台驱动】错误:从设备树获取GPIO失败\\n");
    return -EINVAL;
    }
    pr_info("【平台驱动】从设备树获取到按键GPIO:%d\\n", key_gpio);

    /* 【可选】从设备树解析消抖时间,无该属性则使用默认值20ms */
    of_property_read_u32(np, "debounce-interval", &debounce_interval);
    pr_info("【平台驱动】按键消抖时间:%dms\\n", debounce_interval);

    /* 【硬件初始化1】申请GPIO使用权 */
    err = gpio_request(key_gpio, KEY_NAME);
    if (err) {
    pr_err("【平台驱动】GPIO申请失败,错误码:%d\\n", err);
    return err;
    }

    /* 【硬件初始化2】设置GPIO为输入模式 */
    err = gpio_direction_input(key_gpio);
    if (err) {
    pr_err("【平台驱动】GPIO输入模式设置失败,错误码:%d\\n", err);
    goto err_free_gpio;
    }

    /* 【硬件初始化3】将GPIO号映射为中断号 */
    irq = gpio_to_irq(key_gpio);
    if (irq < 0) {
    err = irq;
    pr_err("【平台驱动】GPIO转中断号失败,错误码:%d\\n", err);
    goto err_free_gpio;
    }
    pr_info("【平台驱动】GPIO映射到中断号:%d\\n", irq);

    /* 【硬件初始化4】注册中断服务函数(上升沿+下降沿双触发) */
    err = request_irq(irq, key_irq_handler,
    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
    KEY_NAME, NULL);
    if (err) {
    pr_err("【平台驱动】中断注册失败,错误码:%d\\n", err);
    goto err_free_gpio;
    }

    pr_info("【平台驱动】probe初始化全部完成,设备已就绪\\n");
    return 0; // 成功返回0

    /* 错误处理:资源逆序释放 */
    err_free_gpio:
    gpio_free(key_gpio);
    return err;
    }

    /*
    * 平台驱动的remove函数(驱动注销时自动执行)
    * 作用:释放probe中申请的所有硬件资源
    */
    int xxx_remove(struct platform_device *pdev)
    {
    pr_info("【平台驱动】开始执行remove,释放硬件资源\\n");

    /* 1. 释放中断服务函数 */
    free_irq(irq, NULL);
    /* 2. 释放GPIO使用权 */
    gpio_free(key_gpio);

    pr_info("【平台驱动】remove执行完成,资源已释放\\n");
    return 0;
    }

    /*
    * 【设备树核心】设备树匹配表
    * 作用:定义驱动支持的设备树compatible标识,和设备树节点一致则匹配成功
    */
    const struct of_device_id key_of_match_table[] = {
    { .compatible = "my,key_test", }, // 必须和设备树节点的compatible完全一致
    { /* 末尾必须加空元素,作为结束标记 */ },
    };
    // 声明设备树匹配表,让内核可以识别
    MODULE_DEVICE_TABLE(of, key_of_match_table);

    /*
    * 平台驱动核心结构体
    * 核心改动:添加of_match_table,绑定设备树匹配表
    */
    struct platform_driver xxx_dri = {
    .probe = xxx_probe, // 绑定匹配成功后的初始化回调
    .remove = xxx_remove, // 绑定驱动注销后的资源释放回调
    .driver = {
    .name = "key_test", // 备用名称匹配,设备树匹配失败时使用
    .of_match_table = key_of_match_table, // 【设备树核心】绑定匹配表
    },
    };

    /*
    * 模块安装入口函数(insmod dri_platform.ko时自动执行)
    */
    static int __init xxx_init(void)
    {
    int err;

    pr_info("【平台驱动】开始安装驱动模块\\n");

    /* 1. 初始化工作队列 */
    INIT_WORK(&key_work, key_work_func);

    /* 2. 注册平台驱动 */
    err = platform_driver_register(&xxx_dri);
    if (err) {
    pr_err("【平台驱动】驱动注册失败,错误码:%d\\n", err);
    return err;
    }

    pr_info("【平台驱动】驱动模块安装成功\\n");
    return 0;
    }

    /*
    * 模块卸载入口函数(rmmod dri_platform.ko时自动执行)
    */
    static void __exit xxx_exit(void)
    {
    pr_info("【平台驱动】开始卸载驱动模块\\n");

    /* 1. 注销平台驱动 */
    platform_driver_unregister(&xxx_dri);

    /* 2. 等待工作队列中的任务完全执行完毕 */
    flush_work(&key_work);

    pr_info("【平台驱动】驱动模块卸载成功\\n");
    }

    /* 绑定模块的安装/卸载入口函数 */
    module_init(xxx_init);
    module_exit(xxx_exit);

    /* 模块声明(内核强制要求) */
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("嵌入式驱动开发");
    MODULE_DESCRIPTION("设备树匹配版-平台总线按键驱动");
    MODULE_VERSION("v2.0-设备树适配版");


    六、编译、烧录与测试验证

    6.1 驱动模块编译

    使用和前文一致的 Makefile,编译驱动模块,生成dri_platform.ko文件,传到开发板。

    6.2 测试步骤

  • 确认设备树节点已生成:

    ls /sys/bus/platform/devices/ | grep key_test

    能看到key_test设备,说明内核已成功解析设备树,自动生成了平台设备。

  • 加载驱动模块:

    insmod dri_platform.ko

  • 按键功能测试:按下 / 松开按键,查看内核日志,能看到按键按下 / 松开的打印,说明驱动工作正常。

    dmesg | grep 按键事件

  • 驱动卸载测试:

    rmmod dri_platform.ko

    查看日志,确认资源正常释放,无报错。

  • 6.3 核心对比

    对比项手动注册平台设备设备树匹配方式
    设备端代码 需要手动编写dev_platform.c 无需编写任何设备端代码
    硬件修改 需修改代码、重新编译模块 只需修改设备树、重新编译 DTB
    匹配方式 设备与驱动 name 字符串匹配 设备树 compatible 属性匹配(优先级更高)
    可移植性 差,仅适配当前硬件 强,同一驱动可适配多块开发板

    七、设备树开发常见避坑指南

  • compatible 属性不匹配

    • 坑点:设备树节点的compatible和驱动of_match_table中的字符串不一致,包括大小写、空格、逗号分隔符错误,导致匹配失败,probe函数不执行;
    • 修复:必须保证两个字符串完全一致,建议使用厂商,设备名的格式,避免拼写错误。
  • GPIO 控制器引用错误

    • 坑点:设备树中gpios属性引用的 GPIO 控制器错误(如 RK3399 写成&gpio1而不是&gpio0),导致 GPIO 解析失败;
    • 修复:对照芯片手册,确认 GPIO 所属的控制器和引脚编号,使用of_get_named_gpio后必须用gpio_is_valid校验合法性。
  • 设备树节点未使能

    • 坑点:设备树节点中忘记写status = "okay",或写成status = "disabled",导致内核不生成对应的设备;
    • 修复:自定义节点必须添加status = "okay"使能设备。
  • 设备树编译后未烧录生效

    • 坑点:修改 DTS 文件后,只编译了内核,没有更新 DTB 文件,或烧录后未重启开发板,新节点不生效;
    • 修复:修改 DTS 后必须重新编译 DTB,烧录到开发板并重启,通过/sys/firmware/devicetree/base/验证节点是否生效。
  • 设备树 API 使用错误

    • 坑点:使用of_property_read_u32直接读取 GPIO 号,没有使用专用的of_get_named_gpio,导致 GPIO 编号映射错误;
    • 修复:GPIO 解析必须使用of_get_named_gpio函数,该函数会自动处理 GPIO 控制器的映射和极性。

  • 八、总结与拓展

    核心总结

  • 设备树的核心价值是彻底将硬件描述与驱动代码分离,解决了传统板级代码冗余、可移植性差的问题,是当前 Linux 驱动开发的主流标准;
  • 平台总线的设备树匹配,核心是compatible属性的匹配,优先级高于传统的 name 匹配,匹配成功后内核自动触发probe函数;
  • 驱动开发中,通过of_系列 API 可以从设备树节点中解析所有硬件信息,无需在驱动中硬编码任何硬件参数。
  • 赞(0)
    未经允许不得转载:171主机测评 » 初学Linux之设备树的使用| RK3399上实操
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址