基于 imx6ull平台按键驱动开发:input子系统+中断子系统+platform总线

在嵌入式 Linux 开发中,按键是最基础的人机交互外设之一。本文以 NXP MX6UL 芯片为例,完整讲解从设备树配置、内核驱动开发(基于 input 子系统 + 平台驱动模型)到应用层测试的全部流程,实现 "按键中断触发 - 软中断延时消抖 - input 事件上报" 的逻辑。

一、背景

本次开发基于 MX6UL 芯片,需求是实现一个按键的驱动开发:

  • 硬件层面:按键接 GPIO1_IO18,低电平触发(下降沿中断);
  • 驱动层面:基于 Linux input子系统 上报按键事件(符合 Linux 输入子系统规范),基于平台驱动模型适配设备树,通过tasklet(软中断)+定时器实现按键消抖(避免机械抖动导致误触发);
  • 应用层面:读取 input 子系统的事件节点,解析按键状态。

技术栈:Linux 平台驱动、设备树(Device Tree)、GPIO 中断、input 子系统、tasklet、内核定时器、sysfs 属性暴露。

二、设备树配置解析

Linux 设备树(DTB)负责硬件信息的抽象描述,驱动通过设备树匹配硬件资源,无需硬编码 GPIO / 中断号,提升可移植性。以下是本次按键对应的设备树节点配置:

1. 按键核心节点(putekey)

dts

复制代码
putekey {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "pute,putekey";  // 驱动匹配的核心标识
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_putekey>;  // 绑定引脚复用配置
    gpio-key = <&gpio1 18 0>;  // 按键GPIO:GPIO1_IO18,属性0(输入模式)
    interrupt-parent = <&gpio1>;  // 中断父控制器(GPIO1)
    interrupts = <18 IRQ_TYPE_EDGE_FALLING>;  // 中断配置:GPIO1_IO18,下降沿触发
    status = "okay";  // 启用该节点
};

关键属性说明:

  • compatible:平台驱动通过of_match_table匹配该字符串,是驱动与设备树绑定的核心;
  • pinctrl-0:绑定引脚复用配置节点pinctrl_putekey,确保 GPIO1_IO18 引脚被正确配置为 GPIO 功能;
  • gpio-key:自定义属性,驱动通过of_get_named_gpio读取该属性获取 GPIO 号;
  • interrupts:中断触发方式(IRQ_TYPE_EDGE_FALLING = 下降沿),驱动通过of_irq_get读取中断号。

2. 引脚复用配置节点(pinctrl_putekey)

dts

复制代码
pinctrl_putekey: putekey {
    fsl,pins = <
        MX6UL_PAD_UART1_CTS_B__GPIO1_IO18       0xF080
    >;
};
  • MX6UL_PAD_UART1_CTS_B__GPIO1_IO18:MX6UL 芯片的引脚复用宏,将 UART1_CTS_B 引脚复用为 GPIO1_IO18 功能;
  • 0xF080:引脚电气属性配置(包含上拉 / 下拉、速率、驱动能力等),需根据硬件电路调整(本次配置为默认上拉,适配按键低电平触发)。

三、内核驱动代码解析(key_drv.c)

驱动代码基于 Linux 平台驱动框架实现,分为 "资源初始化、中断处理、消抖、input 事件上报" 四大模块,以下分模块拆解:

1. 头文件与全局变量

c

运行

复制代码
// 涵盖input子系统、GPIO、中断、平台驱动、设备树、定时器等核心头文件
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/of.h>
#include <asm/uaccess.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <linux/timer.h>
#include <uapi/asm-generic/poll.h>
#include <linux/poll.h>
#include <linux/input.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/platform_device.h>

#define KEY_ON  1
#define KEY_OFF 0

static struct input_dev *pinputdev = NULL;  // input子系统核心结构体
static int gpiokeynum;                     // 按键GPIO号
struct device_node *pkeynode = NULL;        // 设备树节点指针
static int irqno;                           // 中断号
static wait_queue_head_t wq;                // 等待队列(本文未实际使用)
static struct work_struct my_workquene;     // 工作队列(备用)
static struct timer_list my_timer;          // 消抖定时器
  • input_dev:是 Linux 输入子系统的核心结构体,用于向内核上报按键、鼠标等输入事件;
  • 定时器my_timer:用于按键消抖(机械按键按下 / 释放时会有 10ms 左右的抖动,需延时确认);
  • tasklet:软中断机制,用于延后处理中断上下文的耗时操作(中断上下文需尽可能短)。

