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和工作队列优化按键驱动,并加入软件消抖功能。敬请期待!


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

相关推荐
风曦Kisaki6 小时前
# Linux运维Day06:HAproxy负载均衡(代理调度软件对比)、Tomcat服务部署与LNMJ架构
linux·运维·负载均衡
largecode6 小时前
座机号码认证如何操作?申请热线实名名片,树立统一官方客服形象
linux·sql·华为·c#·.net·wpf·harmonyos
杨云龙UP6 小时前
ODA/Oracle RAC 节点 Load 100+ 排查:一个 lsof 残留进程引发的负载虚高问题 2026-05-27
linux·数据库·oracle·centos·误操作
底层开发智库6 小时前
获取编译并运行ARM64可信系统软件栈(TF-A+OP-TEE+UEFI+Linux)
linux·optee·arm64·可信软件
用户2367829801687 小时前
Linux curl 命令深度解析:从 HTTP 请求到网络调试实战
linux
怎么没有名字注册了啊7 小时前
fedora 换源教程
linux·运维·服务器
爱莉希雅&&&7 小时前
Zabbix监控linux服务器和Windows服务器
linux·运维·服务器·zabbix·监控
nLif7 小时前
基于FUSE的文件系统过滤驱动开发方法
驱动开发
小小测试开发7 小时前
加州拟将 Linux 从年龄验证法中豁免:一场开源社区的胜利与反思
linux·运维·开源