Linux字符设备驱动开发(七):输入子系统——驱动GPIO按键并上报事件

前言

上一篇文章中,我们学习了I2C子系统的使用,并驱动了AT24C02 EEPROM。从简单的LED控制到总线设备,我们已经掌握了多种外设的驱动方法。但这些驱动都是"输出型"的------我们向硬件发命令。实际产品中,大量的交互来自输入设备:按键、触摸屏、鼠标、传感器等。

Linux内核为此专门设计了输入子系统(Input Subsystem) ,为所有输入设备提供统一的框架和用户空间接口。本文将以最常见的GPIO按键为例,完整展示一个输入设备驱动的开发过程:从设备树配置、中断申请,到按键事件的上报,最终用户空间可以通过标准的/dev/input/eventX节点读取按键事件。

你将掌握:

  • 输入子系统的架构与核心数据结构
  • 使用input_dev注册输入设备
  • 通过GPIO中断捕获按键动作
  • 使用input_report_keyinput_sync上报事件
  • 在用户空间通过evtest或直接读取input节点验证驱动

一、输入子系统简介

1.1 为什么需要输入子系统?

在没有统一框架的时代,每个输入设备驱动都要自己创建设备节点、定义数据格式、处理应用层的读取逻辑。这导致代码重复、接口不一致。输入子系统则解决了这些问题:

  • 统一设备节点 :所有输入设备都在/dev/input/下,应用程序只需打开/dev/input/eventX即可读取标准化的事件数据。
  • 标准事件格式 :使用struct input_event描述每个输入事件(类型、编码、值),支持键盘、鼠标、触摸屏等多种设备。
  • 自动设备发现与热插拔 :输入子系统与udev配合,自动创建设备文件,用户无需手动mknod
  • 丰富的辅助工具evtestinput-utils等可以方便地测试和调试。

1.2 核心数据结构和API

struct input_dev:代表一个输入设备,包含设备名称、支持的事件类型、事件编码等信息。

常用API

函数 作用
devm_input_allocate_device(dev) 分配并初始化input_dev
set_bit(EV_KEY, input_dev->evbit) 声明设备支持按键事件
set_bit(KEY_ENTER, input_dev->keybit) 声明支持的具体按键(如KEY_ENTER)
input_register_device(input_dev) 向输入子系统注册设备
input_report_key(input_dev, keycode, value) 报告按键状态(1按下,0释放)
input_sync(input_dev) 同步事件,表示一次完整上报结束

1.3 GPIO按键驱动的一般流程

  • 在设备树中描述按键使用的GPIO引脚及有效电平。
  • probe中获取GPIO描述符,映射为中断号(gpiod_to_irq)。
  • 申请中断,指定中断处理函数。
  • 创建input_dev,设置支持的事件类型和按键码。
  • 注册输入设备。
  • 中断处理函数中调用input_report_key上报按键状态,然后调用input_sync通知核心事件完成。

二、设计思路

本文以i.MX6ULL开发板上的一个用户按键(假设为KEY0,连接在GPIO1_IO18)为例。按键一端接GPIO,另一端接地,按下时引脚电平为低。因此:

  • 按下:低电平(0)
  • 释放:高电平(1)

中断触发方式选择双边沿触发IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING),这样按下和释放都能产生中断,我们可以在中断中根据当前电平判断状态并上报。

驱动的设备树节点使用自定义的compatible,以便与我们的驱动匹配,不会和内核自带的gpio-keys驱动冲突。

最终生成输入设备节点(例如/dev/input/event1),用户空间可通过evtest或直接读/dev/input/eventX获得按键事件。


三、设备树修改

在板级设备树中添加按键节点。使用GPIO1_IO18(即&gpio1 18),属性名为key-gpios,与驱动中的gpiod_get(dev, "key", ...)对应。

dts 复制代码
/ {
    gpio_key {
        compatible = "yourname,gpio-key";
        key-gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;  /* 低电平有效,按下时逻辑为1 */
        status = "okay";
    };
};

属性说明

  • compatible:自定义字符串,与驱动的of_match_table匹配。
  • key-gpios:指定GPIO引脚,GPIO_ACTIVE_LOW指示逻辑有效电平为低。gpiod API会自动处理电平翻转,调用gpiod_get_value时,按下(物理低)返回1,释放(物理高)返回0。

重新编译设备树并替换,重启开发板。


四、驱动代码实现

新建文件 gpio_key_drv.c,完整代码如下。本驱动不包含软件消抖,直接使用硬件消抖配合双边沿中断。

