2. 按键中断驱动

任务说明

本案例实现一个基于GPIO中断的按键驱动,目标是在按键按下(下降沿触发)时,通过中断处理函数上报按键事件。驱动采用Linux字符设备框架,中断处理函数中使用底半部机制(工作队列)上报事件,避免在中断上下文执行耗时操作。用户空间程序通过读取设备节点获取按键事件。

硬件连接

SoC GPIO Controller GPIO IRQ GIC KEY SW1 10KΩ 上拉 3.3V GND GPIO1_13 (下降沿触发) 连接说明 信号线 地线

按键一端连接GPIO1_13,另一端接地。GPIO内部使能上拉电阻,按键按下时引脚电平由高变低,产生下降沿中断。

驱动代码实现


#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of_gpio.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/workqueue.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/sched.h>

#define KEY_EVENT_PRESSED  0x01
#define KEY_EVENT_RELEASED 0x00

struct key_event {
    unsigned int code;   /* 按键编号 */
    unsigned int value;  /* 0=释放 1=按下 */
};

struct key_drv_data {
    int gpio;
    int irq;
    struct work_struct work;
    struct key_event event;
    struct miscdevice mdev;
    wait_queue_head_t wq;
    struct key_event last_event;
    int available;
};

static struct key_drv_data *g_key_data;

static void key_work_handler(struct work_struct *work)
{
    struct key_drv_data *kdata = container_of(work, struct key_drv_data, work);
    int val = gpio_get_value(kdata->gpio);

    /* 读取当前电平,判断按下或释放 */
    if (val == 0) {
        kdata->event.code = 1;
        kdata->event.value = KEY_EVENT_PRESSED;
    } else {
        kdata->event.code = 1;
        kdata->event.value = KEY_EVENT_RELEASED;
    }

    /* 保存事件并唤醒读取进程 */
    kdata->last_event = kdata->event;
    kdata->available = 1;
    wake_up_interruptible(&kdata->wq);
}

static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    struct key_drv_data *kdata = dev_id;

    /* 调度工作队列处理实际事件上报 */
    schedule_work(&kdata->work);
    return IRQ_HANDLED;
}

static ssize_t key_read(struct file *file, char __user *buf,
                        size_t count, loff_t *ppos)
{
    struct key_drv_data *kdata = g_key_data;
    int ret;

    if (count < sizeof(struct key_event))
        return -EINVAL;

    /* 等待事件可用 */
    ret = wait_event_interruptible(kdata->wq, kdata->available == 1);
    if (ret)
        return ret;

    /* 拷贝事件到用户空间 */
    if (copy_to_user(buf, &kdata->last_event, sizeof(struct key_event)))
        return -EFAULT;

    kdata->available = 0;
    return sizeof(struct key_event);
}

static struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .read = key_read,
};

static int key_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct key_drv_data *kdata;
    int ret;

    kdata = devm_kzalloc(dev, sizeof(*kdata), GFP_KERNEL);
    if (!kdata)
        return -ENOMEM;

    g_key_data = kdata;

    /* 从设备树获取GPIO */
    kdata->gpio = of_get_named_gpio(np, "key-gpio", 0);
    if (!gpio_is_valid(kdata->gpio)) {
        dev_err(dev, "invalid GPIO\n");
        return -EINVAL;
    }

    /* 申请GPIO并设置为输入 */
    ret = devm_gpio_request(dev, kdata->gpio, "key-input");
    if (ret) {
        dev_err(dev, "GPIO request failed\n");
        return ret;
    }

    ret = gpio_direction_input(kdata->gpio);
    if (ret) {
        dev_err(dev, "GPIO direction failed\n");
        return ret;
    }

    /* 获取中断号 */
    kdata->irq = gpio_to_irq(kdata->gpio);
    if (kdata->irq < 0) {
        dev_err(dev, "GPIO to IRQ failed\n");
        return kdata->irq;
    }

    /* 初始化工作队列与等待队列 */
    INIT_WORK(&kdata->work, key_work_handler);
    init_waitqueue_head(&kdata->wq);
    kdata->available = 0;

    /* 注册中断,下降沿触发 */
    ret = devm_request_irq(dev, kdata->irq, key_irq_handler,
                           IRQF_TRIGGER_FALLING | IRQF_SHARED,
                           "key-interrupt", kdata);
    if (ret) {
        dev_err(dev, "IRQ request failed\n");
        return ret;
    }

    /* 注册misc设备 */
    kdata->mdev.minor = MISC_DYNAMIC_MINOR;
    kdata->mdev.name = "key_event";
    kdata->mdev.fops = &key_fops;
    kdata->mdev.parent = dev;

    ret = misc_register(&kdata->mdev);
    if (ret) {
        dev_err(dev, "misc register failed\n");
        return ret;
    }

    dev_info(dev, "key driver probed, gpio=%d, irq=%d\n",
             kdata->gpio, kdata->irq);
    return 0;
}

static int key_remove(struct platform_device *pdev)
{
    struct key_drv_data *kdata = g_key_data;

    if (kdata) {
        misc_deregister(&kdata->mdev);
        g_key_data = NULL;
    }
    return 0;
}

static const struct of_device_id key_of_match[] = {
    { .compatible = "example,key-interrupt" },
    { }
};
MODULE_DEVICE_TABLE(of, key_of_match);

static struct platform_driver key_driver = {
    .probe = key_probe,
    .remove = key_remove,
    .driver = {
        .name = "key_interrupt",
        .of_match_table = key_of_match,
    },
};

module_platform_driver(key_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BSP Course");
MODULE_DESCRIPTION("GPIO Key Interrupt Driver");

设备树配置


/ {
    key_device {
        compatible = "example,key-interrupt";
        key-gpio = <&gpio1 13 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

编译与加载


# 编译(内核源码树内)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

# 加载驱动
insmod key_interrupt.ko

# 验证设备节点
ls -l /dev/key_event

用户空间测试程序


#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

struct key_event {
    unsigned int code;
    unsigned int value;
};

int main(void)
{
    int fd;
    struct key_event ev;

    fd = open("/dev/key_event", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    printf("Waiting for key events...\n");

    while (1) {
        if (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
            printf("Key %u %s\n", ev.code,
                   ev.value ? "PRESSED" : "RELEASED");
        }
    }

    close(fd);
    return 0;
}

验证方法

编译并加载驱动后,确认设备节点/dev/key_event存在。运行测试程序,按下按键SW1,程序应输出Key 1 PRESSED;松开按键,输出Key 1 RELEASED。若在中断触发后3秒内无输出,检查GPIO编号是否正确、设备树是否匹配、中断是否被其他驱动占用。

判定标准

  • 驱动加载无错误,dmesg显示probe成功信息
  • /dev/key_event节点存在且可读
  • 每次按下/释放按键,测试程序均能收到对应事件
  • 连续快速按键不丢失事件(工作队列保证事件顺序处理)