02-按键驱动.md 21 KB


title: Linux按键输入驱动 tags: [嵌入式Linux, 驱动开发, GPIO输入, 按键驱动, INPUT子系统, 设备树] created: 2026-09-17 updated: 2026-09-17

pdf_ref: "55_第四十九章 Linux按键输入实验.pdf"

Linux按键输入驱动

1. 按键驱动概述

1.1 按键硬件电路

嵌入式开发板上的按键通常采用低电平有效的电路设计:按键一端接GPIO引脚,另一端接地。GPIO引脚通过上拉电阻接VCC,未按下时为高电平(逻辑1),按下时接地为低电平(逻辑0)。

VCC ──┬── R(上拉) ──── GPIO Pin
       │
      [SW] ──────── GND
       │
      GND

I.MX6U-ALPHA开发板的KEY0使用 UART1_CTS_B 这个PIN,复用为 GPIO1_IO18

1.2 GPIO输入检测原理

按键驱动与LED驱动原理相同,都是操作GPIO。区别在于:

  • LED驱动:GPIO输出高低电平(gpio_direction_output
  • 按键驱动:GPIO读取高低电平(gpio_direction_input + gpio_get_value

在驱动程序中使用一个整型变量(原子变量)表示按键值,应用程序通过 read() 读取按键值,判断按键是否按下。

1.3 消抖动原理

机械按键在按下和释放瞬间会产生抖动(bounce),持续时间约5~20ms。抖动期间GPIO电平会快速跳变,导致一次按键被误判为多次。

消抖方案

  1. 硬件消抖:在GPIO引脚并联电容(100nF~100nF),滤除高频毛刺
  2. 软件消抖:检测到电平变化后延时10~20ms,再次读取GPIO确认状态
  3. 定时器消抖:使用内核定时器(timer_list)或工作队列,在中断触发后延迟处理
  4. INPUT子系统消抖:内核INPUT框架内置debounce机制

[!note] 注意 本章基础示例不包含软件消抖,实际按键驱动应使用INPUT子系统或定时器消抖。无消抖时,按一次按键可能输出多行。


2. 按键设备树配置

2.1 设备树节点格式

设备树中按键节点包含三个部分:pinctrl配置、GPIO指定、兼容性字符串。

2.2 添加pinctrl节点

打开 imx6ull-alientek-emmc.dts,在 iomuxc 节点的 imx6ul-evk 子节点下创建 pinctrl_key

pinctrl_key: keygrp {
    fsl,pins = <
        MX6UL_PAD_UART1_CTS_B__GPIO1_IO18  0xF080  /* KEY0 */
    >;
};
  • MX6UL_PAD_UART1_CTS_B__GPIO1_IO18:将UART1_CTS_B引脚复用为GPIO1_IO18
  • 0xF080:引脚电气属性(驱动强度、上拉等)

2.3 添加KEY设备节点

在根节点 / 下创建 key 节点:

key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-key";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;  /* KEY0 */
    status = "okay";
};

2.4 GPIO属性说明

属性 说明
compatible 匹配字符串,驱动中用于设备匹配
pinctrl-names pinctrl状态名称
pinctrl-0 引用pinctrl节点
key-gpio 自定义属性,指定使用的GPIO控制器和引脚号
GPIO_ACTIVE_LOW 低电平有效标志

2.5 检查PIN冲突

修改设备树前必须检查:

  1. UART1_CTS_B 是否被其他pinctrl节点使用,若有则屏蔽
  2. GPIO1_IO18 是否被其他外设使用,若有则屏蔽

2.6 编译与验证

make dtbs          # 重新编译设备树

启动后验证节点是否存在:

ls /proc/device-tree/key/

若存在则说明设备树修改成功。


3. 按键驱动源码分析

3.1 头文件与宏定义

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEY_CNT          1       /* 设备号个数 */
#define KEY_NAME         "key"   /* 名字 */

#define KEY0VALUE        0XF0    /* 按键值 */
#define INVAKEY          0X00    /* 无效的按键值 */

3.2 设备结构体

struct key_dev {
    dev_t devid;                /* 设备号 */
    struct cdev cdev;           /* cdev */
    struct class *class;        /* 类 */
    struct device *device;      /* 设备 */
    int major;                  /* 主设备号 */
    int minor;                  /* 次设备号 */
    struct device_node *nd;     /* 设备节点 */
    int key_gpio;               /* key所使用的GPIO编号 */
    atomic_t keyvalue;          /* 按键值 */
};

struct key_dev keydev;          /* key设备 */

关键成员说明

  • atomic_t keyvalue:原子变量,记录按键值,驱动写入/应用读取,需原子操作保护
  • key_gpio:从设备树获取的GPIO编号

3.3 GPIO初始化函数

static int keyio_init(void)
{
    keydev.nd = of_find_node_by_path("/key");
    if (keydev.nd == NULL) {
        return -EINVAL;
    }

    keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
    if (keydev.key_gpio < 0) {
        printk("can't get key0\r\n");
        return -EINVAL;
    }
    printk("key_gpio=%d\r\n", keydev.key_gpio);

    gpio_request(keydev.key_gpio, "key0");     /* 请求IO */
    gpio_direction_input(keydev.key_gpio);     /* 设置为输入 */
    return 0;
}

逐行解析

  • of_find_node_by_path("/key"):按路径查找设备树节点
  • of_get_named_gpio(nd, "key-gpio", 0):从设备树属性中获取GPIO编号
  • gpio_request():申请GPIO,防止重复占用
  • gpio_direction_input():设置GPIO为输入模式

3.4 驱动open函数

static int key_open(struct inode *inode, struct file *filp)
{
    int ret = 0;
    filp->private_data = &keydev;   /* 设置私有数据 */
    ret = keyio_init();             /* 初始化按键IO */
    if (ret < 0) {
        return ret;
    }
    return 0;
}

每次应用层 open() 打开设备文件时,都会重新初始化按键GPIO。

3.5 驱动read函数(核心)

static ssize_t key_read(struct file *filp, char __user *buf,
                        size_t cnt, loff_t *offt)
{
    int ret = 0;
    unsigned char value;
    struct key_dev *dev = filp->private_data;

    if (gpio_get_value(dev->key_gpio) == 0) {   /* key0按下 */
        while(!gpio_get_value(dev->key_gpio));   /* 等待按键释放 */
        atomic_set(&dev->keyvalue, KEY0VALUE);
    } else {                                     /* 无效的按键值 */
        atomic_set(&dev->keyvalue, INVAKEY);
    }

    value = atomic_read(&dev->keyvalue);         /* 保存按键值 */
    ret = copy_to_user(buf, &value, sizeof(value));
    return ret;
}

逐行解析

  1. gpio_get_value():读取GPIO电平,低电平表示按键按下
  2. while(!gpio_get_value())阻塞等待按键释放,防止重复读取
  3. atomic_set():原子操作写入按键值
  4. copy_to_user():将按键值从内核空间拷贝到用户空间

[!warning] 问题 该实现没有软件消抖,按一次可能输出多行。实际项目应加入延时消抖或使用INPUT子系统。

3.6 file_operations结构体

static struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .open = key_open,
    .read = key_read,
};

