title: Linux按键输入驱动 tags: [嵌入式Linux, 驱动开发, GPIO输入, 按键驱动, INPUT子系统, 设备树] created: 2026-09-17 updated: 2026-09-17
嵌入式开发板上的按键通常采用低电平有效的电路设计:按键一端接GPIO引脚,另一端接地。GPIO引脚通过上拉电阻接VCC,未按下时为高电平(逻辑1),按下时接地为低电平(逻辑0)。
VCC ──┬── R(上拉) ──── GPIO Pin
│
[SW] ──────── GND
│
GND
I.MX6U-ALPHA开发板的KEY0使用 UART1_CTS_B 这个PIN,复用为 GPIO1_IO18。
按键驱动与LED驱动原理相同,都是操作GPIO。区别在于:
gpio_direction_output)gpio_direction_input + gpio_get_value)在驱动程序中使用一个整型变量(原子变量)表示按键值,应用程序通过 read() 读取按键值,判断按键是否按下。
机械按键在按下和释放瞬间会产生抖动(bounce),持续时间约5~20ms。抖动期间GPIO电平会快速跳变,导致一次按键被误判为多次。
消抖方案:
timer_list)或工作队列,在中断触发后延迟处理[!note] 注意 本章基础示例不包含软件消抖,实际按键驱动应使用INPUT子系统或定时器消抖。无消抖时,按一次按键可能输出多行。
设备树中按键节点包含三个部分:pinctrl配置、GPIO指定、兼容性字符串。
打开 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_IO180xF080:引脚电气属性(驱动强度、上拉等)在根节点 / 下创建 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";
};
| 属性 | 说明 |
|---|---|
compatible |
匹配字符串,驱动中用于设备匹配 |
pinctrl-names |
pinctrl状态名称 |
pinctrl-0 |
引用pinctrl节点 |
key-gpio |
自定义属性,指定使用的GPIO控制器和引脚号 |
GPIO_ACTIVE_LOW |
低电平有效标志 |
修改设备树前必须检查:
UART1_CTS_B 是否被其他pinctrl节点使用,若有则屏蔽GPIO1_IO18 是否被其他外设使用,若有则屏蔽make dtbs # 重新编译设备树
启动后验证节点是否存在:
ls /proc/device-tree/key/
若存在则说明设备树修改成功。
#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 /* 无效的按键值 */
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编号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为输入模式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。
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;
}
逐行解析:
gpio_get_value():读取GPIO电平,低电平表示按键按下while(!gpio_get_value()):阻塞等待按键释放,防止重复读取atomic_set():原子操作写入按键值copy_to_user():将按键值从内核空间拷贝到用户空间[!warning] 问题 该实现没有软件消抖,按一次可能输出多行。实际项目应加入延时消抖或使用INPUT子系统。
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
};
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;
}
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");
#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;
}
用户空间 内核空间
│ │
├─ 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)
[!tip] 推荐方式 实际项目中按键驱动应使用Linux INPUT子系统,它专门处理输入设备事件。
用户空间: /dev/input/eventX (evdev)
↓
内核层: evdev.c → Input Core → input-handlers
↓
驱动层: input_report_key() / input_sync()
↓
硬件层: GPIO中断 → 按键事件
| 函数 | 说明 |
|---|---|
input_allocate_device() |
分配input_dev结构体 |
input_register_device() |
注册输入设备到内核 |
input_set_capability() |
声明设备支持的事件类型 |
input_report_key() |
上报按键事件(按下/释放) |
input_sync() |
同步事件,通知内核一次完整事件已就绪 |
input_free_device() |
释放input_dev |
#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");
按键按下时刻 定时器触发(15ms后) GPIO稳定值
│ │ │
↓ ↓ ↓
├─── 毛刺 ───┤─── 毛刺 ───┤─── 稳定低电平
│ │ │
└── mod_timer() ─────┘ │
└── 读取GPIO → input_report_key()
input_report_key() 上报事件/* 按下事件 */
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 事件,表示一次完整的输入事件已完成。
驱动编译(Makefile中设置 obj-m := key.o):
make -j32 # 生成 key.ko
测试APP编译:
arm-linux-gnueabihf-gcc keyApp.c -o keyApp
# 将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工具测试
# 无消抖方式
KEY0 Press, value = 0XF0 # 可能连续输出多行
# INPUT子系统方式
input: key as /devices/virtual/input/input0
rmmod key.ko
| 特性 | 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 |
关键差异:
答:按键值是内核与用户空间的共享资源。驱动写入(atomic_set)、应用读取(copy_to_user)可能并发发生。使用原子变量保证操作的原子性,避免数据竞争。在SMP系统中,普通变量的读-改-写操作可能被中断或其他CPU打断,导致数据不一致。原子操作(atomic_set/atomic_read)在ARM架构上使用 ldrex/strex 指令实现无锁保护。
gpio_get_value 返回值0表示按下还是1表示按下?为什么?答:取决于电路设计。大多数开发板采用低电平有效设计:按键一端接GPIO,另一端接地,GPIO通过上拉电阻接VCC。未按下时GPIO为高(1),按下时接地为低(0)。因此 gpio_get_value() == 0 表示按下。设备树中 GPIO_ACTIVE_LOW 属性也表明低电平有效。
答:机械按键按下/释放瞬间,金属触点弹性振动产生毛刺,GPIO电平在高/低之间快速跳变(5~20ms)。驱动的 key_read() 在 while(!gpio_get_value()) 等待释放后立即读取,毛刺被误判为新的按键事件。解决方案:(1) 定时器消抖,中断触发后延迟15ms再读GPIO;(2) INPUT子系统,内核已集成debounce框架。
input_sync() 的作用是什么?不调用会怎样?答:input_sync() 发送 EV_SYN/SYN_REPORT 事件,告知INPUT核心"一次完整输入事件已就绪"。不调用的话,内核不会处理之前上报的事件,用户空间的应用程序(如evtest、触摸屏驱动)无法接收到按键事件。INPUT框架以事件对为单位处理:report_key + sync = 一个完整事件。
IRQF_TRIGGER_FALLING 和 IRQF_TRIGGER_RISING 各表示什么?答:设备树中通过 interrupts 属性指定中断号和触发方式。驱动中使用 gpio_to_irq() 将GPIO转换为中断号,再用 request_irq() 申请中断。IRQF_TRIGGER_FALLING 表示下降沿触发(高→低,对应按键按下),IRQF_TRIGGER_RISING 表示上升沿触发(低→高,对应按键释放)。两者同时指定则双沿触发,可用于检测按键的按下和释放两个动作。