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

Day64 设备树与GPIO子系统驱动开发实践

day64 设备树与GPIO子系统驱动开发实践

本日学习内容聚焦于Linux内核驱动开发的核心范式:设备树(Device Tree)与GPIO子系统。通过从硬编码资源到动态获取、从手动寄存器操作到标准API调用的演进,我们掌握了如何编写可移植、易维护的现代驱动程序。


一、设备树(Device Tree)基础概念

设备树是Linux内核为了实现“驱动方法统一,资源描述分离”而引入的关键机制。它将硬件平台相关的资源配置信息从内核代码中剥离,独立为外部文件,从而实现了“一次编译,多平台适配”。

1. 传统方式 vs 设备树方式

传统方式设备树方式
驱动与资源耦合驱动与资源分离
内核臃肿,平台依赖强内核精简,平台灵活
修改需重编译内核仅需更新 .dtb
维护成本高可复用、易扩展

💡 核心思想:“让驱动专注逻辑,让设备树专注描述”。

2. 设备树文件类型

文件类型后缀说明
源文件.dts设备树源码,人类可读,包含硬件描述。
头文件.dtsi可复用的公共部分(如处理器通用模块),类似C语言头文件。
二进制文件.dtb编译后的二进制文件,内核启动时加载。

📌 类比
内核 = 手机系统;
.dtb = 说明书(只含当前设备信息);
旧方式 = 系统内置所有品牌/型号说明书 → 膨胀且冗余。

3. DTS 与 DTSI 的分工

文件类型功能定位示例
.dts描述具体开发板的硬件资源(如LED、按键、SPI设备)。imx6ul-14x14-evk.dts
.dtsi描述处理器平台的通用硬件模块(如CPU、I²C、UART控制器)。imx6ull.dtsi

设计原则

  • 开发板 .dts 包含平台 .dtsi → 复用通用模块。
  • 不同开发板共用同一 .dtsi → 减少重复代码。

4. 设备树结构与语法

一个设备树节点的基本结构如下:

节点名字 {属性 = 值;子节点名 {属性 = 值;};
};
关键特性
  • 节点嵌套:支持层级结构,描述设备层次关系。
  • 节点合并:同名节点属性自动合并。
  • 属性覆盖:后定义属性覆盖先定义属性。
  • 节点引用:通过 &label 引用其他节点,避免重复描述。
属性值类型
  • 字符串"value"(双引号包裹)
  • 数字<0x123><10>
  • 数组<0x1 0x2 0x3>
  • 布尔值:空属性(如 status = "okay";

二、设备树驱动开发流程

1. 设备树层:定义硬件资源

在你的开发板 .dts 文件中(如 arch/arm/boot/dts/pt.dts),添加一个新节点来描述你的设备。

示例1:使用 reg 属性描述寄存器资源 (led_dts.c)
myled {#address-cells = <1>; // 定义地址字段长度#size-cells = <1>;    // 定义大小字段长度compatible = "myled"; // 驱动匹配标识reg = <0x20e0068 4    // GPIO1_SW_MUX_CTL 寄存器地址和大小0x20e02f4 4    // GPIO1_SW_PAD_CTL 寄存器地址和大小0x209c004 4    // GPIO1_DIR 寄存器地址和大小0x209c000 4>;  // GPIO1_DATA 寄存器地址和大小status = "okay";      // 启用该设备节点
};

此节点描述了LED控制所需的四个寄存器的物理地址和映射大小。

示例2:使用 gpios 属性描述GPIO引脚 (led1_subgpio.c, key1_dts.c)
// LED设备节点
myled_subgpio {#address-cells = <1>;#size-cells = <1>;compatible = "myled_subgpio";pinctrl-0 = <&pinctrl_myled>; // 引用引脚配置节点gpio-led = <&gpio1 3 1>;     // 指定GPIO控制器、引脚号、电平极性status = "okay";
};// 按键设备节点
mykey {#address-cells = <1>;#size-cells = <1>;compatible = "pt,mykey";pinctrl-0 = <&pinctrl_mykey>; // 引用引脚配置节点gpio-key = <&gpio1 18 1>;    // 指定GPIO控制器、引脚号、电平极性status = "okay";
};
示例3:定义引脚配置 (pinctrl)

&iomuxc_snvs&iomuxc 节点下,定义具体的引脚功能和电气属性。

pinctrl_myled: myledgrp {fsl,pins = <MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0>; // 将GPIO1_IO03配置为GPIO功能,电气属性为0x10B0
};pinctrl_mykey: mykeygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x10B0>; // 将GPIO1_IO18配置为GPIO功能,电气属性为0x10B0
};