3.7 驱动入口函数

static int __init mykey_init(void)
{
    atomic_set(&keydev.keyvalue, INVAKEY);  /* 初始化原子变量 */

    /* 1、创建设备号 */
    if (keydev.major) {
        keydev.devid = MKDEV(keydev.major, 0);
        register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
    } else {
        alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);
        keydev.major = MAJOR(keydev.devid);
        keydev.minor = MINOR(keydev.devid);
    }

    /* 2、初始化cdev */
    keydev.cdev.owner = THIS_MODULE;
    cdev_init(&keydev.cdev, &key_fops);

    /* 3、添加一个cdev */
    cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);

    /* 4、创建类 */
    keydev.class = class_create(THIS_MODULE, KEY_NAME);
    if (IS_ERR(keydev.class)) {
        return PTR_ERR(keydev.class);
    }

    /* 5、创建设备 */
    keydev.device = device_create(keydev.class, NULL, keydev.devid,
                                  NULL, KEY_NAME);
    if (IS_ERR(keydev.device)) {
        return PTR_ERR(keydev.device);
    }
    return 0;
}

3.8 驱动出口函数

static void __exit mykey_exit(void)
{
    gpio_free(keydev.key_gpio);
    cdev_del(&keydev.cdev);
    unregister_chrdev_region(keydev.devid, KEY_CNT);
    device_destroy(keydev.class, keydev.devid);
    class_destroy(keydev.class);
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

4. 按键测试程序

4.1 测试APP源码

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

#define KEY0VALUE        0XF0
#define INVAKEY          0X00

int main(int argc, char *argv[])
{
    int fd, ret;
    char *filename;
    unsigned char keyvalue;

    if(argc != 2) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开key驱动 */
    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("file %s open failed!\r\n", argv[1]);
        return -1;
    }

    /* 循环读取按键值数据 */
    while(1) {
        read(fd, &keyvalue, sizeof(keyvalue));
        if (keyvalue == KEY0VALUE) {
            printf("KEY0 Press, value = %#X\r\n", keyvalue);
        }
    }

    ret = close(fd);
    if(ret < 0) {
        printf("file %s close failed!\r\n", argv[1]);
        return -1;
    }
    return 0;
}

