kernel之gpio-keys驱动

kernel之gpio-keys驱动 这是Linux 内核中最经典、最通用的GPIO按键 / 开关驱动属于输入子系统InputSubsystem驱动专门用来把GPIO 硬件按键转换成标准的 Linux 输入事件比如键盘按键、开关状态供上层应用Android、Linux 桌面、嵌入式系统直接使用。驱动位置driver/input/keyboard/gpio-keys.c一、整体架构核心数据结构1.struct gpio_button_data单个按键管理一个按键的所有硬件 / 软件资源struct gpio_button_data { const struct gpio_keys_button *button; // 按键配置码值、GPIO、中断等 struct input_dev *input; // 输入设备 struct gpio_desc *gpiod; // GPIO 描述符 struct timer_list release_timer; // 纯中断按键的释放定时器 struct delayed_work work; // GPIO 按键的消抖工作队列 unsigned int irq; // 中断号 spinlock_t lock; // 并发保护 bool disabled; // 按键是否被禁用 bool key_pressed; // 按键是否按下 bool suspended; // 是否处于休眠 };2.struct gpio_keys_drvdata驱动全局管理整个驱动的所有资源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]; // 柔性数组所有按键 };二、驱动执行全流程probe - 运行 - suspend/resume第 1 步驱动加载与探测gpio_keys_initlate_initcall(gpio_keys_init); static int __init gpio_keys_init(void) { return platform_driver_register(gpio_keys_device_driver); }第 2 步设备探测gpio_keys_probe—— 核心入口获取配置优先从设备树解析按键配置gpio_keys_get_devtree_pdata没有设备树则用驱动默认数据分配内存分配全局驱动数据gpio_keys_drvdata分配按键映射表、柔性数组存储所有按键创建输入设备input devm_input_allocate_device(dev);初始化输入设备名称、物理路径、ID绑定open/close回调逐个初始化按键gpio_keys_setup_key这是最核心的硬件初始化函数获取GPIO从设备树或平台数据获取 GPIO配置为输入配置消抖硬件消抖优先失败则用软件消抖定时器 / 工作队列获取中断GPIO 转中断号注册中断处理函数GPIO 按键 →gpio_keys_gpio_isr纯中断按键 →gpio_keys_irq_isr设置输入能力告诉内核这个设备支持哪些按键注册唤醒源如果配置创建 SYSFS 接口在/sys/devices/platform/gpio-keys/下创建 4 个文件keys可读支持的按键列表switches可读支持的开关列表disabled_keys可读写禁用的按键disabled_switches可读写禁用的开关注册输入设备error input_register_device(input);注册成功后内核会生成/dev/input/eventX设备节点应用可以直接读取按键事件。第 3 步按键工作原理中断 - 消抖 - 上报事件驱动分两种按键处理逻辑模式 1标准 GPIO 按键流程按键按下/松开→GPIO 中断触发→中断处理函数→软件消抖→上报输入事件中断服务函数gpio_keys_gpio_isr不做耗时操作只调度延迟工作队列实现软件消抖延迟几十毫秒再处理工作队列函数gpio_keys_gpio_work_func读取 GPIO 真实电平调用gpio_keys_gpio_report_event上报事件事件上报state gpiod_get_value_cansleep(bdata-gpiod); input_event(input, type, *bdata-code, state); input_sync(input);input_event上报按键按下 / 松开input_sync告诉内核事件发送完成模式 2纯中断按键适用于无法读取电平、只有中断信号的按键中断触发 → 上报按下启动定时器 → 定时时间到 → 自动上报松开用定时器模拟按键释放第 4 步SYSFS 用户态接口动态禁用 / 启用按键驱动实现了一套完整的 sysfs 控制逻辑用户态可以查看支持的按键cat /sys/devices/platform/gpio-keys/keys动态禁用某个按键echo 116 /sys/devices/platform/gpio-keys/disabled_keys内核实现逻辑show函数gpio_keys_attr_show_helper→ 生成按键位图字符串store函数gpio_keys_attr_store_helper→ 解析用户输入调用gpio_keys_disable_button禁用中断gpio_keys_enable_button启用中断第 5 步电源管理休眠 / 唤醒驱动完整支持 Linux 电源管理休眠suspend如果该按键是唤醒源配置中断为唤醒触发否则关闭输入设备节省功耗唤醒resume恢复中断触发模式重新上报所有按键当前状态恢复输入设备唤醒源功能按键可以在系统休眠时唤醒系统比如手机电源键唤醒。三、配置Config 需要配置如下打开编译CONFIG_INPUT_KEYBOARDy CONFIG_KEYBOARD_GPIOm设备树配置gpio-keys { compatible gpio-keys; #address-cells 1; #size-cells 0; autorepeat; /* 可选启用按键重复功能 */ /* 在这里添加你的按键 */ key-reset { label Reset Button; linux,code KEY_RESTART; /* 或使用数值 143 */ gpios gpio1 15 GPIO_ACTIVE_LOW; debounce-interval 10; /* 防抖时间单位毫秒 */ wakeup-source; /* 可选作为唤醒源 */ }; };linux,code 在dt-bindings/input/linux-event-codes.h中查看属性描述是否必需compatible必须为 gpio-keys是label按键的描述名称推荐linux,code按键的Linux键码是gpiosGPIO引用和极性是GPIO按键interrupts中断号如果没有GPIO可选debounce-interval防抖时间(ms)否默认5mswakeup-source是否可以作为唤醒源否wakeup-event-action唤醒触发动作EV_ACT_ASSERTED/DEASSERTED/ANY否linux,input-type输入类型默认EV_KEY否linux,can-disable是否可以通过sysfs禁用否autorepeat在父节点启用按键重复否应用操作示例#include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include linux/input.h #include errno.h #include signal.h #include time.h #define KEY_EVENT_DEVICE /dev/input/event0 #define LONG_PRESS_TIME 3000 // 长按阈值毫秒 struct key_state { int code; struct timespec press_time; int is_pressed; int long_press_reported; // 防止重复报告长按 }; volatile int running 1; void signal_handler(int sig) { running 0; } // 获取时间差毫秒 int get_time_diff_ms(struct timespec *start, struct timespec *end) { return (end-tv_sec - start-tv_sec) * 1000 (end-tv_nsec - start-tv_nsec) / 1000000; } int main(int argc, char *argv[]) { struct input_event ev; struct key_state keys[256] {0}; // 支持最多256个按键状态 int fd; char device_path[64]; struct timespec current_time; // 设置信号处理 signal(SIGINT, signal_handler); if (argc 1) { snprintf(device_path, sizeof(device_path), /dev/input/event%s, argv[1]); } else { strcpy(device_path, KEY_EVENT_DEVICE); } fd open(device_path, O_RDONLY); if (fd 0) { printf(无法打开设备 %s: %s\n, device_path, strerror(errno)); return -1; } printf(开始监听按键事件...\n); printf(长按阈值: %d 毫秒\n, LONG_PRESS_TIME); printf(按下 CtrlC 退出\n\n); while (running) { // 设置select超时用于定期检查长按状态 fd_set readfds; struct timeval tv; FD_ZERO(readfds); FD_SET(fd, readfds); tv.tv_sec 0; tv.tv_usec 100000; // 100ms超时用于检查长按 int ret select(fd 1, readfds, NULL, NULL, tv); if (ret 0) { if (errno EINTR) continue; printf(select错误\n); break; } // 检查是否有新事件 if (ret 0 FD_ISSET(fd, readfds)) { ssize_t bytes read(fd, ev, sizeof(struct input_event)); if (bytes (ssize_t)sizeof(struct input_event)) { if (errno ! EINTR) { printf(读取事件失败: %s\n, strerror(errno)); break; } continue; } if (ev.type EV_KEY) { clock_gettime(CLOCK_MONOTONIC, current_time); if (ev.value 1) { // 按下 keys[ev.code].code ev.code; keys[ev.code].press_time current_time; keys[ev.code].is_pressed 1; keys[ev.code].long_press_reported 0; printf(按键 %3d: 按下\n, ev.code); } else if (ev.value 0) { // 释放 if (keys[ev.code].is_pressed) { int press_duration get_time_diff_ms(keys[ev.code].press_time, current_time); if (press_duration LONG_PRESS_TIME) { printf(按键 %3d: 长按释放 (持续时间: %d ms)\n, ev.code, press_duration); } else { printf(按键 %3d: 短按释放 (持续时间: %d ms)\n, ev.code, press_duration); } keys[ev.code].is_pressed 0; } } } } // 定期检查是否有按键处于长按状态超过阈值 clock_gettime(CLOCK_MONOTONIC, current_time); for (int i 0; i 256; i) { if (keys[i].is_pressed !keys[i].long_press_reported) { int press_duration get_time_diff_ms(keys[i].press_time, current_time); if (press_duration LONG_PRESS_TIME) { printf(按键 %3d: 检测到长按 (已持续 %d ms)\n, i, press_duration); keys[i].long_press_reported 1; // 标记已报告避免重复 } } } } close(fd); printf(\n程序退出\n); return 0; }