Key Platform 驱动开发(中断与定时器消抖) 文章封面

Key Platform 驱动开发(中断与定时器消抖)

硬件: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,松开 = 0
  • devm_gpiod_get(&pdev->dev, "key", GPIOD_IN) 对应 DTS 属性 key-gpio
  • gpiod_to_irq(gpiod) 替代旧版 gpio_to_irq(gpio)

详细对比见 Beep_Platform驱动开发 § 五。


六、DTS 配置

/* imxplore.dts */
/ {
    key {
        compatible = "alientek,key";
        pinctrl-names = "default";
        pinctrl-0 = &lt;&amp;key_pins&gt;;
        key-gpio = &lt;&amp;gpio1 18 GPIO_ACTIVE_LOW&gt;;
        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-&gt;private_data;  /* misc 框架设置 */
    struct key_dev *dev = container_of(miscdev, struct key_dev, miscdev);
    filp-&gt;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(&amp;dev-&gt;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-&gt;gpiod);  /* 1=按下 0=松开 */

    spin_lock_irqsave(&amp;dev-&gt;lock, flags);
    dev-&gt;key_val = (unsigned char)val;
    spin_unlock_irqrestore(&amp;dev-&gt;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-&gt;private_data;
    unsigned char val;
    unsigned long flags;

    if (size &lt; 1)
        return 0;

    spin_lock_irqsave(&amp;dev-&gt;lock, flags);
    val = dev-&gt;key_val;
    spin_unlock_irqrestore(&amp;dev-&gt;lock, flags);

    if (copy_to_user(buf, &amp;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_LOWGPIOD_IN
中断无双边沿中断 + 消抖定时器
file_operationsopen + writeopen + read
并发保护无需spinlock 保护 key_val
资源管理devm_gpiod_get / misc_register同上,额外 del_timer_sync

关联笔记

延伸阅读

传统按键字符设备实现可与 Platform 模型和 Input 子系统方案对照学习。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部