1. IMX6ULL按键驱动开发概述
在嵌入式Linux系统开发中,外设驱动开发是最基础也是最重要的环节之一。作为一名长期从事嵌入式开发的工程师,我经常需要为各种外设编写驱动程序。今天要分享的是基于IMX6ULL开发板的按键驱动开发经验,这个案例虽然看似简单,但涵盖了Linux驱动开发的多个核心概念和技术要点。
这个驱动实现了以下几个关键功能:
- 采用Platform总线框架,实现驱动与硬件的解耦
- 使用中断机制检测按键动作
- 通过中断顶/底半部分离处理紧急和非紧急任务
- 实现阻塞式读取,避免用户空间轮询消耗CPU资源
2. 驱动开发核心知识点
2.1 Linux中断处理机制
Linux内核的中断处理采用顶半部(Top Half)和底半部(Bottom Half)分离的设计理念。这种设计源于一个基本事实:中断服务程序(ISR)需要尽快执行完毕,不能长时间占用CPU。
顶半部特点:
- 运行在中断上下文
- 需要快速执行完毕
- 不能休眠或阻塞
- 通常只做最必要的处理(如标记中断发生)
底半部特点:
- 可以运行在中断上下文或进程上下文
- 处理非紧急但可能耗时的任务
- 根据实现方式不同,可能有不同的限制
2.2 中断底半部实现方式
在Linux内核中,底半部主要有三种实现方式:
-
软中断(Softirq):
- 运行在中断上下文
- 静态分配,编译时确定
- 执行优先级高
-
Tasklet:
- 基于软中断实现
- 运行在中断上下文
- 动态创建,使用灵活
- 同类型tasklet不能并行执行
-
工作队列(Workqueue):
- 运行在进程上下文
- 可以休眠和阻塞
- 由内核线程执行
- 适合处理耗时操作
在我们的按键驱动中,将分别展示tasklet和workqueue两种实现方式。
2.3 阻塞式I/O实现原理
阻塞式I/O是Linux驱动中常见的用户空间与内核空间交互方式。其核心是通过等待队列(Wait Queue)实现:
- 当没有数据可读时,用户空间read调用被阻塞
- 驱动将当前进程加入等待队列
- 当条件满足(如按键按下),驱动唤醒等待队列中的进程
- 用户空间read调用返回数据
这种方式避免了用户空间不断轮询查询状态,大大降低了CPU占用率。
3. 驱动架构设计
3.1 整体架构
我们的按键驱动采用以下架构:
code复制Platform驱动注册 → 设备树匹配 → probe函数初始化 → 注册字符设备 → 配置GPIO和中断 → 等待按键中断 → 中断处理 → 用户空间读取
3.2 关键数据结构
驱动中定义了以下重要数据结构:
c复制static int key_gpio; // 按键GPIO号
static int key_irq; // 按键中断号
static wait_queue_head_t wq;// 等待队列头
static int condition; // 阻塞唤醒条件标志
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &fops
};
static struct of_device_id key_table[] = {
{.compatible = "pt-key"},
{}
};
static struct platform_driver pdrv = {
.probe = probe,
.remove = remove,
.driver = {
.name = DEV_NAME,
.of_match_table = key_table
}
};
3.3 设备树配置
驱动通过设备树获取硬件资源信息,需要在设备树中添加如下节点:
dts复制ptkey {
compatible = "pt-key";
ptkey-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
status = "okay";
};
4. 驱动核心实现
4.1 Platform驱动注册
Platform驱动是Linux内核中用于管理不依赖于传统总线类型的设备。我们的按键驱动通过Platform驱动框架实现:
c复制static int __init key_driver_init(void)
{
return platform_driver_register(&pdrv);
}
static void __exit key_driver_exit(void)
{
platform_driver_unregister(&pdrv);
}
module_init(key_driver_init);
module_exit(key_driver_exit);
MODULE_LICENSE("GPL");
4.2 Probe函数实现
Probe函数是Platform驱动的核心,在设备与驱动匹配成功后调用:
c复制static int probe(struct platform_device *pdev)
{
struct device_node *pdts;
int ret;
// 注册杂项设备
ret = misc_register(&misc_dev);
if(ret) goto err_misc_register;
// 获取设备树节点
pdts = of_find_node_by_path("/ptkey");
if(!pdts) {
ret = -ENODEV;
goto err_of_find;
}
// 从设备树获取GPIO号
key_gpio = of_get_named_gpio(pdts, "ptkey-gpio", 0);
if(key_gpio < 0) {
ret = key_gpio;
goto err_of_find;
}
// 获取中断号
key_irq = irq_of_parse_and_map(pdts, 0);
if(key_irq < 0) {
ret = key_irq;
goto err_irq_map;
}
// 申请中断
ret = request_irq(key_irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "key0_irq", &arg);
if(ret < 0) goto err_request_irq;
// 初始化等待队列
init_waitqueue_head(&wq);
// 初始化底半部(tasklet或workqueue)
// ...
return 0;
// 错误处理
err_request_irq:
free_irq(key_irq, &arg);
err_irq_map:
err_of_find:
misc_deregister(&misc_dev);
err_misc_register:
return ret;
}
4.3 文件操作实现
我们实现了基本的文件操作接口:
c复制static struct file_operations fops = {
.owner = THIS_MODULE,
.open = open,
.read = read,
.release = close
};
static int open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "key device opened\n");
return 0;
}
static ssize_t read(struct file *file, char __user *buf,
size_t size, loff_t *loff)
{
int ret;
int status = 0;
condition = 0;
// 阻塞等待按键事件
wait_event_interruptible(wq, condition);
status = 1; // 表示按键按下
ret = copy_to_user(buf, &status, sizeof(status));
return sizeof(status);
}
static int close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "key device closed\n");
return 0;
}
5. 中断处理实现
5.1 中断顶半部
中断顶半部需要尽可能快速地执行:
c复制static irqreturn_t key_irq_handler(int irq, void *dev)
{
int arg = *(int *)dev;
// 参数校验
if(100 != arg)
return IRQ_NONE;
// 调度底半部
tasklet_schedule(&tsk); // 或schedule_work(&work);
return IRQ_HANDLED;
}
5.2 Tasklet实现
Tasklet版本的底半部实现:
c复制static struct tasklet_struct tsk;
static void key_tasklet_handler(unsigned long arg)
{
condition = 1;
wake_up_interruptible(&wq);
printk(KERN_INFO "Tasklet executed, arg=%lu\n", arg);
}
// 在probe函数中初始化
tasklet_init(&tsk, key_tasklet_handler, 100);
5.3 Workqueue实现
Workqueue版本的底半部实现:
c复制static struct work_struct work;
static void key_work_func(struct work_struct *work)
{
// 可以安全地休眠
ssleep(1);
condition = 1;
wake_up_interruptible(&wq);
printk(KERN_INFO "Workqueue executed\n");
}
// 在probe函数中初始化
INIT_WORK(&work, key_work_func);
6. 驱动测试与验证
6.1 编译驱动
编写Makefile进行交叉编译:
makefile复制obj-m := key_driver.o
KDIR := /path/to/kernel
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
6.2 用户空间测试程序
c复制#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd;
int status;
fd = open("/dev/key", O_RDONLY);
if(fd < 0) {
perror("open");
return -1;
}
while(1) {
read(fd, &status, sizeof(status));
printf("Key pressed! Status=%d\n", status);
}
close(fd);
return 0;
}
6.3 测试步骤
-
加载驱动模块:
bash复制
insmod key_driver.ko -
查看设备节点:
bash复制ls /dev/key -
查看内核日志:
bash复制dmesg | tail -
运行测试程序:
bash复制
./key_test -
按下按键观察输出
7. 开发经验与注意事项
在实际开发过程中,我总结了以下几点重要经验:
-
中断上下文限制:
- 在中断顶半部和tasklet中不能调用可能休眠的函数
- 避免执行耗时操作
- 不要直接访问用户空间内存
-
资源管理:
- 所有申请的资源必须释放
- 错误处理要遵循"先申请后释放"的原则
- 使用goto语句进行集中错误处理是个好习惯
-
并发控制:
- 考虑多个进程同时访问设备的情况
- 必要时使用锁机制保护共享数据
-
调试技巧:
- 合理使用printk输出调试信息
- 可以通过/proc/interrupts查看中断统计
- 使用strace工具跟踪系统调用
-
性能考量:
- 尽量减少中断处理时间
- 合理选择底半部实现方式
- 避免不必要的内存拷贝
8. 扩展与优化
基于当前驱动,还可以进行以下扩展:
-
按键消抖:
- 在workqueue中添加延时消抖处理
- 典型消抖时间为10-20ms
-
支持多个按键:
- 扩展设备树描述多个按键
- 在驱动中管理多个GPIO和中断
-
非阻塞I/O支持:
- 实现poll文件操作
- 允许用户空间以非阻塞方式查询按键状态
-
输入子系统集成:
- 将驱动注册为输入设备
- 生成标准输入事件
-
电源管理:
- 实现suspend/resume回调
- 在系统休眠时合理处理中断
这个按键驱动虽然功能简单,但涵盖了Linux驱动开发的许多核心概念。通过这个案例,我们可以深入理解Platform总线、中断处理、阻塞I/O等关键技术在实际项目中的应用。