2. 核心功能函数

(1)sysfs 属性函数(key_show)

暴露按键状态到用户空间(sysfs),方便调试:

c

运行

复制代码
static ssize_t key_show(struct device *dev, struct device_attribute *attr, char *buf)
{   
    // 读取GPIO电平(!gpio_get_value:低电平表示按键按下)
    if (KEY_ON == !gpio_get_value(gpiokeynum)) {
        pr_info("KEY_ON\n");
    }
    else if (KEY_OFF == !gpio_get_value(gpiokeynum)) {
        pr_info("KEY_OFF\n");
    }
    return 0;
}
static struct device_attribute key_attr = {
    .attr = {
        .name = "attr",
        .mode = 0444,  // 只读权限
    },
    .show = key_show,
};

驱动加载后,可通过cat /sys/devices/virtual/input/inputX/attr查看按键状态日志。

(2)定时器消抖函数(timerout_fun)

延时 10ms 后确认按键状态,避免抖动误触发:

c

运行

复制代码
static void timerout_fun(unsigned long data)
{
    // 再次读取GPIO,确认按键真的按下(消抖核心)
    if(!gpio_get_value(gpiokeynum)){
        // 上报KEY_0按下事件(value=1)
        input_event(pinputdev, EV_KEY, KEY_0, 1);
        input_sync(pinputdev);  // 同步事件(input子系统必需)
        // 上报KEY_0释放事件(value=0)
        input_event(pinputdev, EV_KEY, KEY_0, 0);
        input_sync(pinputdev);
    }
    return;   
}
  • input_event:向 input 子系统上报事件,参数分别为:input 设备、事件类型(EV_KEY = 按键)、按键码(KEY_0)、状态(1 = 按下 / 0 = 释放);
  • input_sync:同步事件,告知内核本次按键事件上报完成。
(3)tasklet 处理函数(tasklet_fun)

tasklet 是软中断机制,用于延后处理中断上下文的操作(中断上下文不能执行耗时操作):

c

运行

复制代码
static void tasklet_fun(unsigned long data)
{
    // 重置定时器:延时10ms触发消抖函数
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(10));
}
static DECLARE_TASKLET(my_tasklet, tasklet_fun, 0);  // 定义并初始化tasklet
(4)中断处理函数(key_irq_handle)

按键中断触发后的入口函数,仅调度 tasklet,保证中断上下文极简:

c

运行

复制代码
static irqreturn_t key_irq_handle(int irq, void *dev_id)
{
    // 调度tasklet(软中断),延后处理消抖逻辑
    tasklet_schedule(&my_tasklet);
    // 工作队列(备用方案,本文未启用)
    //schedule_work(&my_workquene);
    return IRQ_HANDLED;
}

3. 平台驱动核心(probe/remove)

(1)probe 函数(设备匹配成功后执行)

probe 是平台驱动的核心入口,负责初始化硬件资源、注册 input 设备:

c

运行

