IMX6ULL Linux 驱动开发:GPIO 子系统 + misc 框架实现按键输入驱动开发

上篇博客讲解的驱动开发的标准具体开发流程,但是那个框架没用到设备树和子系统可移植性太低了,但掌握那个是基础的东西。今天介绍一种新的框架GPIO 子系统 + misc 框架,本文基于正点原子 IMX6ULL 开发板,讲解一个基于 misc 框架的按键输入驱动开发全流程,同时配套对应的应用层测试程序。

一、理论基础

1.1 Linux 驱动分层思想与两大核心子系统

Linux 驱动设计的核心思想是设备与驱动分离驱动分层,这一思想在 GPIO 相关驱动中,落地为 pinctrl 子系统和 GPIO 子系统两大核心组件,彻底替代了裸机开发中直接操作寄存器的开发模式。

  • pinctrl 子系统:负责芯片引脚的复用配置与电气特性设置。IMX6ULL 的绝大多数引脚都具备多种功能,既可以配置为通用 GPIO,也可以复用为 I2C、PWM、UART 等外设功能,这部分引脚级的配置工作全部由 pinctrl 子系统统一管理,避免了驱动代码中直接操作寄存器带来的移植性问题。
  • GPIO 子系统:在 pinctrl 子系统将引脚配置为 GPIO 功能后,GPIO 子系统负责统一管理 GPIO 的输入 / 输出方向配置、电平读写等操作。内核封装了一套跨平台的标准 API,开发者无需关注底层寄存器细节,只需调用通用 API 即可完成 GPIO 的所有操作,极大提升了驱动的跨平台性和可维护性。

1.2 misc 杂项设备驱动框架

标准的字符设备驱动开发,需要手动分配设备号、初始化 cdev 结构体、添加字符设备、创建设备类与设备节点,流程繁琐且模板代码冗余。而 misc 设备(杂项设备)是内核对字符设备驱动的一层轻量化封装,其主设备号固定为 10,内核会自动分配次设备号,大幅简化了简单字符设备的开发流程。

  • 核心数据结构struct miscdevice,核心成员包括:
    • minor:次设备号,通常设置为MISC_DYNAMIC_MINOR,让内核自动分配;
    • name:设备名,对应 /dev 目录下生成的设备节点名称;
    • fops:文件操作集,绑定驱动的 open/read/write 等业务接口。
  • 核心 API
    • misc_register():注册 misc 设备,一步完成字符设备注册、设备类与设备节点的创建,替代了标准字符设备的多步操作;
    • misc_deregister():注销 misc 设备,自动释放所有相关内核资源。
  • 核心优势:对于按键、LED、蜂鸣器这类简单的字符设备,使用 misc 框架可以大幅减少模板代码,让开发者聚焦于硬件操作的核心业务逻辑。

1.3 设备树与驱动的适配

Linux 3.x 版本之后引入设备树(Device Tree),核心目的是实现硬件信息与驱动代码的分离。硬件平台的寄存器地址、引脚信息、中断配置等硬件相关内容,全部写在设备树.dts 文件中,驱动代码通过内核提供的 API 读取设备树中的硬件信息,从而实现一套驱动适配多个硬件平台。

  • 设备树 GPIO 节点核心属性:
    • compatible:驱动与设备节点匹配的唯一标识符,驱动中通过该字符串匹配对应的设备树节点;
    • gpio-key = <&gpio1 9 GPIO_ACTIVE_LOW>:自定义 GPIO 属性,指定了 GPIO 所属组、引脚编号、有效电平;
    • status = "okay":启用该节点,内核会解析该节点;若为disabled则内核会忽略该节点。
  • 驱动中设备树常用 API:
    • of_find_node_by_path():通过设备树节点路径查找对应的节点结构体;
    • of_get_named_gpio():从设备树节点中解析出 GPIO 编号,供 GPIO 子系统 API 使用。

1.4 sysfs 虚拟文件系统

sysfs 是 Linux 内核提供的一种虚拟文件系统,默认挂载在 /sys 目录下,用于向用户空间暴露内核中设备、驱动、总线的相关信息与属性,是内核与用户空间交互的重要通道。

  • 核心目录结构:
    • /sys/devices:存放系统中所有物理设备的层级信息,是 sysfs 的核心目录;
    • /sys/bus:存放各类总线(platform、i2c、spi 等)的设备与驱动信息,实现设备与驱动的匹配管理;
    • /sys/class:按设备类型分类存放设备信息,misc 设备会统一存放在/sys/class/misc/目录下;
    • /sys/dev/:按字符设备 / 块设备的主次设备号分类,提供设备节点的快速访问。
  • 实际作用:misc 按键驱动注册成功后,会自动在/sys/class/misc/下生成 key_misc 目录,里面包含了设备的主次设备号、驱动归属等信息,既可以用于驱动调试,也可以通过 sysfs 属性文件实现用户空间与内核空间的直接交互。