c 复制代码
/*
 * gpio_key_drv.c
 * GPIO按键输入设备驱动。
 * 基于platform_driver,使用gpiod API和输入子系统上报按键事件。
 * 加载后生成 /dev/input/eventX,可通过 evtest 或 cat 读取事件。
 * 作者:[你的ID]
 * 适配内核:Linux 5.x (4.x 亦可)
 * 参考开发板:i.MX6ULL
 */

#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>  /* gpiod API */
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/of.h>

static struct input_dev *key_input;       /* 输入设备结构体 */
static struct gpio_desc *key_gpio;        /* GPIO描述符 */
static int key_irq;                       /* 中断号 */

/* 中断处理函数(顶半部) */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    int val;

    /* 读取当前GPIO逻辑电平(已由gpiod自动处理极性) */
    val = gpiod_get_value(key_gpio);

    /* 上报按键状态:按下(val=1)报告1,释放(val=0)报告0 */
    input_report_key(key_input, KEY_ENTER, val ? 1 : 0);
    input_sync(key_input);

    pr_info("gpio_key: key %s, val=%d\n", val ? "pressed" : "released", val);
    return IRQ_HANDLED;
}

/* ---------------- platform_driver 部分 ---------------- */

static int gpio_key_probe(struct platform_device *pdev)
{
    int ret;
    struct device *dev = &pdev->dev;

    pr_info("gpio_key: probe called\n");

    /* 1. 获取GPIO描述符,con_id为"key",对应设备树属性"key-gpios" */
    key_gpio = gpiod_get(dev, "key", GPIOD_IN);
    if (IS_ERR(key_gpio)) {
        pr_err("gpio_key: failed to get key gpio\n");
        return PTR_ERR(key_gpio);
    }

    /* 2. 将GPIO引脚转换为中断号 */
    key_irq = gpiod_to_irq(key_gpio);
    if (key_irq < 0) {
        pr_err("gpio_key: gpiod_to_irq failed, err=%d\n", key_irq);
        ret = key_irq;
        goto err_get_irq;
    }
    pr_info("gpio_key: irq number = %d\n", key_irq);

    /* 3. 申请中断(双边沿触发) */
    ret = request_irq(key_irq, key_irq_handler,
                      IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                      "gpio_key", NULL);
    if (ret) {
        pr_err("gpio_key: request_irq failed, err=%d\n", ret);
        goto err_req_irq;
    }

    /* 4. 分配并初始化输入设备 */
    key_input = devm_input_allocate_device(dev);
    if (!key_input) {
        pr_err("gpio_key: input_allocate_device failed\n");
        ret = -ENOMEM;
        goto err_alloc_input;
    }

    key_input->name = "GPIO Key";
    key_input->phys = "gpio_key/input0";
    key_input->id.bustype = BUS_HOST;
    key_input->id.vendor = 0x0001;
    key_input->id.product = 0x0001;
    key_input->id.version = 0x0100;

    /* 设置支持的按键类型 */
    set_bit(EV_KEY, key_input->evbit);
    set_bit(KEY_ENTER, key_input->keybit);   /* 上报的按键码为KEY_ENTER */

    /* 5. 注册输入设备 */
    ret = input_register_device(key_input);
    if (ret) {
        pr_err("gpio_key: input_register_device failed, err=%d\n", ret);
        goto err_register_input;
    }

    pr_info("gpio_key: input device registered as /dev/input/eventX\n");
    return 0;

err_register_input:
    /* input_allocate_device 分配的内存由devm管理,无需手动释放 */
err_alloc_input:
    free_irq(key_irq, NULL);
err_req_irq:
err_get_irq:
    gpiod_put(key_gpio);
    return ret;
}

static int gpio_key_remove(struct platform_device *pdev)
{
    pr_info("gpio_key: remove called\n");

    free_irq(key_irq, NULL);
    /* devm_input_allocate_device 会自动注销 input_dev,无需手动调用 */
    gpiod_put(key_gpio);

    return 0;
}

/* 设备树匹配表 */
static const struct of_device_id gpio_key_of_match[] = {
    { .compatible = "yourname,gpio-key" },
    { }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);

static struct platform_driver gpio_key_driver = {
    .probe  = gpio_key_probe,
    .remove = gpio_key_remove,
    .driver = {
        .name           = "gpio_key",
        .owner          = THIS_MODULE,
        .of_match_table = gpio_key_of_match,
    },
};