复制代码
static int key_probe(struct platform_device *pdevice)
{
    int ret = 0;

    // 1. 分配input设备
    pinputdev = input_allocate_device();
    if (pinputdev == NULL) {
        pr_info("alloc input dev faikey\n");
        return -1;
    }

    // 2. 配置input设备属性
    pinputdev->name = "key_drv";
    pinputdev->evbit[0] = EV_KEY | EV_REP;  // 支持按键事件+重复事件
    input_set_capability(pinputdev, EV_KEY, KEY_0);  // 注册KEY_0按键

    // 3. 从设备树获取GPIO资源
    pkeynode = of_find_node_by_path("/putekey");
    if (pkeynode == NULL) {
        pr_info("can not find gpiokeynum");
        input_free_device(pinputdev);
        return -1;
    }
    gpiokeynum = of_get_named_gpio(pkeynode, "gpio-key", 0);
    if (gpiokeynum < 0) {
        pr_info("get gpiokeynum faikey\n");
        input_free_device(pinputdev);
        return -1;
    }

    // 4. 初始化定时器、工作队列
    init_timer(&my_timer);
    my_timer.function = timerout_fun;
    add_timer(&my_timer);
    INIT_WORK(&my_workquene, workquene_fun);

    // 5. 获取中断号(从设备树读取)
    irqno = of_irq_get(pkeynode, 0);
    pr_info("get irq %d\n", irqno);

    // 6. 请求中断(devm_xxx:设备资源管理,自动释放)
    ret = devm_request_irq(&pinputdev->dev, irqno, key_irq_handle, IRQF_TRIGGER_FALLING, "key_drv", NULL);
    if (ret != 0) {
        pr_info("request irq faikey\n");
        input_free_device(pinputdev);
        return -1;
    }

    // 7. 请求GPIO并配置为输入
    ret = devm_gpio_request(&pinputdev->dev, gpiokeynum, "key_drv");
    if (ret != 0) {
        pr_info("request gpio faikey\n");
        input_free_device(pinputdev);
        return -1;
    }
    gpio_direction_input(gpiokeynum);

    // 8. 注册input设备(核心:向系统暴露input事件节点)
    ret = input_register_device(pinputdev);
    if (ret != 0) {
        pr_info("input_register_device faikey\n");
        input_free_device(pinputdev);
        return -1;
    }

    // 9. 创建sysfs属性文件(调试用)
    ret = device_create_file(&pinputdev->dev, &key_attr);
    if (ret != 0) {
        pr_info("device_create_file failed\n");
        return -1;
    }

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

关键要点:

  • devm_request_irq/devm_gpio_request:内核设备资源管理接口,无需手动释放资源,驱动卸载时自动回收,避免内存泄漏;
  • input_register_device:注册后,系统会在/dev/input/目录下生成 eventX 节点(如 event1),应用层可通过该节点读取按键事件。
(2)remove 函数(驱动卸载时执行)

释放资源,保证内核资源不泄漏:

c

运行

复制代码
static int key_remove(struct platform_device *pdevice)
{
    input_unregister_device(pinputdev);  // 注销input设备
    del_timer(&my_timer);               // 删除定时器
    pr_info("Kernel:key remove success\n");
    return 0;
}

4. 平台驱动注册

c

运行

复制代码
// 设备树匹配表(与设备树compatible属性对应)
static const struct of_device_id key_of_match_table[] = {
    {.compatible = "pute,putekey"},
    {},
};

// 平台设备ID表(备用匹配方式)
static const struct platform_device_id key_id_table[] = {
    {.name = "putekey"},
    {},
};

// 平台驱动结构体
static struct platform_driver key_drv = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "putekey",
        .of_match_table = key_of_match_table,  // 优先匹配设备树
    },
    .id_table = key_id_table,
};

// 模块入口/出口
static int __init key_drv_init(void)
{
    platform_driver_register(&key_drv);  // 注册平台驱动
    pr_info("key_drv_init success!\n");
    return 0;
}
static void __exit key_drv_exit(void)
{
    platform_driver_unregister(&key_drv);  // 注销平台驱动
    pr_info("key_drv_exit success!\n");
}
module_init(key_drv_init);
module_exit(key_drv_exit);

MODULE_LICENSE("GPL");  // 必须声明GPL协议,否则内核拒绝加载
MODULE_AUTHOR("pute");

四、应用层代码解析(key_app.c)

应用层通过读取/dev/input/eventX节点,解析 input 子系统上报的按键事件:

c

运行

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <linux/input.h>

#define KEY_ON  1
#define KEY_OFF 0

void delay_ms(int ms)
{
    usleep(ms * 1000);
}
int main(void)
{
    int fd = 0;
    int ret = 0;
    struct input_event env;

    // 打开input事件节点(需根据实际系统调整event1)
    fd = open("/dev/input/event1", O_RDONLY);
    if (-1 == fd)
    {
        printf("open error\n");
        return -1;
    }

    // 循环读取按键事件
    while (1)
    {
        ret = read(fd, &env, sizeof(env));
        printf("===============================================\n");
        printf("type:%d, code:%d, value:%d\n", env.type, env.code, env.value);
    }
    
    close(fd);
    return 0;
}
  • struct input_event:input 子系统的事件结构体,包含type(事件类型)、code(按键码)、value(状态);
  • 关键值说明:
    • type=1:EV_KEY(按键事件);
    • code=11:KEY_0(对应驱动中注册的 KEY_0);
    • value=1:按键按下;value=0:按键释放。