1.5 驱动逻辑与 GPIO 标准操作

  • GPIO 子系统核心 API:
    • gpio_request()/devm_gpio_request():申请 GPIO 资源,防止多驱动同时占用同一个 GPIO;其中devm_前缀的函数是设备资源管理版,驱动卸载时会自动释放资源,无需手动调用gpio_free()
    • gpio_direction_input():将 GPIO 配置为输入模式,适用于按键、传感器等输入外设;
    • gpio_direction_output():将 GPIO 配置为输出模式,适用于 LED、继电器等输出外设;
    • gpio_get_value():读取 GPIO 引脚的电平值,返回 0 为低电平,非 0 为高电平;
    • gpio_set_value():设置 GPIO 引脚的输出电平。

二、按键驱动开发(基于 misc 框架 + GPIO 子系统)

基于上述理论基础,我们完整实现一个按键输入驱动,配套代码分为驱动层key_drv.c和应用层key_app.c,同时完成设备树节点的适配,最终实现按键按下翻转 LED 灯状态的完整功能。

2.1 硬件与功能需求说明

  • 硬件平台:正点原子 IMX6ULL 开发板,按键引脚连接到 SOC 的 GPIO1_IO09,按键采用上拉输入设计,按下时引脚为低电平,松开时为高电平;LED 灯通过 GPIO1_IO03 控制,推挽输出模式。
  • 功能需求
    1. 驱动通过 misc 框架注册,生成/dev/key_misc设备节点,LED 驱动同步生成/dev/led_misc设备节点;
    2. 应用层通过 read 接口读取按键状态,按下返回KEY_ON(1),松开返回KEY_OFF(0)
    3. 应用程序实现按键消抖,按下按键时翻转 LED 灯状态,实现按键控制 LED 的完整链路。

2.2 设备树节点修改

首先需要在 IMX6ULL 的设备树文件imx6ull-alientek-emmc.dts中添加按键的设备树节点,将硬件信息传递给内核与驱动。

步骤 1:添加 pinctrl 引脚配置节点

在 iomuxc 根节点中添加按键引脚的 pinctrl 配置,将引脚复用为 GPIO 功能,并设置上拉电气特性:

dts

复制代码
pinctrl_key: keygrp {
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO09__GPIO1_IO09  0x10B0 /* 按键引脚,上拉模式 */
    >;
};
步骤 2:添加按键设备根节点

在设备树根节点下添加 putekey 自定义节点,描述按键的硬件信息:

dts

复制代码
putekey {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "pute-key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    gpio-key = <&gpio1 9 GPIO_ACTIVE_LOW>;
    status = "okay";
};

节点关键说明:

  • pinctrl-0 = <&pinctrl_key>:引用上面定义的引脚复用配置,确保内核启动时将该引脚初始化为 GPIO 功能;
  • gpio-key = <&gpio1 9 GPIO_ACTIVE_LOW>:指定按键使用 GPIO1 组的第 9 号引脚,低电平为有效状态(按键按下);
  • status = "okay":启用该节点,确保内核启动时解析该节点。
步骤 3:编译与更新设备树

修改完成后,在内核源码目录执行make dtbs重新编译设备树,生成的imx6ull-alientek-emmc.dtb文件通过 tftp 下载到开发板,替换原有的设备树文件,重启开发板使配置生效。

2.3 驱动层代码完整解析(key_drv.c)

1. 头文件引入

c

运行

复制代码
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>

关键头文件说明:

  • linux/miscdevice.h:misc 设备驱动核心头文件,包含核心结构体与注册 / 注销 API;
  • linux/gpio.h/linux/of_gpio.h:GPIO 子系统与设备树 GPIO 解析 API 的核心头文件;
  • asm/uaccess.h:包含copy_to_user/copy_from_user,是内核空间与用户空间数据交互的唯一合规方式;
  • linux/of.h:设备树节点操作 API 的核心头文件。
2. 宏定义与全局变量

c

运行

复制代码
#define KEY_ON  1
#define KEY_OFF 0
static int gpiokeynum; // 保存从设备树解析出的GPIO编号,全局供read接口使用

gpiokeynum为全局静态变量,在驱动入口函数中完成 GPIO 编号的解析与赋值,在 read 接口中用于读取引脚电平。