2. 编译设备树生成 .dtb

在内核源码根目录下执行以下命令:

方法一:编译所有设备树
make dtbs
方法二:只编译指定设备树
make pt.dtb

🔍 注意

  • 编译前确保你的 .dts 已在 arch/arm/boot/dts/Makefile 中被包含。
  • Makefile 中找到对应平台的行(如 dtb-$(CONFIG_SOC_IMX6ULL)),添加你的设备树文件名。
  • 编译成功后,生成的 .dtb 文件位于 arch/arm/boot/dts/ 目录下。
# 示例:编辑 Makefile
vim arch/arm/boot/dts/Makefile
# 在对应行添加
dtb-$(CONFIG_SOC_IMX6ULL) += \... \pt.dtb

3. 驱动层:从设备树读取资源并初始化硬件

步骤1:引入设备树相关头文件
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h> // 如果使用GPIO子系统
步骤2:在驱动probe函数中获取设备树资源
方式A:读取寄存器地址 (reg 属性) - led_dts.c
static int __init led_init(void)
{struct device_node * node;const char * str_compatible = NULL;unsigned int reg_array[8] = {0}; // 用于存储reg属性的值int ret;// 1. 注册misc设备ret = misc_register(&miscdev);if(ret < 0)goto err_misc_register;// 2. 根据路径查找设备树节点node = of_find_node_by_path("/myled");if(NULL == node){ret = PTR_ERR(node); // 获取错误码goto err_dts;}printk("find node\n"); // 输出日志// 3. 读取compatible属性ret = of_property_read_string(node, "compatible", &str_compatible);if(ret < 0)goto err_dts;printk("compatible = %s\n", str_compatible);// 4. 读取reg属性,将其解析为u32数组ret = of_property_read_u32_array(node, "reg", reg_array, 8); // 8个元素,4对地址+大小if(ret < 0)goto err_dts;// 5. 打印读取到的寄存器地址和大小printk("0x%x  %x \n", reg_array[0], reg_array[1]);printk("0x%x  %x \n", reg_array[2], reg_array[3]);printk("0x%x  %x \n", reg_array[4], reg_array[5]);printk("0x%x  %x \n", reg_array[6], reg_array[7]);// 6. 使用ioremap将物理地址映射到虚拟地址空间led_iomuxc = ioremap(reg_array[0], reg_array[1]); // 映射第一个寄存器led_pad_ctl = ioremap(reg_array[2], reg_array[3]); // 映射第二个寄存器led_gdir = ioremap(reg_array[4], reg_array[5]);     // 映射第三个寄存器led_dr = ioremap(reg_array[6], reg_array[7]);       // 映射第四个寄存器printk("##################   led_misc_init!\n");return 0;err_dts:
err_misc_register:misc_deregister(&miscdev);printk("############### led_misc_register failed\n");return ret;
}

讲解

  • 此代码首先注册了一个misc设备,然后通过 of_find_node_by_path 查找 /myled 节点。
  • 接着读取 compatiblereg 属性,并打印出来进行验证。
  • 最后,使用 ioremap 函数将设备树中提供的物理寄存器地址映射到内核的虚拟地址空间,以便后续驱动可以通过指针直接访问这些寄存器。
方式B:读取GPIO引脚 (gpios 属性) - led1_subgpio.c, key1_dts.c
static int __init led_init(void)
{struct device_node * node;const char * str_compatible = NULL;int ret;// 1. 注册misc设备ret = misc_register(&miscdev);if(ret < 0)goto err_misc_register;// 2. 根据路径查找设备树节点node = of_find_node_by_path("/myled_subgpio");if(NULL == node){ret = PTR_ERR(node);goto err_dts;}printk("find node\n");// 3. 读取compatible属性ret = of_property_read_string(node, "compatible", &str_compatible);if(ret < 0)goto err_dts;printk("compatible = %s\n", str_compatible);// 4. 从设备树中获取GPIO编号gpio_led = of_get_named_gpio(node, "gpio-led", 0); // 从"gpio-led"属性获取第一个GPIOif(gpio_led < 0){ret = gpio_led; // 返回错误码goto err_dts;}// 5. 请求并配置GPIO为输出模式gpio_request(gpio_led, "gpioled"); // 请求GPIO,提供标签gpio_direction_output(gpio_led, 1); // 设置为输出,并初始电平为高printk("##################   led_misc_init!\n");return 0;err_dts:
err_misc_register:misc_deregister(&miscdev);return ret;
}