4.2 调用流程

用户空间                         内核空间
  │                                │
  ├─ open("/dev/key") ──────────→ key_open()
  │                                ├─ filp->private_data = &keydev
  │                                └─ keyio_init()
  │                                   ├─ of_find_node_by_path()
  │                                   ├─ of_get_named_gpio()
  │                                   ├─ gpio_request()
  │                                   └─ gpio_direction_input()
  │                                │
  ├─ read(fd, &keyvalue, 1) ────→ key_read()
  │                                ├─ gpio_get_value()
  │                                ├─ atomic_set()
  │                                └─ copy_to_user()
  │                                │
  └─ close(fd) ─────────────────→ (文件引用计数-1)

5. INPUT子系统按键驱动

[!tip] 推荐方式 实际项目中按键驱动应使用Linux INPUT子系统,它专门处理输入设备事件。

5.1 INPUT子系统架构

用户空间: /dev/input/eventX (evdev)
    ↓
内核层: evdev.c → Input Core → input-handlers
    ↓
驱动层: input_report_key() / input_sync()
    ↓
硬件层: GPIO中断 → 按键事件

5.2 关键API

函数 说明
input_allocate_device() 分配input_dev结构体
input_register_device() 注册输入设备到内核
input_set_capability() 声明设备支持的事件类型
input_report_key() 上报按键事件(按下/释放)
input_sync() 同步事件,通知内核一次完整事件已就绪
input_free_device() 释放input_dev

5.3 INPUT子系统按键驱动完整示例

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <asm/uaccess.h>

#define KEY_NAME        "key"
#define KEY0VALUE       0XF0
#define INVAKEY         0X00
#define DEBOUNCE_TIME   15  /* 消抖时间15ms */

struct key_input_dev {
    struct device_node *nd;
    int key_gpio;
    int irq;
    struct timer_list timer;
    unsigned char curkey;
};

static struct key_input_dev keyinputdev;

/* 定时器中断处理:消抖后读取GPIO */
static void key_timer_handler(struct timer_list *t)
{
    unsigned char value;
    struct key_input_dev *dev = from_timer(dev, t, timer);

    value = gpio_get_value(dev->key_gpio);
    if (value == 0) {
        /* 按键按下事件 */
        input_report_key(dev->input, KEY_0, 1);
        input_sync(dev->input);
    } else {
        /* 按键释放事件 */
        input_report_key(dev->input, KEY_0, 0);
        input_sync(dev->input);
    }
}