3. 文件操作集 read 接口实现

c

运行

复制代码
static ssize_t key_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    int value = 0;
    unsigned long nret = 0;
    // 调用GPIO子系统API读取引脚电平
    value = gpio_get_value(gpiokeynum);
    if (value < 0) {
        pr_info("get gpiokeynum faikey\n");
        return -EINVAL;
    }
    // 电平逻辑转换:低电平(按键按下)→ KEY_ON,高电平(按键松开)→ KEY_OFF
    value = (value == KEY_ON) ? KEY_OFF : KEY_ON;
    // 将按键状态从内核空间拷贝到用户空间
    nret = copy_to_user(puser, &value, sizeof(value));
    if (nret != 0) {
        pr_info("copy_to_user faikey\n");
        return -EFAULT;
    }
    return sizeof(value);
}

核心逻辑解析:

  • gpio_get_value(gpiokeynum):GPIO 子系统标准 API,替代了手动映射寄存器读取电平的裸机操作;
  • 电平逻辑转换:匹配设备树中GPIO_ACTIVE_LOW的配置,将硬件电平转换为应用层可识别的按键状态;
  • copy_to_user:内核空间无法直接访问用户空间的内存地址,必须通过该函数完成数据拷贝,返回 0 表示拷贝成功,非 0 表示失败,这是 Linux 驱动中用户与内核空间交互的强制规范;
  • 接口返回值:返回成功拷贝的字节数。
4. 文件操作集结构体绑定

c

运行

复制代码
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = key_read,
};

struct file_operations是字符设备驱动的核心,里面的函数指针绑定了驱动对设备文件的操作接口。这里我们仅实现了 read 接口用于读取按键状态,若需要实现写操作,只需添加.write成员并绑定对应的处理函数即可(代码注释部分为 write 接口的完整实现)。

  • .owner = THIS_MODULE:固定写法,将该操作集归属到当前内核模块,防止模块被使用时被意外卸载。
5. misc 设备结构体定义

c

运行

复制代码
static struct miscdevice key_misc = {
    .minor = MISC_DYNAMIC_MINOR, // 内核自动分配次设备号
    .name = "key_misc", // 设备节点名,注册成功后生成/dev/key_misc
    .fops = &fops, // 绑定文件操作集
};

这是 misc 驱动的结构体。通过动态分配次设备号,避免了手动管理设备号的冲突问题,设备名直接对应 /dev 目录下的设备节点名称。

6. 驱动入口函数实现

c

运行

复制代码
static int __init key_drv_init(void)
{ 
    int ret = 0;
    struct device_node *pkeynode = NULL;

    // 步骤1:注册misc设备,一步完成字符设备全流程注册
    ret = misc_register(&key_misc);
    if (ret != 0) {
        pr_info("misc register faikey\n");
        return ret;
    }

    // 步骤2:通过路径查找设备树中的putekey节点
    pkeynode = of_find_node_by_path("/putekey");
    if (pkeynode == NULL) {
        pr_info("can not find gpiokeynum\n");
        ret = -ENODEV;
        goto err_deregister;
    }

    // 步骤3:从设备树节点中解析GPIO编号
    gpiokeynum = of_get_named_gpio(pkeynode, "gpio-key", 0);
    if (gpiokeynum < 0) {
        pr_info("get gpiokeynum faikey\n");
        ret = -EINVAL;
        goto err_deregister;
    }

    // 步骤4:申请GPIO资源,使用devm_自动管理版本,避免资源泄漏
    ret = devm_gpio_request(key_misc.this_device, gpiokeynum, "key_drv");
    if (ret != 0) {
        pr_info("request gpio faikey\n");
        ret = -EBUSY;
        goto err_deregister;
    }

    // 步骤5:将GPIO配置为输入模式
    gpio_direction_input(gpiokeynum);

    pr_info("key drv init success!\n");
    return 0;

// 错误处理:注册失败时注销已申请的misc设备,避免内核资源泄漏
err_deregister:
    misc_deregister(&key_misc);
    return ret;
}

入口函数是驱动加载时执行的核心逻辑,采用 goto 错误处理的内核规范写法,步骤拆解如下:

  1. 注册 misc 设备:调用misc_register一步完成字符设备的全流程注册,这是 misc 框架相比标准字符设备最大的优势;
  2. 查找设备树节点:通过of_find_node_by_path找到自定义的 putekey 节点,是驱动获取硬件信息的第一步;
  3. 解析 GPIO 编号:通过of_get_named_gpio从节点属性中解析出 GPIO 编号,后续所有 GPIO 操作都基于该编号;
  4. 申请 GPIO 资源:调用devm_gpio_request申请 GPIO,确保该引脚不会被其他驱动占用,同时利用内核资源管理机制自动释放;
  5. 配置 GPIO 模式:调用gpio_direction_input将引脚设置为输入模式,匹配按键的输入外设属性;
  6. 错误处理:每一步操作失败时,都通过 goto 语句注销已注册的 misc 设备,避免内核资源泄漏。

