一、驱动概述
本驱动是对旧版 key.c(miscdevice 方案)的重构版本,使用 Linux Input 子系统
替代自定义字符设备,实现同一硬件的两种驱动方式对比学习。
| 对比维度 | 旧版 key.c | 新版 key_input.c |
|---|---|---|
| 框架 | miscdevice + file_operations | Input 子系统 |
| 用户接口 | /dev/key,返回自定义 0/1 | /dev/input/eventX,标准 input_event |
| 数据获取 | 用户轮询(sleep + read 循环) | 内核上报,用户 read() 阻塞等待 |
| 工具兼容 | 仅自己的 app | evtest、Android InputReader 直接可用 |
| 驱动切换 | DTS compatible = "alientek,key" | DTS compatible = "alientek,key-input" |
- 硬件平台:iMX6ULL ALPHA,GPIO1_IO18,低有效(GPIO_ACTIVE_LOW)
- 内核版本:4.14
二、整体架构与初始化流程
2.1 软件层次
用户空间:read(/dev/input/eventX) → struct input_event
↓ 阻塞,有事件时内核唤醒
evdev 层:evdev_handler 接收 input_dev 上报的事件,写入环形缓冲
↓
Input 核心:input_handle_event 分发事件
↓ input_report_key + input_sync
驱动层: 防抖定时器到期 → gpiod_get_value → input_report_key
↑ mod_timer(防抖)
IRQ 上半部:GPIO 边沿中断
↑
硬件: GPIO1_IO18 按键
2.2 Probe 初始化流程
flowchart TD
A[key_input_probe 入口] --> B[devm_kzalloc<br/>分配 key_input_dev]
B --> C[devm_gpiod_get<br/>获取 key GPIO 描述符]
C -->|失败| D[return PTR_ERR]
C -->|成功| E[devm_input_allocate_device<br/>分配 input_dev]
E --> F[设置 idev->name<br/>idev->dev.parent]
F --> G["input_set_capability<br/>(EV_KEY, KEY_0)<br/>声明支持的事件能力"]
G --> H[input_register_device<br/>注册,创建 /dev/input/eventX]
H -->|失败| I[return ret]
H -->|成功| J["setup_timer<br/>初始化防抖定时器"]
J --> K["gpiod_to_irq<br/>GPIO → IRQ 编号"]
K --> L["devm_request_irq<br/>FALLING + RISING 双沿"]
L -->|失败| M[return ret]
L -->|成功| N["platform_set_drvdata<br/>probe ok"]三、数据结构
classDiagram
class key_input_dev {
+gpio_desc *gpiod
+input_dev *idev
+timer_list timer
+unsigned int irqnum
}
class input_dev {
+char *name
+unsigned long evbit[]
+unsigned long keybit[]
+struct device dev
}
class gpio_desc {
+GPIO1_IO18
+ACTIVE_LOW
}
class timer_list {
+key_input_timer()
+expires
}
class UserSpace {
<<用户空间>>
+/dev/input/eventX
+struct input_event
+read() 阻塞
}
key_input_dev --> input_dev : .idev
key_input_dev --> gpio_desc : .gpiod
key_input_dev --> timer_list : .timer
input_dev ..> UserSpace : Input核心分发事件四、关键代码说明
4.1 完整事件流时序图
sequenceDiagram
participant HW as 硬件<br/>GPIO1_IO18
participant IRQ as IRQ 上半部<br/>key_input_irq
participant TMR as 防抖定时器<br/>key_input_timer
participant INP as Input 核心
participant EVD as evdev
participant USR as 用户空间<br/>read(eventX)
HW->>IRQ: GPIO 下降沿(按下)
IRQ->>TMR: mod_timer(+10ms)
Note over IRQ: 只做 mod_timer<br/>立即返回
HW->>IRQ: 抖动边沿(忽略)
IRQ->>TMR: mod_timer 重置
TMR->>TMR: 10ms 到期执行
TMR->>HW: gpiod_get_value()
HW-->>TMR: 1(逻辑高=按下)
TMR->>INP: input_report_key(KEY_0, 1)
TMR->>INP: input_sync()
INP->>EVD: 分发 EV_KEY + EV_SYN
EVD->>USR: 唤醒阻塞的 read()
USR-->>USR: 处理 input_event
HW->>IRQ: GPIO 上升沿(松开)
IRQ->>TMR: mod_timer(+10ms)
TMR->>HW: gpiod_get_value()
HW-->>TMR: 0(逻辑低=松开)
TMR->>INP: input_report_key(KEY_0, 0)
TMR->>INP: input_sync()
INP->>EVD: 分发 EV_KEY + EV_SYN
EVD->>USR: 唤醒阻塞的 read()4.2 核心代码段
声明事件能力:
dev->idev->name = "key-input";
dev->idev->dev.parent = &pdev->dev;
/* 声明支持 EV_KEY 类型中的 KEY_0 键码
* Input 核心只会向上层传递已声明的事件,未声明的会被过滤 */
input_set_capability(dev->idev, EV_KEY, KEY_0);
ret = input_register_device(dev->idev);
防抖定时器上报事件:
static void key_input_timer(unsigned long arg)
{
struct key_input_dev *dev = (struct key_input_dev *)arg;
int val = gpiod_get_value(dev->gpiod); /* 1=按下,0=松开 */
input_report_key(dev->idev, KEY_0, val); /* 上报键值 */
input_sync(dev->idev); /* 提交,触发用户 read 返回 */
}
IRQ 上半部(极简):
static irqreturn_t key_input_irq(int irq, void *data)
{
struct key_input_dev *dev = data;
/* 只做防抖延时,不在中断上下文读 GPIO */
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(KEY_DEBOUNCE_MS));
return IRQ_HANDLED;
}
remove 时等待定时器:
static int key_input_remove(struct platform_device *pdev)
{
struct key_input_dev *dev = platform_get_drvdata(pdev);
/* del_timer_sync:等待正在执行的定时器回调结束
* 防止 remove 返回后回调访问已释放的 idev 指针 */
del_timer_sync(&dev->timer);
return 0;
}
五、DTS 配置
key {
compatible = "alientek,key-input";
/* 改为 "alientek,key" 可切换回旧版 misc 驱动 */
pinctrl-names = "default";
pinctrl-0 = <&key_pins>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
};
两套驱动切换只需改一行 compatible,无需修改任何代码:
"alientek,key-input" → 加载 key_input.ko → /dev/input/eventX
"alientek,key" → 加载 key.ko → /dev/key
六、测试验证
6.1 加载驱动
insmod /lib/modules/key_input.ko
dmesg | tail -3
# 期望:key-input probe ok (irq=49)
ls /dev/input/
# 出现 event0 或 event1
cat /sys/class/input/event0/device/name
# 输出:key-input
6.2 测试 App
key_input_test # 自动查找设备,阻塞等待
key_input_test /dev/input/event0 # 指定路径
输出示例:
Opening: /dev/input/event0
Waiting for key events (Ctrl+C to exit)...
[1234.567890] KEY_11 PRESSED
[1234.678901] KEY_11 RELEASED
6.3 使用 evtest(如果板子上有)
evtest /dev/input/event0
七、旧版 vs 新版代码对比
用户空间读取方式对比
旧版(miscdevice):
/* 必须主动轮询,浪费 CPU */
while (1) {
read(fd, &val, sizeof(val)); /* 立即返回,不管有没有事件 */
if (val != last_val) {
printf("key %sn", val ? "pressed" : "released");
last_val = val;
}
usleep(20000); /* 必须手动 sleep 降低 CPU 占用 */
}
新版(Input 子系统):
/* read() 阻塞,内核有事件才返回,零轮询 */
while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
if (ev.type == EV_KEY)
printf("key %sn", ev.value ? "pressed" : "released");
}
内核驱动代码量对比
| 部分 | 旧版 key.c | 新版 key_input.c |
|---|---|---|
| 数据结构 | miscdevice + file_operations + spinlock | input_dev(无需 lock) |
| open/read/release | 需实现 | 不需要,Input 核心处理 |
| 上报数据 | 存入 key_val,等 read 取走 | input_report_key + input_sync |
| 代码总行数 | ~180 行 | ~120 行 |
八、文件索引
drivers/key_input/
├── key_input.c ← Input 子系统驱动
└── Makefile
app/key_input_test/
├── key_input_test.c ← 测试应用(自动查找设备,阻塞读取)
└── Makefile
kernel4.14/arch/arm/boot/dts/
└── imxplore.dts ← key 节点 compatible 改为 "alientek,key-input"
参考
- 19_Linux_Input子系统详解 — Input 子系统通用知识
- 14_Key_Platform驱动开发 — 旧版 misc 按键驱动(对比参考)
drivers/input/keyboard/gpio_keys.c— 内核标准 GPIO 按键驱动
延伸阅读
新版 Input 按键驱动适合与子系统原理和旧版 miscdevice 实现一起比较。
- 返回嵌入式 Linux 学习路线,确认本文在完整知识结构中的位置。
- Linux Input 子系统详解
- Key Platform 驱动开发(中断与定时器消抖)




