当前位置: 首页 > news >正文

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

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

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

  1. 硬件修改仍需改动代码:更换按键 GPIO 引脚时,必须修改dev_platform.c,重新编译内核模块,无法做到「一次编译,适配多硬件」;
  2. 内核板级代码冗余:在设备树出现之前,ARM 架构 Linux 内核中存在大量针对不同开发板的板级描述代码,每一款开发板都要在内核中添加对应的硬件描述文件,导致内核源码冗余臃肿。

设备树(Device Tree)就是为了解决这些问题诞生的:它是一种专门描述硬件的数据结构,将硬件资源描述完全独立于内核代码之外,通过单独的.dts文件编写,编译成二进制.dtb文件后,由 bootloader 传递给内核。内核启动时解析 dtb 文件,自动生成对应的平台设备、I2C 设备、SPI 设备等,无需我们再手动编写任何设备端代码


二、设备树核心基础概念

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

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

核心工作流程

  1. 针对开发板编写 DTS 文件,引用芯片通用的 DTSI 文件,添加自定义硬件节点;
  2. 用 DTC 将 DTS 编译为 DTB 二进制文件;
  3. bootloader 启动内核时,将 DTB 加载到内存,把 DTB 的内存地址传递给内核;
  4. 内核启动时解析 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:节点名,必须以字母开头,只能包含数字、字母、下划线、破折号等字符,建议使用反映设备功能的通用名称(如keyledgpio);
  • @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 驱动开发核心属性

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

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

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

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

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

设备树匹配的完整工作流程

  1. 内核启动时解析设备树 DTB 文件,根据节点内容生成platform_device平台设备,注册到平台总线的设备链表;
  2. 我们加载驱动模块时,驱动注册到平台总线的驱动链表;
  3. 平台总线自动遍历设备链表,对比驱动of_match_table中的compatible和设备树节点的compatible
  4. 字符串完全一致则匹配成功,总线自动触发驱动的probe函数,执行硬件初始化;
  5. 驱动卸载时,总线自动触发remove函数释放资源。

核心优势:硬件信息完全写在设备树中,更换硬件只需要修改 DTS 文件、重新编译 DTB,无需修改驱动代码、无需重新编译驱动模块,真正实现了「驱动代码与硬件描述完全解耦」。


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

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

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

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

操作步骤
  1. 进入内核源码的设备树目录:
    cd linux-sdk/kernel/arch/arm64/boot/dts/rockchip/
  2. 编辑通用 dtsi 文件(NanoPC-T4 的通用配置文件):
    vim rk3399-nanopi4-common.dtsi
  3. 在根节点/下添加如下按键节点:

    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 };
  4. 保存退出,编译设备树,生成新的 DTB 文件:
    make ARCH=arm64 nanopi4-images -j8
  5. 将新的 DTB 文件烧录到开发板,重启开发板。
节点验证

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

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

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

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

  1. 添加设备树匹配表of_match_table:定义compatible匹配标识,和设备树节点一致;
  2. 移除硬编码 GPIO 获取:不再从platform_data获取 GPIO 号,改为通过of_系列 API 从设备树节点解析 GPIO;
  3. 完善设备树节点合法性校验:在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 测试步骤

  1. 确认设备树节点已生成

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

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

  2. 加载驱动模块

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

    dmesg | grep 按键事件
  4. 驱动卸载测试

    rmmod dri_platform.ko

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

6.3 核心对比

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

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

  1. compatible 属性不匹配

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

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

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

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

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

八、总结与拓展

核心总结

  1. 设备树的核心价值是彻底将硬件描述与驱动代码分离,解决了传统板级代码冗余、可移植性差的问题,是当前 Linux 驱动开发的主流标准;
  2. 平台总线的设备树匹配,核心是compatible属性的匹配,优先级高于传统的 name 匹配,匹配成功后内核自动触发probe函数;
  3. 驱动开发中,通过of_系列 API 可以从设备树节点中解析所有硬件信息,无需在驱动中硬编码任何硬件参数。
http://www.cnnetsun.cn/news/1625694.html

相关文章:

  • SonarQube 从零到生产:安装、部署与高效配置实战指南
  • 高效智能网页时光机:构建你的数字记忆档案
  • Qwen3.5-2B参数调优指南:Top-P=0.95时创意写作多样性与可控性平衡
  • 3步打造智能车载中枢:树莓派驱动的开源车载系统全攻略
  • 从Prompt到成稿|像素剧本圣殿输入剧情大纲→输出标准剧本全流程
  • M2LOrder模型Python爬虫实战:应对动态渲染与数据加密网站
  • Feishin:打造完美自托管音乐播放器的终极指南 [特殊字符]
  • FLUX.1-dev创意应用:5个场景实战,教你用AI生成营销素材
  • emu8086实战:3个经典运算实验带你玩转汇编指令(附完整代码)
  • 3分钟学会Real-CUGAN:让模糊动漫图片瞬间变清晰的终极神器
  • 计组实验手记:从字拓展到位拓展,构建你的存储器扩展实战指南
  • 技术解密:OpenCore Legacy Patcher如何突破Mac硬件限制
  • 懒人必备!一键生成论文大纲 + 正文,这几款 AI 软件让导师赞不绝口
  • HSTracker:macOS炉石传说智能追踪器的终极指南
  • 如何从iOS和Android获取短信记录?
  • 3分钟解决B站资源下载难题:BiliTools跨平台工具箱完全指南
  • Go HTTP 服务连接池优化策略
  • Qwen3.5-9B地球科学:地质图识别+矿产分布分析+勘探报告生成
  • 【TVM教程】面向机器学习模型的图抽象
  • 打造理想编码环境:Inconsolata字体全场景应用指南
  • intv_ai_mk11效果对比:相同提示词下不同Top P值输出差异分析
  • C++ Move 语义性能优化分析
  • PlugY:暗黑破坏神2单机模式的全方位增强工具
  • GitHub Token配置完全指南:从权限设置到云打包授权验证
  • 解锁PlotJuggler数据可视化:工业时序数据处理与分析指南
  • GLM-4.7-Flash效果展示:自动生成CSDN风格技术博客
  • 用AI写专著,精选工具推荐,快速且精准完成专著撰写
  • Zemax多组态操作避坑指南:如何避免优化失败和设置错误
  • MGeo中文地址解析模型入门指南:地址要素边界识别难点与MOMETAS多任务缓解策略
  • 5分钟掌握:如何用这款免费工具让Windows飞起来?