c

运行

复制代码
static void __exit key_drv_exit(void)
{
    // 注销misc设备,释放所有相关内核资源
    misc_deregister(&key_misc);
    pr_info("key drv exit success!\n");
}

驱动卸载时执行该函数,核心操作是调用misc_deregister注销 misc 设备,自动删除 /dev 目录下的设备节点。由于我们使用了devm_gpio_request申请 GPIO,这里无需手动释放 GPIO,内核会在设备注销时自动处理相关资源。

8. 模块声明与许可证

c

运行

复制代码
module_init(key_drv_init);  // 绑定驱动入口函数,insmod时执行
module_exit(key_drv_exit);  // 绑定驱动出口函数,rmmod时执行
MODULE_LICENSE("GPL");      // 声明许可证,必须遵循GPL协议,否则无法使用内核核心API
MODULE_AUTHOR("wang");      // 声明驱动作者

这是 Linux 内核模块的固定格式,MODULE_LICENSE("GPL")是强制要求,否则内核会报模块污染警告,且无法使用内核的大部分核心 API。

2.4 应用层测试代码解析(key_app.c)

驱动实现完成后,编写应用层程序,遵循 Linux "一切皆是文件" 的设计思想,通过设备文件访问驱动,实现按键状态读取与 LED 控制。

c

运行

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define KEY_ON  1
#define KEY_OFF 0
#define LED_ON  1
#define LED_OFF 0

// 毫秒级延时函数,用于按键软件消抖
void delay_ms(int ms)
{
    usleep(ms * 1000);
}

int main(void)
{
    int fd_key = 0;
    int fd_led = 0;
    int readstat_pre = 0;  // 上一次的按键状态
    int readstat_cur = 0;  // 当前的按键状态
    int flag = 0;          // LED灯状态标志,0为灭,1为亮

    // 1. 打开按键设备节点,获取文件描述符
    fd_key = open("/dev/key_misc", O_RDWR);
    if (-1 == fd_key)
    {
        perror("fail to open key device");
        return -1;
    }

    // 2. 打开LED设备节点(需提前实现LED的misc驱动)
    fd_led = open("/dev/led_misc", O_RDWR);
    if (-1 == fd_led)
    {
        perror("fail to open led device");
        close(fd_key);
        return -1;
    }

    // 3. 循环扫描按键状态
    while (1)
    {
        // 读取当前按键状态,触发驱动中的key_read接口
        read(fd_key, &readstat_cur, sizeof(readstat_cur));

        // 按键状态发生变化,执行消抖处理
        if (readstat_cur != readstat_pre)
        { 
            // 仅在按键按下时,翻转LED状态
            if(readstat_cur == KEY_ON)
            {
                flag = !flag;
                write(fd_led, &flag, sizeof(flag));
                printf("key pressed, led state: %s\n", flag ? "on" : "off");
            }
        }

        // 10ms延时消抖,更新上一次的按键状态
        delay_ms(10);
        readstat_pre = readstat_cur;
    }

    // 关闭设备文件,释放资源
    close(fd_key);
    close(fd_led);
    return 0;
}

核心逻辑解析:

  1. 设备文件打开 :通过open函数打开/dev/key_misc/dev/led_misc设备节点,获取文件描述符,这是 Linux 应用层访问字符设备的标准方式。
  2. 按键状态读取 :在循环中通过read函数读取按键状态,该函数会系统调用陷入内核,最终执行驱动中绑定的key_read接口,获取按键的实时状态;
  3. 按键消抖处理:通过对比当前状态与上一次状态,判断按键是否发生有效变化,配合 10ms 的延时,避免按键机械触点抖动导致的状态误判;
  4. LED 状态控制 :仅当按键按下时,翻转 LED 状态标志,并通过write函数将状态写入 LED 设备节点,触发 LED 驱动的 write 接口,完成 LED 亮灭控制。

三、驱动编译与加载测试

3.1 驱动 Makefile 编写

编写 Makefile,通过交叉编译工具链将驱动代码编译为可在 IMX6ULL 开发板上运行的.ko 内核模块文件:

makefile

