--- 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,配合上拉电阻构成RC低通),滤除高频毛刺 2. **软件消抖**:检测到电平变化后延时10~20ms,再次读取GPIO确认状态 3. **定时器消抖**:使用内核定时器(`timer_list`)或工作队列,在中断触发后延迟处理 4. **INPUT子系统消抖**:内核INPUT框架内置debounce机制 > [!note] 注意 > 原书第四十九章的示例**只用原子变量保护按键值,未实现消抖,也没有使用定时器**(定时器消抖 + INPUT子系统是原书第五十八章《Linux INPUT子系统实验》的内容)。实际按键驱动应使用INPUT子系统或定时器消抖;无消抖时,按一次按键可能输出多行。 --- ## 2. 按键设备树配置 ### 2.1 设备树节点格式 设备树中按键节点包含三个部分:pinctrl配置、GPIO指定、兼容性字符串。 ### 2.2 添加pinctrl节点 打开 `imx6ull-alientek-emmc.dts`,在 `iomuxc` 节点的 `imx6ul-evk` 子节点下创建 `pinctrl_key`: ```dts 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` 节点: ```dts 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 编译与验证 ```bash make dtbs # 重新编译设备树 ``` 启动后验证节点是否存在: ```bash ls /proc/device-tree/key/ ``` 若存在则说明设备树修改成功。 --- ## 3. 按键驱动源码分析 ### 3.1 头文件与宏定义 ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define KEY_CNT 1 /* 设备号个数 */ #define KEY_NAME "key" /* 名字 */ #define KEY0VALUE 0XF0 /* 按键值 */ #define INVAKEY 0X00 /* 无效的按键值 */ ``` ### 3.2 设备结构体 ```c 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初始化函数 ```c 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函数 ```c 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函数(核心) ```c 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结构体 ```c static struct file_operations key_fops = { .owner = THIS_MODULE, .open = key_open, .read = key_read, }; ``` ### 3.7 驱动入口函数 ```c 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 驱动出口函数 ```c 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源码 ```c #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子系统按键驱动 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第四十九章内容(第四十九章仅使用原子变量 + `read` 轮询),为扩展知识。其中的定时器消抖 + INPUT子系统上报方式对应原书**第五十八章《Linux 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子系统按键驱动完整示例 ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; /* 4.1.15内核使用静态定义的timer_list */ unsigned char curkey; struct input_dev *input; /* input子系统设备 */ }; static struct key_input_dev keyinputdev; /* 定时器处理:消抖后读取GPIO * 注意:4.1.15内核的定时器回调形参为 unsigned long, * 4.15及以上内核才改为 struct timer_list * + from_timer()。 */ static void key_timer_handler(unsigned long arg) { unsigned char value; struct key_input_dev *dev = (struct key_input_dev *)arg; 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); /* 获取中断号(原书使用 irq_of_parse_and_map 从设备树解析) */ keyinputdev.irq = irq_of_parse_and_map(keyinputdev.nd, 0); /* 初始化定时器(4.1.15内核用init_timer,非timer_setup) */ init_timer(&keyinputdev.timer); keyinputdev.timer.function = key_timer_handler; keyinputdev.timer.data = (unsigned long)&keyinputdev; /* 申请中断 */ 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) { int ret; /* 先申请并初始化input设备,再初始化GPIO/中断,避免中断先于input就绪 */ keyinputdev.input = input_allocate_device(); if (!keyinputdev.input) { return -ENOMEM; } keyinputdev.input->name = KEY_NAME; __set_bit(EV_KEY, keyinputdev.input->evbit); __set_bit(EV_REP, keyinputdev.input->evbit); input_set_capability(keyinputdev.input, EV_KEY, KEY_0); ret = input_register_device(keyinputdev.input); if (ret) { input_free_device(keyinputdev.input); return ret; } ret = keyio_init(); if (ret) return ret; return 0; } static void __exit keyinput_exit(void) { del_timer_sync(&keyinputdev.timer); free_irq(keyinputdev.irq, &keyinputdev); gpio_free(keyinputdev.key_gpio); input_unregister_device(keyinputdev.input); input_free_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 ```c /* 按下事件 */ 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` 事件,表示一次完整的输入事件已完成。 ### 5.6 4.1.15内核 timer_list API(原书第五十章) 原书第五十章《Linux内核定时器实验》给出 4.1.15 内核的定时器接口签名,示例如下: | 函数 | 原型 | 说明 | | ---------------- | ---------------------------------------------------------------- | --------------------------------------------------------- | | `init_timer` | `void init_timer(struct timer_list *timer)` | 初始化定时器(新版内核已改为 `timer_setup`) | | `add_timer` | `void add_timer(struct timer_list *timer)` | 向内核注册并启动定时器 | | `mod_timer` | `int mod_timer(struct timer_list *timer, unsigned long expires)` | 修改超时时间;未激活时会激活,可用于周期性定时 | | `del_timer` | `int del_timer(struct timer_list *timer)` | 删除定时器(可在中断上下文调用) | | `del_timer_sync` | `int del_timer_sync(struct timer_list *timer)` | 同步版删除,等待其他CPU上的回调退出,不能在中断上下文调用 | 关键点(4.1.15 与 4.15+ 的差异): - **回调原型**:4.1.15 为 `void (*function)(unsigned long)`;4.15+ 改为 `void (*function)(struct timer_list *)` 并配合 `from_timer()` 取回私有数据。 - **私有数据**:4.1.15 通过 `timer.data = (unsigned long)dev;` 传递,回调内强转 `(struct key_input_dev *)arg`。 - **时间换算**:内核 `CONFIG_HZ=100`(`HZ=100`),使用 `msecs_to_jiffies(ms)`、`jiffies_to_msecs()` 换算;`jiffies + msecs_to_jiffies(15)` 即 15ms 后超时。 - **静态 vs 动态**:本示例定时器是全局/结构体成员,用 `init_timer()`;动态分配时同样先 `init_timer()` 再赋值 `function`/`data`。 > [!note] 设备树中断 > 使用 `irq_of_parse_and_map(nd, 0)` 时,设备树节点中必须有 `interrupt-parent` 与 `interrupts` 属性;若节点只有 `key-gpio` 而无 `interrupts`,则需改用 `gpio_to_irq()`。 --- ## 6. 实验验证 ### 6.1 编译方法 **驱动编译**(Makefile中设置 `obj-m := key.o`): ```bash make -j32 # 生成 key.ko ``` **测试APP编译**: ```bash arm-linux-gnueabihf-gcc keyApp.c -o keyApp ``` ### 6.2 加载与测试 ```bash # 将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 卸载驱动 ```bash rmmod key.ko ``` --- ## 7. 跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 | 特性 | 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. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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_FALLING` 和 `IRQF_TRIGGER_RISING` 各表示什么? **答**:设备树中通过 `interrupts` 属性指定中断号和触发方式。驱动中使用 `irq_of_parse_and_map()`(原书第五十八章做法)或 `gpio_to_irq()` 将节点/GPIO 转换为中断号,再用 `request_irq()` 申请中断。`IRQF_TRIGGER_FALLING` 表示下降沿触发(高→低,对应按键按下),`IRQF_TRIGGER_RISING` 表示上升沿触发(低→高,对应按键释放)。两者同时指定则双沿触发,可用于检测按键的按下和释放两个动作。 --- **代码来源**:正点原子I.MX6U嵌入式Linux驱动开发指南 V2.0.1(第四十九章) **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第49章 Linux按键输入实验 **最后更新**: 2026-09-17