module_platform_driver(gpio_key_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A GPIO key input device driver");
MODULE_VERSION("1.0");

代码关键点解析

  • gpiod_get(dev, "key", GPIOD_IN) :获取设备树中名为"key-gpios"的GPIO资源(con_id"key"),初始化为输入模式。
  • gpiod_to_irq :从GPIO描述符获取中断号,无需在设备树中显式声明interrupts
  • request_irq :触发标志设为IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,保证按下和释放都能产生中断。
  • 输入设备设置 :使用devm_input_allocate_device分配,设置名称和ID,并通过set_bit声明支持按键事件和KEY_ENTER码。
  • 中断处理函数gpiod_get_value返回逻辑电平(已自动处理ACTIVE_LOW),直接用作input_report_key的value。按下时报告1,释放时报告0。最后必须调用input_sync表示一次事件结束。
  • 资源释放devm_input_allocate_device分配的设备在驱动卸载时自动注销;中断和GPIO在remove中手动释放。probe错误路径使用goto逐级回滚。

关于消抖:本驱动未加入软件消抖,依赖硬件消抖和双边沿中断。如果按键抖动严重导致连续上报,可在中断中加入简易时间滤波(见后续文章)。


五、Makefile

makefile 复制代码
# Makefile for gpio_key

KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := gpio_key_drv.o

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean

交叉编译时设置ARCHCROSS_COMPILE


六、测试与验证

6.1 加载驱动

bash 复制代码
insmod gpio_key_drv.ko
dmesg | tail
# gpio_key: probe called
# gpio_key: irq number = xxx
# gpio_key: input device registered as /dev/input/eventX

6.2 确定输入设备节点

驱动加载后,通过以下方式找到对应的eventX

bash 复制代码
ls /dev/input/
# 对比加载前后的变化

# 或查看内核信息
dmesg | grep "input: GPIO Key"
# 例如输出:input: GPIO Key as /devices/platform/gpio_key/input/input1

也可用cat /proc/bus/input/devices,找到N: Name="GPIO Key"的行,其后的H: Handlers=...会显示eventX

6.3 使用evtest测试

bash 复制代码
evtest /dev/input/event1   # 替换为实际节点

按下和释放按键,终端会输出类似:

复制代码
Event: time 123456.789012, type 1 (EV_KEY), code 28 (KEY_ENTER), value 1
Event: time 123456.789012, -------------- SYN_REPORT ------------
Event: time 123456.890123, type 1 (EV_KEY), code 28 (KEY_ENTER), value 0
Event: time 123456.890123, -------------- SYN_REPORT ------------

6.4 直接读取event节点

若没有evtest,可用hexdump查看原始事件数据:

bash 复制代码
hexdump -C /dev/input/event1

按下按键时会输出16字节的行(struct input_event),可对照格式解析时间戳、类型、编码和值。

6.5 卸载驱动

bash 复制代码
rmmod gpio_key_drv

输入设备节点会自动消失,中断和GPIO被正确释放。


七、常见问题排查

  1. insmod后没有生成/dev/input/eventX

    • 检查dmesg中是否有input_register_device错误。
    • 确保内核配置启用了CONFIG_INPUT=yCONFIG_EVDEV=y(通用事件接口)。
  2. 按键事件不产生,中断计数不增加

    • cat /proc/interrupts | grep gpio_key查看中断触发次数。
    • 检查GPIO引脚是否与原理图一致,GPIO_ACTIVE_LOW是否正确。
    • 确认该GPIO未被他用(cat /sys/kernel/debug/gpio)。
  3. 按键出现多次事件(抖动)

    硬件消抖不足时可引入软件消抖,如在中断中记录上次触发时间,小于20ms则丢弃。进阶方法将在下一篇文章中介绍。

  4. gpiod_get(dev, "key", ...)失败

    请确保设备树属性名为key-gpios,且compatible字符串与驱动一致。


八、总结与下篇预告

本文成功将GPIO按键接入Linux输入子系统,通过标准/dev/input/eventX节点向用户空间上报按键事件。这也是我们首次在驱动中使用中断,中断是嵌入式驱动中最重要的异步通知机制。

下篇预告 :中断处理要求快速完成,耗时操作应推迟到底半部。下一篇我们将深入中断顶半部与底半部 的机制,使用tasklet和工作队列优化按键驱动,并加入软件消抖功能。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

相关推荐
A小辣椒3 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒7 小时前
TShark:基础知识
linux
AlfredZhao9 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言