五、编译与测试

1. 驱动编译(Makefile)

编写 Makefile,基于 MX6UL 内核源码编译驱动模块:

makefile

复制代码
OBJ := key_drv


#内核路径
kerdir := (填自己的内核路径)

#当前驱动工程目录
curdir := $(shell pwd)

#代码添加到工程编译选项中
obj-m += $(OBJ).o 

all:
	make -C $(kerdir) M=$(curdir) modules
	cp $(OBJ).ko ~/nfs/rootfs

.PHONY:

clean:
	make -C $(kerdir) M=$(curdir) modules clean

distclean:
	make -C $(kerdir) M=$(curdir) modules clean
	rm ~/nfs/rootfs/$(OBJ).ko

执行make编译,生成key_drv.ko模块。

2. 应用层编译(交叉编译)

MX6UL 是 ARM 架构,需用交叉编译器:

bash

运行

复制代码
自己的交叉编译工具 key_app.c -o key_app

3. 测试步骤

  1. 加载驱动 :将key_drv.ko拷贝到开发板,执行insmod key_drv.ko

  2. 验证驱动加载 :执行dmesg | grep key,查看日志:

    plaintext

    复制代码
    key_drv_init success!
    get irq 48
    key drv init success!
  3. 确认 input 设备 :执行cat /proc/bus/input/devices,找到Name="key_drv"的设备,记录其 event 节点(如 event1);

  4. 运行应用程序 :执行./key_app,按下按键,终端打印:

  5. 调试 sysfs 属性 :执行cat /sys/devices/virtual/input/inputX/attr,按下按键时会打印KEY_ON,释放时打印KEY_OFF

4. 卸载驱动

bash

运行

复制代码
rmmod key_drv

执行dmesg | grep key,可看到Kernel:key remove success日志。

六、关键注意事项

  1. 设备树匹配 :驱动中of_match_tablecompatible必须与设备树一致,否则平台驱动无法匹配;
  2. 中断触发方式 :驱动中IRQF_TRIGGER_FALLING需与设备树interruptsIRQ_TYPE_EDGE_FALLING一致;
  3. input 节点号/dev/input/event1可能因系统不同变化,需通过/proc/bus/input/devices确认;
  4. 消抖延时:10ms 是经验值,可根据硬件按键的机械特性调整(如 5ms/20ms);
  5. 中断上下文规范:中断处理函数仅做 "调度"(tasklet / 工作队列),不执行耗时操作,符合 Linux 内核编程规范;
  6. 资源释放 :优先使用devm_xxx接口,避免手动释放资源导致的内存泄漏。

七、总结

本文基于 MX6UL 平台,完整实现了 "设备树 + 平台驱动 + input 子系统 + 中断 + 消抖" 的按键驱动开发流程

相关推荐
莎士比亚的文学花园2 小时前
Linux驱动开发(1)——系统移植
linux·运维·服务器
PH = 72 小时前
OverlayFS联合文件系统使用示例
java·linux·服务器
AC赳赳老秦2 小时前
OpenClaw进阶技巧:批量修改文件内容、替换关键词,解放双手
java·linux·人工智能·python·算法·测试用例·openclaw
Joseph Cooper3 小时前
STM32MP157 Linux驱动学习笔记(四):典型总线与设备模型(SPI/USB)
linux·stm32·学习
坚持就完事了3 小时前
Linux中的mv命令
linux·运维·服务器
SongYuLong的博客3 小时前
Claude Code安装配置(Linux)
linux·运维·服务器
栈低来信4 小时前
kernel信号量源码分析
linux
结衣结衣.4 小时前
手把手教你实现文档搜索引擎
linux·c++·搜索引擎·开源·c++11
sdm0704275 小时前
进程间通信
linux·运维·服务器