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. 设备树结构与语法

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

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

二、设备树驱动开发流程

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

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

示例1:使用 reg 属性描述寄存器资源 (led_dts.c)
dts 复制代码
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)
dts 复制代码
// 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 节点下,定义具体的引脚功能和电气属性。

dts 复制代码
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

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

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

🔍 注意

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

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

步骤1:引入设备树相关头文件
c 复制代码
#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
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
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"属性获取第一个GPIO
    if(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
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. 请求并配置GPIO
    gpio_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)
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)
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); // 点亮LED
        else 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驱动开发的核心技能,标志着你已迈入"标准驱动开发"的大门。

相关推荐
玉树临风江流儿15 小时前
Linux驱动开发总结速记
linux·运维·驱动开发
A-花开堪折16 小时前
Qemu 嵌入式Linux驱动开发
linux·运维·驱动开发
算力魔方AIPC20 小时前
Spec-Kit+Copilot打造AI规格驱动开发
人工智能·驱动开发·copilot
mucheni1 天前
迅为RK3568开发板OpenHarmony系统南向驱动开发手册-pdf配置 rk3568_uart_config.hcs
驱动开发·pdf
应用市场2 天前
Linux驱动开发原理详解:从入门到实践
linux·运维·驱动开发
linweidong2 天前
跨平台驱动开发:打造兼容多款MCU的硬核方案
驱动开发·单片机·嵌入式硬件·bsp·rtos·spi驱动·hal设计
撬动未来的支点3 天前
【Linux】Linux驱动开发与BSP开发:嵌入式系统的两大基石
linux·驱动开发
碰大点3 天前
第8章 zynq uboot更新系统镜像并引导启动和个人心得
驱动开发·fpga开发·uboot·zynq