1. GPIO按键驱动概述
在嵌入式Linux系统中,GPIO按键是最基础也是最常用的人机交互方式之一。gpio-keys驱动作为Linux内核的标准组件,负责将物理按键的GPIO电平变化转换为标准的输入事件,使得上层应用可以统一处理各种输入设备。
这个驱动属于Linux输入子系统(Input Subsystem)的一部分,主要功能包括:
- 将GPIO电平变化转换为标准的输入事件(EV_KEY)
- 提供按键消抖功能,防止机械抖动导致的误触发
- 支持通过sysfs动态配置按键状态
- 实现电源管理,支持系统唤醒功能
驱动源码位于内核的drivers/input/keyboard/gpio-keys.c,自2.6内核时代就已存在,经过多年演进已经非常稳定成熟。
2. 驱动架构与核心数据结构
2.1 单个按键管理结构(struct gpio_button_data)
这个结构体管理一个物理按键的所有资源:
c复制struct gpio_button_data {
const struct gpio_keys_button *button; // 按键配置
struct input_dev *input; // 关联的输入设备
struct gpio_desc *gpiod; // GPIO描述符
struct timer_list release_timer; // 释放定时器(纯中断模式)
struct delayed_work work; // 工作队列(用于消抖)
unsigned int irq; // 中断号
spinlock_t lock; // 自旋锁(保护并发访问)
bool disabled; // 按键是否被禁用
bool key_pressed; // 当前按下状态
bool suspended; // 是否处于休眠状态
};
每个字段都有其特定作用:
button:存储来自设备树或平台数据的配置信息work:用于实现软件消抖的延迟工作队列release_timer:纯中断模式下模拟按键释放的定时器key_pressed:记录当前按键状态,避免重复上报
2.2 驱动全局管理结构(struct gpio_keys_drvdata)
这个结构体管理整个驱动的全局资源:
c复制struct gpio_keys_drvdata {
const struct gpio_keys_platform_data *pdata; // 平台配置数据
struct input_dev *input; // 输入设备实例
struct mutex disable_lock; // 互斥锁(保护禁用操作)
unsigned short *keymap; // 键码映射表
struct gpio_button_data data[0]; // 柔性数组(存储所有按键)
};
关键设计点:
pdata:包含所有按键的配置信息,可以来自设备树或静态定义data[0]:使用柔性数组动态管理不定数量的按键disable_lock:保护按键禁用/启用操作的互斥锁
3. 驱动初始化流程
3.1 驱动加载入口(gpio_keys_init)
驱动通过标准的platform_driver机制注册:
c复制late_initcall(gpio_keys_init);
static int __init gpio_keys_init(void)
{
return platform_driver_register(&gpio_keys_device_driver);
}
使用late_initcall确保驱动在系统启动后期加载,此时GPIO和中断子系统已初始化完成。
3.2 设备探测(gpio_keys_probe)
这是驱动的核心初始化函数,主要流程如下:
3.2.1 获取配置数据
c复制pdata = gpio_keys_get_devtree_pdata(dev);
if (!pdata) {
dev_err(dev, "无法获取平台数据\n");
return -EINVAL;
}
驱动优先从设备树获取配置,如果没有则使用平台数据。设备树是现代Linux系统的标准配置方式。
3.2.2 分配驱动数据结构
c复制drvdata = devm_kzalloc(dev, struct_size(drvdata, data, nbuttons), GFP_KERNEL);
使用devm_系列函数分配内存,可以自动管理资源释放。
3.2.3 创建输入设备
c复制input = devm_input_allocate_device(dev);
input->name = pdata->name ? : "gpio-keys";
input->phys = "gpio-keys/input0";
input->dev.parent = dev;
input->open = gpio_keys_open;
input->close = gpio_keys_close;
设置输入设备的基本信息和回调函数,open/close用于电源管理。
3.2.4 初始化各个按键(gpio_keys_setup_key)
这是最复杂的部分,主要步骤包括:
- GPIO获取与配置
c复制gpiod = gpiod_get_index(dev, NULL, i, GPIOD_IN);
获取GPIO并配置为输入模式,支持active-low极性。
- 中断配置
c复制irq = gpiod_to_irq(gpiod);
ret = request_any_context_irq(irq, isr_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
desc, bdata);
将GPIO映射为中断,并注册中断处理函数。使用IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING捕获按键按下和释放事件。
- 消抖处理
c复制if (button->debounce_interval) {
gpiod_set_debounce(gpiod, button->debounce_interval * 1000);
} else {
INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
}
优先使用硬件消抖,如果不支持则初始化工作队列用于软件消抖。
- 唤醒功能配置
c复制if (button->wakeup) {
device_init_wakeup(dev, true);
irq_set_irq_wake(irq, true);
}
如果按键配置为唤醒源,则设置相应的唤醒功能。
3.2.5 注册输入设备
c复制ret = input_register_device(input);
注册成功后,会在/dev/input/下生成对应的设备节点,如event0。
4. 按键事件处理机制
4.1 标准GPIO按键模式
这是最常用的模式,处理流程如下:
- 中断触发
c复制static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
schedule_delayed_work(&bdata->work,
msecs_to_jiffies(bdata->software_debounce));
return IRQ_HANDLED;
}
中断处理函数只调度工作队列,不做耗时操作。
- 工作队列处理
c复制static void gpio_keys_gpio_work_func(struct work_struct *work)
{
state = gpiod_get_value_cansleep(bdata->gpiod);
gpio_keys_gpio_report_event(bdata);
}
在工作队列中读取GPIO实际电平并上报事件。
- 事件上报
c复制input_event(input, EV_KEY, button->code, state);
input_sync(input);
使用标准的输入子系统接口上报按键事件。
4.2 纯中断按键模式
适用于只有中断信号没有GPIO的按键:
- 中断处理
c复制static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
mod_timer(&bdata->release_timer,
jiffies + msecs_to_jiffies(bdata->software_debounce));
input_event(input, EV_KEY, bdata->button->code, 1);
input_sync(input);
}
中断触发时立即上报按下事件,并启动释放定时器。
- 定时器回调
c复制static void gpio_keys_irq_timer(struct timer_list *t)
{
input_event(bdata->input, EV_KEY, bdata->button->code, 0);
input_sync(bdata->input);
}
定时器超时后自动上报释放事件。
5. 设备树配置详解
标准的设备树配置示例如下:
dts复制gpio-keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
autorepeat;
power-key {
label = "Power Button";
linux,code = <KEY_POWER>;
gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
debounce-interval = <10>;
wakeup-source;
};
volume-up {
label = "Volume Up";
linux,code = <KEY_VOLUMEUP>;
gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;
};
};
关键属性说明:
| 属性 | 描述 | 是否必须 |
|---|---|---|
| compatible | 必须为"gpio-keys" | 是 |
| linux,code | 按键键码,如KEY_POWER(116) | 是 |
| gpios | GPIO引用和极性 | GPIO按键必须 |
| interrupts | 中断号(纯中断按键) | 纯中断按键必须 |
| debounce-interval | 消抖时间(ms) | 否,默认5ms |
| wakeup-source | 是否作为唤醒源 | 否 |
| linux,input-type | 输入事件类型 | 否,默认EV_KEY |
| autorepeat | 启用按键重复 | 否 |
提示:键码定义在
include/uapi/linux/input-event-codes.h中,常用的有:
- KEY_POWER(116):电源键
- KEY_VOLUMEUP(115):音量加
- KEY_VOLUMEDOWN(114):音量减
- KEY_HOME(102):主页键
6. 用户空间接口
6.1 Sysfs控制接口
驱动在/sys/devices/platform/gpio-keys/下提供以下文件:
keys:只读,显示支持的按键列表disabled_keys:读写,控制按键禁用状态switches:只读,支持的开关列表disabled_switches:读写,控制开关禁用状态
使用示例:
bash复制# 查看支持的按键
cat /sys/devices/platform/gpio-keys/keys
# 禁用KEY_POWER(116)
echo 116 > /sys/devices/platform/gpio-keys/disabled_keys
# 重新启用
echo "" > /sys/devices/platform/gpio-keys/disabled_keys
6.2 输入事件读取
应用程序可以通过/dev/input/eventX设备读取按键事件,示例代码:
c复制struct input_event ev;
int fd = open("/dev/input/event0", O_RDONLY);
while (1) {
read(fd, &ev, sizeof(ev));
if (ev.type == EV_KEY) {
printf("按键 %d: %s\n",
ev.code,
ev.value ? "按下" : "释放");
}
}
7. 电源管理实现
7.1 休眠处理(suspend)
c复制static int gpio_keys_suspend(struct device *dev)
{
if (device_may_wakeup(dev)) {
enable_irq_wake(bdata->irq);
} else {
disable_irq(bdata->irq);
}
}
对于唤醒源按键,启用中断唤醒;其他按键则禁用中断节省功耗。
7.2 唤醒处理(resume)
c复制static int gpio_keys_resume(struct device *dev)
{
if (device_may_wakeup(dev)) {
disable_irq_wake(bdata->irq);
} else {
enable_irq(bdata->irq);
}
// 重新上报当前状态
gpio_keys_report_state(bdata);
}
恢复中断设置并重新上报按键状态,确保系统唤醒后状态一致。
8. 实际开发经验
8.1 常见问题排查
- 按键无响应
- 检查GPIO配置是否正确,特别是极性(GPIO_ACTIVE_HIGH/LOW)
- 确认设备树节点已正确加载:
cat /proc/device-tree/gpio-keys - 检查中断是否注册成功:
cat /proc/interrupts
- 按键抖动严重
- 增加消抖时间:
debounce-interval = <20>; - 确保硬件设计有适当的RC滤波电路
- 唤醒功能失效
- 确认内核配置了CONFIG_PM_SLEEP
- 检查设备树是否设置了
wakeup-source - 验证GPIO支持唤醒功能
8.2 性能优化建议
- 中断处理优化
- 对于高频按键,考虑使用threaded IRQ减少中断延迟
- 避免在中断上下文中进行耗时操作
- 电源管理优化
- 非唤醒按键在休眠时完全禁用
- 使用
gpiod_get_value_cansleep替代轮询方式
- 资源管理
- 使用
devm_系列函数自动管理资源 - 合理设置柔性数组大小,避免内存浪费
8.3 调试技巧
- 查看输入设备信息
bash复制cat /proc/bus/input/devices
- 监控输入事件
bash复制evtest /dev/input/event0
- 调试GPIO状态
bash复制cat /sys/kernel/debug/gpio
- 查看设备树节点
bash复制dtc -I fs /proc/device-tree
9. 内核配置与编译
9.1 必要内核配置
config复制CONFIG_INPUT=y
CONFIG_INPUT_KEYBOARD=y
CONFIG_KEYBOARD_GPIO=y
9.2 编译为模块
config复制CONFIG_KEYBOARD_GPIO=m
加载模块:
bash复制modprobe gpio_keys
9.3 调试配置
config复制CONFIG_INPUT_EVDEV=y
CONFIG_INPUT_EVBUG=m # 仅调试使用
CONFIG_DEBUG_GPIO=y
10. 高级应用场景
10.1 组合键实现
通过修改驱动可以支持组合键检测:
c复制static void check_combo_keys(struct gpio_keys_drvdata *ddata)
{
if (key1_pressed && key2_pressed) {
input_event(input, EV_KEY, KEY_COMBO, 1);
input_sync(input);
// 添加适当的延时
input_event(input, EV_KEY, KEY_COMBO, 0);
input_sync(input);
}
}
10.2 长按检测
在用户空间实现长按检测的示例:
c复制struct timespec press_time;
if (ev.value == 1) { // 按下
clock_gettime(CLOCK_MONOTONIC, &press_time);
} else { // 释放
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long duration = (now.tv_sec - press_time.tv_sec) * 1000 +
(now.tv_nsec - press_time.tv_nsec) / 1000000;
if (duration > 1000) {
printf("长按检测: %d ms\n", duration);
}
}
10.3 按键重定义
通过sysfs动态修改键码:
c复制static ssize_t keymap_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
unsigned int index, code;
if (sscanf(buf, "%u %u", &index, &code) == 2) {
drvdata->keymap[index] = code;
}
return count;
}
11. 驱动开发注意事项
- 并发控制
- 使用自旋锁保护中断上下文的数据访问
- 使用互斥锁保护sysfs操作
- 电源管理
- 正确处理suspend/resume回调
- 合理管理唤醒源
- 错误处理
- 检查所有资源分配结果
- 实现适当的回滚逻辑
- 性能考虑
- 避免在中断上下文中进行耗时操作
- 合理设置消抖时间(通常10-20ms)
- 兼容性
- 同时支持设备树和平台数据
- 遵循Linux输入子系统标准
在实际项目中,gpio-keys驱动已经非常成熟,大多数情况下不需要修改驱动本身,只需正确配置设备树即可。但对于特殊需求,如组合键、长按检测等,可能需要对驱动进行适当扩展。