硬件:iMX6ULL ALPHA 开发板,KEY0 连接 GPIO1_IO18,低电平触发
内核:Linux 4.14
目标:以 platform driver + miscdevice + 中断消抖实现按键字符设备驱动
一、硬件说明
| 参数 | 值 |
|---|---|
| 引脚 | GPIO1_IO18(复用自 UART1_CTS_B) |
| 触发电平 | 低电平有效(按下 → 物理低,松开 → 物理高) |
| DTS 极性 | GPIO_ACTIVE_LOW |
| gpiod 逻辑值 | 1 = 按下,0 = 松开 |
二、整体架构与初始化流程
驱动初始化流程图
flowchart TD
A["DTS 节点<br/>compatible = alientek,key"] --> B[内核创建 platform_device]
B --> C{"of_device_id 匹配<br/>key_of_match"}
C -->|匹配成功| D[调用 key_probe]
C -->|无匹配| Z[跳过]
D --> E["devm_kzalloc<br/>分配 key_dev 结构体"]
E --> F["devm_gpiod_get<br/>申请 GPIO1_IO18(输入模式)"]
F --> G["gpiod_to_irq<br/>获取中断号"]
G --> H["spin_lock_init + setup_timer<br/>初始化自旋锁与消抖定时器"]
H --> I["devm_request_irq<br/>注册双边沿中断(FALLING|RISING)"]
I --> J["misc_register<br/>创建 /dev/key"]
J --> K["platform_set_drvdata<br/>保存 key_dev 指针"]
style A fill:#dae8fc
style J fill:#d5e8d4
style Z fill:#f8cecc三、中断消抖机制
机械按键按下/松开时,触点会在数毫秒内反复抖动,产生多次 GPIO 电平变化。若不消抖,单次按键会触发数十次中断。
本驱动的消抖方案:定时器延迟采样
按键抖动波形:
____ _____________________
|_|_|_||_____________________|
抖动区 稳定区(10ms 后采样)
中断触发:每次边沿都重启定时器(mod_timer)
定时器到期:GPIO 已稳定,读取一次作为最终键值
- 任意边沿(上升/下降)触发 ISR →
mod_timer重启 10ms 计时 - 抖动期间定时器不断被重置,不会到期
- 抖动结束后 10ms,定时器到期 →
key_timer_func采样 GPIO 确定最终值 - 键值存入
key_val,受自旋锁保护,供key_read随时读取
四、数据结构
struct key_dev {
struct gpio_desc *gpiod; /* GPIO 描述符,devm_ 自动释放 */
struct miscdevice miscdev; /* misc 设备,自动创建 /dev/key */
struct timer_list timer; /* 消抖定时器 */
spinlock_t lock; /* 保护 key_val 的自旋锁 */
unsigned int irqnum; /* GPIO 中断号 */
unsigned char key_val; /* 消抖后键值:1=按下 0=松开 */
};
数据结构类图
classDiagram
class key_dev {
+gpio_desc *gpiod
+miscdevice miscdev
+timer_list timer
+spinlock_t lock
+unsigned int irqnum
+unsigned char key_val
}
class miscdevice {
+int minor
+const char *name
+file_operations *fops
}
class file_operations {
+open()
+read()
}
class gpio_desc {
<<kernel内部>>
+GPIO1_IO18
+极性: ACTIVE_LOW
}
class timer_list {
<<kernel内部>>
+function: key_timer_func
+expires: jiffies+10ms
}
class file {
+void *private_data
}
key_dev *-- miscdevice : 内嵌
key_dev *-- gpio_desc : 持有引用(devm_)
key_dev *-- timer_list : 内嵌(消抖)
miscdevice --> file_operations : fops 指向
file ..> miscdevice : ①misc框架设 private_data=miscdev
file ..> key_dev : ②key_open改 private_data=key_dev相比 Beep 驱动,key_dev 多了 timer、lock、irqnum、key_val 四个字段,用于支持中断 + 消抖的异步更新机制。
五、gpiod API 说明
与 Beep 驱动相同,本驱动使用 gpiod API,GPIO_ACTIVE_LOW 使驱动代码无需关心物理极性:
gpiod_get_value(gpiod)返回逻辑值:按下 = 1,松开 = 0devm_gpiod_get(&pdev->dev, "key", GPIOD_IN)对应 DTS 属性key-gpiogpiod_to_irq(gpiod)替代旧版gpio_to_irq(gpio)
详细对比见 Beep_Platform驱动开发 § 五。
六、DTS 配置
/* imxplore.dts */
/ {
key {
compatible = "alientek,key";
pinctrl-names = "default";
pinctrl-0 = <&key_pins>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
/* key_pins 已在 imx6ul-14x14-evk.dts 中定义 */
/* MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 */
GPIO1_IO18 复用自 UART1_CTS_B,若内核启用了 uart1 且声明了同一 pad 的 pinctrl,
会与 key 节点冲突(probe failed -22)。遇到此问题需在 uart1 节点中删除对应 pinctrl。
七、关键代码说明
中断→消抖→读取 完整时序图
sequenceDiagram
participant App as 用户态 App
participant VFS as VFS 层
participant Misc as misc 框架
participant Drv as key 驱动
participant HW as GPIO 硬件
participant Timer as 消抖定时器
Note over App,Timer: ① 打开设备
App ->> VFS : open("/dev/key")
VFS ->> Misc : misc_open()
Misc ->> Misc : filp->private_data = &miscdev
Misc ->> Drv : key_open()
Drv ->> Drv : container_of → key_dev
Drv ->> Drv : filp->private_data = key_dev
Drv -->> App : fd 返回
Note over App,Timer: ② 按键按下(异步,与 read 独立)
HW ->> Drv : 下降沿中断 → key_irq_handler
Drv ->> Timer: mod_timer(+10ms) 启动消抖
Note over HW,Timer: 抖动期间中断持续触发,定时器不断重置
Timer ->> Drv : 10ms 到期 → key_timer_func
Drv ->> HW : gpiod_get_value() = 1(逻辑高=按下)
Drv ->> Drv : spin_lock → key_val = 1 → spin_unlock
Note over App,Timer: ③ App 读取键值
App ->> VFS : read(fd, buf, 1)
VFS ->> Drv : key_read()
Drv ->> Drv : spin_lock → val = key_val → spin_unlock
Drv ->> App : copy_to_user(val=1) → 返回 1
Note over App,Timer: ④ 按键松开(异步)
HW ->> Drv : 上升沿中断 → key_irq_handler
Drv ->> Timer: mod_timer(+10ms)
Timer ->> Drv : 10ms 到期 → key_timer_func
Drv ->> HW : gpiod_get_value() = 0(逻辑低=松开)
Drv ->> Drv : spin_lock → key_val = 0 → spin_unlock关键设计:中断更新
key_val与 App 调用read完全异步,
自旋锁保证两者访问key_val时的数据一致性。
7.1 open:container_of 获取 key_dev
static int key_open(struct inode *inode, struct file *filp)
{
struct miscdevice *miscdev = filp->private_data; /* misc 框架设置 */
struct key_dev *dev = container_of(miscdev, struct key_dev, miscdev);
filp->private_data = dev; /* 覆写为 key_dev,后续 read 直接用 */
return 0;
}
7.2 key_irq_handler + key_timer_func:消抖核心
static irqreturn_t key_irq_handler(int irq, void *data)
{
struct key_dev *dev = data;
/* 任意边沿均重启定时器,抖动期间定时器不断被推迟 */
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(KEY_DEBOUNCE_MS));
return IRQ_HANDLED;
}
static void key_timer_func(unsigned long arg)
{
struct key_dev *dev = (struct key_dev *)arg;
unsigned long flags;
/* 定时器到期:GPIO 已稳定,读取最终逻辑值 */
int val = gpiod_get_value(dev->gpiod); /* 1=按下 0=松开 */
spin_lock_irqsave(&dev->lock, flags);
dev->key_val = (unsigned char)val;
spin_unlock_irqrestore(&dev->lock, flags);
}
7.3 key_read:返回消抖后的键值
static ssize_t key_read(struct file *filp, char __user *buf,
size_t size, loff_t *offset)
{
struct key_dev *dev = filp->private_data;
unsigned char val;
unsigned long flags;
if (size < 1)
return 0;
spin_lock_irqsave(&dev->lock, flags);
val = dev->key_val;
spin_unlock_irqrestore(&dev->lock, flags);
if (copy_to_user(buf, &val, 1))
return -EFAULT;
return 1; /* 成功:返回读取字节数 */
}
八、用户态测试程序
测试程序位于 app/key_test/key_test.c:
key_test # 轮询模式:打印 press/release 事件,Ctrl+C 退出
key_test once # 读一次当前键值(1=按下 0=松开)
key_test wait # 阻塞,等待第一次按下后退出(适合脚本联动)
测试输出示例(poll 模式):
Polling key events (Ctrl+C to exit)...
[KEY] pressed
[KEY] released
[KEY] pressed
[KEY] released
九、与 Beep 驱动的对比
| 对比项 | Beep 驱动 | Key 驱动 |
|---|---|---|
| 操作方向 | 输出(控制) | 输入(采集) |
| GPIO 方向 | GPIOD_OUT_LOW | GPIOD_IN |
| 中断 | 无 | 双边沿中断 + 消抖定时器 |
| file_operations | open + write | open + read |
| 并发保护 | 无需 | spinlock 保护 key_val |
| 资源管理 | devm_gpiod_get / misc_register | 同上,额外 del_timer_sync |
关联笔记
- Beep_Platform驱动开发 — 同框架对比:输出类驱动,无中断
- LED_Platform驱动开发 — 手动 cdev 注册方式,可与 miscdevice 对比
- 平台设备驱动模型 — platform driver 完整框架
- iMX6ULL环境数据采集站 — 按键是采集站项目第一阶段组件
延伸阅读
传统按键字符设备实现可与 Platform 模型和 Input 子系统方案对照学习。
- 返回嵌入式 Linux 学习路线,确认本文在完整知识结构中的位置。
- Linux Platform 设备驱动模型
- Linux Input 子系统详解