讲解

  • 此代码同样先注册misc设备并查找节点。
  • 核心在于 of_get_named_gpio 函数,它根据节点和属性名(如 "gpio-led")以及索引(0)来获取对应的GPIO编号。
  • 之后调用 gpio_requestgpio_direction_output 来申请并初始化这个GPIO,使其可以作为输出引脚使用。
方式C:结合Platform总线模型 - led1_dts_platfrom.c
static int probe(struct platform_device * pdev)
{struct device_node * node;int ret;// 1. 注册misc设备ret = misc_register(&misc);if(ret < 0)goto err_misc_register;// 2. 查找设备树节点node = of_find_node_by_path("/myled_subgpio");if(NULL == node){ret = PTR_ERR(node);goto err_dts;}// 3. 获取GPIO编号gpio_led = of_get_named_gpio(node, "gpio-led", 0);if(gpio_led < 0){ret = gpio_led;goto err_dts;}// 4. 请求并配置GPIOgpio_request(gpio_led, "gpioled");gpio_direction_output(gpio_led, 1);printk("led1 probe  ############################ ***\n");return 0;err_dts:
err_misc_register:misc_deregister(&misc);return ret;
}// 定义platform驱动结构体
static const struct of_device_id led_table[] = {{.compatible = "myled_subgpio"}, // 必须与设备树中的compatible一致{}
};static struct platform_driver drv = {.probe = probe,.remove = remove,.driver = {.name = DEV_NAME,.of_match_table = led_table // 指定匹配表}
};// 驱动初始化函数
static int __init led1_driver_init(void)
{int ret = platform_driver_register(&drv); // 注册platform驱动if(ret < 0)goto err_driver_register;printk("led1 platform_driver_register ...\n");return 0;err_driver_register:platform_driver_unregister(&drv);return ret;
}

讲解

  • 这种方式更符合现代Linux驱动的规范。驱动不再直接初始化,而是由platform总线框架在设备匹配成功后调用 probe 函数。
  • of_match_table 是关键,它定义了驱动能匹配哪些设备树节点。内核会自动将设备树节点与驱动的匹配表进行比较,如果 compatible 字段匹配,则调用 probe 函数。
  • 这样做的好处是解耦了设备发现和驱动初始化的过程,使得驱动代码更加模块化和标准化。

4. 应用层:控制硬件

示例:LED应用 (led1_app.c)
int main(int argc, const char *argv[])
{int fd = open("/dev/led", O_RDWR); // 打开LED设备文件if(fd < 0){perror("open led ");return -1;}// 控制LED亮灭write(fd, "ledon", 5);  // 发送"ledon"命令sleep(1);               // 等待1秒write(fd, "ledoff", 6); // 发送"ledoff"命令close(fd);return 0;
}
示例:按键应用 (key1_app.c)
int main(int argc, const char *argv[])
{int fd_key = open("/dev/key", O_RDWR); // 打开按键设备文件if(fd_key < 0){perror("open key1 ");return -1;}int fd_led = open("/dev/led", O_RDWR); // 打开LED设备文件if(fd_led < 0){perror("open led1 ");return -1;}unsigned char status = 0;while(1){read(fd_key, &status, 1); // 读取按键状态printf("status = %d\n", status);if(status == 0) // 按键按下write(fd_led, "ledon", 5); // 点亮LEDelse if(status == 1) // 按键松开write(fd_led, "ledoff", 6); // 熄灭LED}close(fd_key);close(fd_led);return 0;
}

三、常见问题与调试技巧

Q1:of_find_node_by_path 返回 NULL?

  • 原因:设备树节点路径错误或未编译进 .dtb
  • 解决
    • 检查 .dts 中节点名是否拼写正确。
    • 确保 .dts 已加入 Makefile 并重新编译 dtbs
    • 在内核启动日志中搜索 myled,确认节点是否存在。

Q2:of_property_read_u32_array 返回错误?

  • 原因:属性名错误、数据类型不匹配、数组长度不足。
  • 解决
    • 检查 .dtsreg 属性是否为 <addr size> 对形式。
    • 确保 count 参数与实际数据量匹配(如8个u32对应4对地址+大小)。
    • 使用 of_property_count_u32_elems(np, "reg") 先获取元素数量。

Q3:of_get_named_gpio 返回无效值?

  • 原因:设备树中 gpios 属性未正确定义或拼写错误。
  • 解决
    • 检查 gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; 格式是否正确。
    • 确认 &gpio1 是否在设备树中存在(通常在 .dtsi 中定义)。
    • 使用 dmesg 查看内核启动日志,确认设备树节点是否被解析。

Q4:驱动加载失败?

  • 调试建议
    • 在驱动中增加 pr_info 输出关键步骤。
    • 查看 dmesg 或串口输出,定位错误位置。
    • 确保 .dtb 已正确烧录到开发板并被内核加载。

四、总结与最佳实践

1. 主流驱动开发模式对比

开发模式特点适用场景
传统platform驱动手动匹配设备名,硬编码资源老版本内核,无设备树
设备树 + platform驱动通过 compatible 匹配,资源动态获取主流开发模式,推荐使用
GPIO子系统标准API操作引脚,无需关心寄存器所有GPIO设备,简化开发

最佳实践

  • 设备树层:描述硬件资源,定义 compatiblegpios
  • 驱动层:使用 platform_driver + of_match_table 匹配设备,调用 gpio_*gpiod_* API 操作引脚。
  • 调试:通过 dmesggpioinfo 验证驱动加载和引脚状态。

2. 学习路径

“先模仿 → 再理解 → 后创新”

  • 第一遍:复制现有代码,确保能运行。
  • 第二遍:删除注释,尝试自己写出关键部分。
  • 第三遍:修改功能(如增加多个按键、支持中断)。
  • 第四遍:独立完成一个完整项目(如“按键控制LED+蜂鸣器”)。

3. 分层设计思想

层级职责
硬件层提供物理按键和LED
驱动层提供标准接口(如 read, write),屏蔽硬件细节
应用层实现业务逻辑(如按键控制灯)

优势

  • 驱动可复用:同一驱动可用于不同应用。
  • 应用灵活:可根据需求修改逻辑,无需改动驱动。

掌握设备树与GPIO子系统,是Linux驱动开发的核心技能,标志着你已迈入“标准驱动开发”的大门。

http://www.dtcms.com/a/490917.html

相关文章:

  • 贝莱德终止收购云交所,YUNC暴跌风波
  • 源码网站php重庆观音桥网站建设
  • SWE-QA:语言模型能否回答仓库级代码问题?
  • 建功能网站有没有专业做效果图的网站
  • 做网站 教程做国外网站推广
  • k8s容器java应用频繁重启问题排查 OOM方向
  • 宁夏建设工程造价网站做pc端网站新闻
  • Spring Boot + Filebeat + ELK日志在线查看
  • 使用高性能流式的库SpreadCheetah创建EXCEL文件
  • 【西瓜播放器+Vue】前端实现网页短视频:上下滑动、自动播放、显示视频信息等
  • 软件下载网站模版html软件下载手机版
  • 哪些平台可以免费推广广州百度提升优化
  • Redis-缓存问题(穿透、击穿、雪崩)
  • Mysql数据库系统库数据恢复
  • 服务器数据恢复—RAID5硬盘掉线,热备盘未启用如何恢复raid5阵列数据?
  • 在 Linux 服务器上配置 SFTP 的完整指南(2025 最新安全实践)
  • pytorch 数据加载加速
  • 网站建设平台设备荣耀手机官网
  • 调用apisix admin 接口创建资源
  • 迅为RK3568开发板OpenHarmony系统南向驱动开发手册-pdf配置 rk3568_uart_config.hcs
  • 中兴通讯的网站建设分析wordpress安装后要删除哪些文件
  • 建设银行对账单查询网站简述电子商务网站开发的主要步骤
  • ARMA模型
  • 智慧园区:引领城市未来发展新趋势
  • python命名约定 私有变量 保护变量 公共变量
  • 气泡图 vs 散点图:什么时候加第三维?
  • 西安网站开发工程师wordpress+中文版
  • 网页设计网站源代码淘宝网站的建设目的
  • 分布式系统的幂等性设计:从理论到生产实践
  • Advanced Port Scanner,极速端口扫描利器