/* GPIO中断处理函数 */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_input_dev *dev = dev_id;

    /* 启动定时器消抖 */
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME));
    return IRQ_RETVAL(IRQ_HANDLED);
}

static int keyio_init(void)
{
    int ret;

    keyinputdev.nd = of_find_node_by_path("/key");
    if (keyinputdev.nd == NULL) {
        return -EINVAL;
    }

    keyinputdev.key_gpio = of_get_named_gpio(keyinputdev.nd, "key-gpio", 0);
    if (keyinputdev.key_gpio < 0) {
        printk("can't get key0 gpio\r\n");
        return -EINVAL;
    }

    gpio_request(keyinputdev.key_gpio, "key0");
    gpio_direction_input(keyinputdev.key_gpio);

    /* 获取中断号 */
    keyinputdev.irq = gpio_to_irq(keyinputdev.key_gpio);

    /* 初始化定时器 */
    timer_setup(&keyinputdev.timer, key_timer_handler, 0);

    /* 申请中断 */
    ret = request_irq(keyinputdev.irq, key_irq_handler,
                      IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                      "key0", &keyinputdev);
    if (ret) {
        gpio_free(keyinputdev.key_gpio);
        return ret;
    }

    return 0;
}

static int __init keyinput_init(void)
{
    keyio_init();

    /* 注册input设备 */
    keyinputdev.input = input_allocate_device();
    if (!keyinputdev.input) {
        return -ENOMEM;
    }

    keyinputdev.input->name = KEY_NAME;
    input_set_capability(keyinputdev.input, EV_KEY, KEY_0);

    ret = input_register_device(keyinputdev.input);
    if (ret) {
        input_free_device(keyinputdev.input);
        return ret;
    }

    return 0;
}

static void __exit keyinput_exit(void)
{
    free_irq(keyinputdev.irq, &keyinputdev);
    del_timer_sync(&keyinputdev.timer);
    gpio_free(keyinputdev.key_gpio);
    input_unregister_device(keyinputdev.input);
}

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("embedded");

5.4 消抖实现原理

按键按下时刻        定时器触发(15ms后)       GPIO稳定值
    │                    │                     │
    ↓                    ↓                     ↓
    ├─── 毛刺 ───┤─── 毛刺 ───┤─── 稳定低电平
    │                    │                     │
    └── mod_timer() ─────┘                     │
                                              └── 读取GPIO → input_report_key()
  1. GPIO下降沿触发中断
  2. 中断处理函数中启动定时器(15ms)
  3. 15ms后定时器回调读取GPIO电平
  4. 此时按键已稳定,读取值可靠
  5. 通过 input_report_key() 上报事件

5.5 input_report_key与input_sync

/* 按下事件 */
input_report_key(dev->input, KEY_0, 1);  // 事件类型EV_KEY, 键码KEY_0, 值1=按下
input_sync(dev->input);                   // 同步,通知框架处理

/* 释放事件 */
input_report_key(dev->input, KEY_0, 0);  // 值0=释放
input_sync(dev->input);

input_sync() 必须在每次事件后调用,它向INPUT核心发送 EV_SYN / SYN_REPORT 事件,表示一次完整的输入事件已完成。


6. 实验验证

6.1 编译方法

驱动编译(Makefile中设置 obj-m := key.o):

make -j32          # 生成 key.ko

测试APP编译

arm-linux-gnueabihf-gcc keyApp.c -o keyApp

6.2 加载与测试

# 将key.ko和keyApp拷贝到开发板
cp key.ko keyApp /rootfs/lib/modules/4.1.15/

# 开发板上执行
cd /lib/modules/4.1.15
depmod                    # 首次加载需运行
modprobe key.ko           # 加载驱动