复制代码
# 内核源码路径,需替换为自己的实际路径
KERNELDIR := /home/linux/imx6ull/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# 当前工程路径
CURRENT_PATH := $(shell pwd)
# 交叉编译工具链,对应课程第7章配置的工具链
CROSS_COMPILE := arm-linux-gnueabihf-
# 目标硬件架构
ARCH := arm

# 指定编译的目标模块
obj-m := key_drv.o

# 编译目标
build: kernel_modules

# 内核模块编译命令
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

# 清理编译产物
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean

在 Ubuntu 终端执行make命令,即可生成key_drv.ko驱动模块文件。

3.2 应用程序交叉编译

使用交叉编译工具链编译应用层代码,生成可在开发板上运行的可执行文件:

bash

运行

复制代码
arm-linux-gnueabihf-gcc key_app.c -o key_app

编译完成后,生成key_app可执行文件。

3.3 开发板加载与功能测试

  1. 文件传输 :将key_drv.kokey_app文件通过 nfs 共享目录(课程第 6 章配置)拷贝到开发板中;

  2. 驱动加载 :在开发板终端执行以下命令加载驱动模块:

    bash

    运行

    复制代码
    insmod key_drv.ko

    加载成功后,终端会打印key drv init success!,同时查看 /dev 目录,会自动生成key_misc设备节点:

    bash

    运行

    复制代码
    ls /dev/key_misc

    也可通过 sysfs 查看设备信息,验证 misc 设备注册成功:

    bash

    运行

    复制代码
    ls /sys/class/misc/key_misc
  3. LED 驱动准备 :确保 LED 的 misc 驱动已成功加载,/dev/led_misc设备节点正常生成;

  4. 应用程序运行 :给应用程序添加执行权限并启动:

    bash

    运行

    复制代码
    chmod 777 key_app
    ./key_app
  5. 功能测试:按下开发板上的按键,终端会打印状态信息,同时 LED 灯状态翻转;松开按键无操作,再次按下再次翻转,功能符合预期;

  6. 驱动卸载 :测试完成后,执行以下命令卸载驱动模块:

    bash

    运行

    复制代码
    rmmod key_drv.ko

    卸载成功后,终端会打印key drv exit success!/dev/key_misc设备节点会被内核自动删除。

四、常见问题排查与知识点深化

4.1 驱动加载常见问题与解决方案

  1. 问题 :insmod 驱动时提示No such device,终端打印can not find gpiokeynum

    • 原因:设备树节点未正确添加、语法错误,或设备树文件未更新到开发板,内核无法找到/putekey节点;
    • 解决方案:检查设备树节点语法,重新编译并更新开发板的设备树文件,通过ls /proc/device-tree/确认节点已被内核正确解析。
  2. 问题 :insmod 驱动时提示Device or resource busy,打印request gpio faikey

    • 原因:申请的 GPIO 引脚被设备树中其他节点占用,或被其他驱动申请;
    • 解决方案:检查设备树中是否有其他节点使用了该 GPIO,禁用冲突节点;或更换未被使用的 GPIO 引脚,通过cat /sys/kernel/debug/gpio查看 GPIO 占用情况。
  3. 问题 :应用层 open 设备节点失败,提示No such file or directory

    • 原因:驱动未成功加载,设备节点未生成;或设备节点名称拼写错误;
    • 解决方案:通过dmesg查看内核打印信息,确认驱动注册成功;通过ls /dev查看设备节点是否正常生成。
  4. 问题:应用层 read 按键状态无变化,始终为固定值

    • 原因:硬件接线错误、GPIO 引脚配置错误,或设备树中有效电平设置与实际硬件不符;

    • 解决方案:通过万用表测量按键引脚的实际电平,确认硬件接线正确;检查设备树中GPIO_ACTIVE_LOW/GPIO_ACTIVE_HIGH是否与硬件匹配;通过 sysfs 手动操作 GPIO,验证引脚功能正常:

      bash

      运行

      复制代码
      # 导出GPIO,替换为实际的GPIO编号
      echo 9 > /sys/class/gpio/export
      # 设置为输入模式
      echo in > /sys/class/gpio/gpio9/direction
      # 读取电平值
      cat /sys/class/gpio/gpio9/value

五、总结

整个开发过程中,我们需要遵循 Linux 驱动的设计思想:通过设备树实现硬件信息与驱动代码的分离,通过 GPIO 子系统屏蔽底层寄存器操作,通过 misc 框架简化字符设备的注册流程,最终实现了一套可移植、易维护的外设驱动。这一开发框架,不仅适用于按键、LED 这类简单外设,也是 I2C、SPI、LCD 等复杂外设驱动开发的基础。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式