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
验证方法
- 加载检查:执行
dmesg | tail,应看到Key driver initialized, IRQ=xxx日志,且/dev/car_key节点存在。 - 中断验证:执行
cat /proc/interrupts | grep car_key,记录当前中断计数。 - 功能测试:终端执行
cat /dev/car_key(阻塞等待),然后手动将GPIO 68接地再释放(模拟按键按下)。 - 通过标准:
cat命令返回并打印数值1;再次查看/proc/interrupts,对应中断计数增加。若按键未按下时cat保持阻塞,说明事件等待机制正常。
关键说明
- 本驱动使用tasklet作为中断底半部,在软中断上下文执行,适合快速置位标志并唤醒等待队列。
- 读取接口采用阻塞式设计,用户空间
cat会等待直到按键事件发生,每次读取返回一个int型事件值(1表示按下)。 - 若需同时检测释放事件,可修改触发方式为
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,并在处理函数中读取GPIO电平区分按下/释放。 - 实际车载环境中GPIO编号需根据板级设备树或原理图修改
KEY_GPIO宏定义。