# 测试
./keyApp /dev/key

# 查看按键事件(INPUT子系统方式)
cat /proc/bus/input/devices    # 查看输入设备
evtest /dev/input/event0       # 用evtest工具测试

6.3 预期结果

# 无消抖方式
KEY0 Press, value = 0XF0       # 可能连续输出多行

# INPUT子系统方式
input: key as /devices/virtual/input/input0

6.4 卸载驱动

rmmod key.ko

7. 跨平台对比

特性 I.MX6ULL STM32(MP1) RK3568
GPIO库 gpio_request/gpio_direction_input HAL_GPIO_ReadPin gpio_request/gpio_get_value
设备树 imx6ull-alientek-emmc.dts STM32Cube生成 rk3568-evb.dts
中断方式 request_irq + GPIO中断 NVIC外部中断 request_irq + GPIO中断
消抖实现 INPUT子系统内置 硬件消抖+定时器 INPUT子系统内置
输入框架 INPUT子系统 HAL库直读 INPUT子系统
调试工具 /proc/interrupts, evtest CubeMX调试 /proc/interrupts, evtest

关键差异

  • STM32通常不使用设备树(裸机/HAL库方式),STM32MP1 Linux需设备树
  • I.MX6ULL和RK3568都使用标准Linux框架,设备树+INPUT子系统方式一致
  • 消抖:Linux平台推荐INPUT子系统内置debounce,STM32常用硬件RC滤波

8. 面试精选

Q1: 为什么按键驱动要用原子变量而不是普通int?

:按键值是内核与用户空间的共享资源。驱动写入(atomic_set)、应用读取(copy_to_user)可能并发发生。使用原子变量保证操作的原子性,避免数据竞争。在SMP系统中,普通变量的读-改-写操作可能被中断或其他CPU打断,导致数据不一致。原子操作(atomic_set/atomic_read)在ARM架构上使用 ldrex/strex 指令实现无锁保护。

Q2: gpio_get_value 返回值0表示按下还是1表示按下?为什么?

:取决于电路设计。大多数开发板采用低电平有效设计:按键一端接GPIO,另一端接地,GPIO通过上拉电阻接VCC。未按下时GPIO为高(1),按下时接地为低(0)。因此 gpio_get_value() == 0 表示按下。设备树中 GPIO_ACTIVE_LOW 属性也表明低电平有效。

Q3: 无消抖的按键驱动为什么按一次会输出多行?

:机械按键按下/释放瞬间,金属触点弹性振动产生毛刺,GPIO电平在高/低之间快速跳变(5~20ms)。驱动的 key_read()while(!gpio_get_value()) 等待释放后立即读取,毛刺被误判为新的按键事件。解决方案:(1) 定时器消抖,中断触发后延迟15ms再读GPIO;(2) INPUT子系统,内核已集成debounce框架。

Q4: input_sync() 的作用是什么?不调用会怎样?

input_sync() 发送 EV_SYN/SYN_REPORT 事件,告知INPUT核心"一次完整输入事件已就绪"。不调用的话,内核不会处理之前上报的事件,用户空间的应用程序(如evtest、触摸屏驱动)无法接收到按键事件。INPUT框架以事件对为单位处理:report_key + sync = 一个完整事件。

Q5: 如何在设备树中配置GPIO中断?IRQF_TRIGGER_FALLINGIRQF_TRIGGER_RISING 各表示什么?

:设备树中通过 interrupts 属性指定中断号和触发方式。驱动中使用 gpio_to_irq() 将GPIO转换为中断号,再用 request_irq() 申请中断。IRQF_TRIGGER_FALLING 表示下降沿触发(高→低,对应按键按下),IRQF_TRIGGER_RISING 表示上升沿触发(低→高,对应按键释放)。两者同时指定则双沿触发,可用于检测按键的按下和释放两个动作。