2. 车载内核模块参数传递与权限控制

任务目标

在车载Linux环境中,编写一个按键GPIO中断驱动程序。当按键按下时,GPIO产生下降沿中断,驱动通过中断处理函数(底半部机制)上报按键事件至用户空间。本案例聚焦于中断申请、底半部处理及事件上报的完整最小实现。

环境与依赖

  • 内核版本:Linux 4.14+(支持request_irq及gpiod API)
  • 硬件:任意GPIO按键(按下接地,默认上拉高电平),本文以GPIO编号68为例
  • 头文件依赖:linux/interrupt.h、linux/gpio.h、linux/miscdevice.h

关键完整代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>

#define KEY_GPIO        68
#define KEY_IRQ_NAME    "car_key_irq"
#define KEY_EVENT_PRESS 0x01

static unsigned int irq_num;
static struct tasklet_struct key_tasklet;
static struct miscdevice key_miscdev;
static wait_queue_head_t key_wq;
static int key_pressed;
static int key_event_available;

/* 中断底半部:更新事件标志并唤醒读进程 */
static void key_tasklet_handler(unsigned long data)
{
    key_pressed = KEY_EVENT_PRESS;
    key_event_available = 1;
    wake_up_interruptible(&key_wq);
}

/* 中断顶半部:调度底半部 */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
    tasklet_schedule(&key_tasklet);
    return IRQ_HANDLED;
}

/* 用户空间读取按键事件 */
static ssize_t key_read(struct file *file, char __user *buf,
                        size_t count, loff_t *ppos)
{
    int ret;

    if (count < sizeof(int))
        return -EINVAL;

    /* 无事件时睡眠等待 */
    wait_event_interruptible(key_wq, key_event_available != 0);

    ret = copy_to_user(buf, &key_pressed, sizeof(key_pressed));
    if (ret)
        return -EFAULT;

    key_event_available = 0;
    key_pressed = 0;
    return sizeof(key_pressed);
}

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

static int __init key_driver_init(void)
{
    int ret;

    /* 1. 申请GPIO */
    ret = gpio_request(KEY_GPIO, KEY_IRQ_NAME);
    if (ret) {
        pr_err("Failed to request GPIO %d\n", KEY_GPIO);
        return ret;
    }

    /* 2. 配置为输入并获取中断号 */
    gpio_direction_input(KEY_GPIO);
    irq_num = gpio_to_irq(KEY_GPIO);
    if (irq_num < 0) {
        pr_err("Failed to map GPIO to IRQ\n");
        gpio_free(KEY_GPIO);
        return irq_num;
    }

    /* 3. 申请中断:下降沿触发 */
    ret = request_irq(irq_num, key_irq_handler,
                      IRQF_TRIGGER_FALLING, KEY_IRQ_NAME, NULL);
    if (ret) {
        pr_err("Failed to request IRQ %d\n", irq_num);
        gpio_free(KEY_GPIO);
        return ret;
    }

    /* 4. 初始化底半部与等待队列 */
    tasklet_init(&key_tasklet, key_tasklet_handler, 0);
    init_waitqueue_head(&key_wq);
    key_pressed = 0;
    key_event_available = 0;

    /* 5. 注册misc设备 */
    key_miscdev.minor = MISC_DYNAMIC_MINOR;
    key_miscdev.name = "car_key";
    key_miscdev.fops = &key_fops;
    ret = misc_register(&key_miscdev);
    if (ret) {
        pr_err("Failed to register misc device\n");
        free_irq(irq_num, NULL);
        tasklet_kill(&key_tasklet);
        gpio_free(KEY_GPIO);
        return ret;
    }

    pr_info("Key driver initialized, IRQ=%d\n", irq_num);
    return 0;
}

static void __exit key_driver_exit(void)
{
    misc_deregister(&key_miscdev);
    tasklet_kill(&key_tasklet);
    free_irq(irq_num, NULL);
    gpio_free(KEY_GPIO);
    pr_info("Key driver removed\n");
}

module_init(key_driver_init);
module_exit(key_driver_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Car Key GPIO Interrupt Driver");

编译与加载

# Makefile(与源码同目录)
obj-m := key_drv.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

# 编译与加载
make
sudo insmod key_drv.ko
sudo chmod 666 /dev/car_key

验证方法

  1. 加载检查:执行dmesg | tail,应看到Key driver initialized, IRQ=xxx日志,且/dev/car_key节点存在。
  2. 中断验证:执行cat /proc/interrupts | grep car_key,记录当前中断计数。
  3. 功能测试:终端执行cat /dev/car_key(阻塞等待),然后手动将GPIO 68接地再释放(模拟按键按下)。
  4. 通过标准:cat命令返回并打印数值1;再次查看/proc/interrupts,对应中断计数增加。若按键未按下时cat保持阻塞,说明事件等待机制正常。

关键说明

  • 本驱动使用tasklet作为中断底半部,在软中断上下文执行,适合快速置位标志并唤醒等待队列。
  • 读取接口采用阻塞式设计,用户空间cat会等待直到按键事件发生,每次读取返回一个int型事件值(1表示按下)。
  • 若需同时检测释放事件,可修改触发方式为IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,并在处理函数中读取GPIO电平区分按下/释放。
  • 实际车载环境中GPIO编号需根据板级设备树或原理图修改KEY_GPIO宏定义。