嵌入式Linux驱动开发:i.MX6ULL中断处理
嵌入式Linux驱动开发:i.MX6ULL中断处理
1. 概述
本文档基于提供的imx6uirq.c
、tasklet.c
、work.c
源码以及imx6ull-alientek-emmc.dts
设备树文件,详细解析了i.MX6ULL平台上的中断驱动开发。重点分析了中断处理的三种方式:直接处理、软中断(tasklet)和工作队列(workqueue),并结合设备树配置,全面阐述了中断驱动的理论基础和实现细节。
2. 设备树(DTS)分析
设备树是描述硬件配置的关键文件,它将硬件信息从内核代码中分离出来,使得驱动程序更加通用。以下是对imx6ull-alientek-emmc.dts
中相关中断节点的分析。
2.1 key
节点定义
在设备树中,key
节点定义了按键硬件的配置:
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
2.1.1 关键属性解释
compatible
: 该属性是驱动程序与设备树节点匹配的关键。驱动程序通过of_match_table
查找具有相同compatible
字符串的节点。在此例中,"alientek,key"
表明这是一个正点原子开发板上的按键设备。pinctrl-names
和pinctrl-0
: 这两个属性用于配置GPIO引脚的复用功能和电气特性。pinctrl-0
指向了&pinctrl_key
,该节点定义了GPIO1_IO18引脚的具体配置。key-gpios
: 这是一个GPIO描述符,指定了按键连接到哪个GPIO控制器和具体的引脚号。<&gpio1 18 GPIO_ACTIVE_HIGH>
表示:&gpio1
: GPIO控制器1。18
: 引脚号为18。GPIO_ACTIVE_HIGH
: 按键按下时,引脚电平为高电平。
interrupt-parent
: 指定中断的父控制器。<&gpio1>
表明该中断由GPIO1控制器管理。interrupts
: 定义中断源和触发类型。<18 IRQ_TYPE_EDGE_BOTH>
表示:18
: 中断号,对应GPIO1_IO18引脚。IRQ_TYPE_EDGE_BOTH
: 触发方式为双边沿触发(上升沿和下降沿都会触发中断)。
2.2 pinctrl_key
引脚配置
在&iomuxc
节点下,pinctrl_key
定义了GPIO1_IO18引脚的电气特性:
pinctrl_key: keygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080>;
};
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18
: 将UART1_CTS_B这个物理引脚复用为GPIO1_IO18功能。0xF080
: 这是一个32位的配置值,用于设置引脚的驱动能力、上/下拉电阻、开漏模式等。具体的位定义需要查阅i.MX6ULL参考手册。
2.3 设备树与驱动的关联
驱动程序通过of_find_node_by_path("/key")
在设备树中查找/key
节点,然后使用of_get_named_gpio()
和of_property_read_u32()
等函数读取节点中的属性,从而获取硬件配置信息。这种方式实现了驱动与硬件的解耦。
3. 中断处理基础
在Linux内核中,中断处理分为两个部分:中断上半部(Top Half) 和 中断下半部(Bottom Half)。
3.1 中断上半部 (Top Half)
- 特点: 运行在中断上下文中,对时间要求极为严格,必须快速完成。
- 限制: 不能睡眠,不能调用可能引起睡眠的函数(如
copy_to_user
、kmalloc
withGFP_KERNEL
、mutex_lock
等)。 - 任务: 通常只做最紧急的操作,如清除中断标志、读取硬件状态,然后将耗时的任务调度到下半部处理。
3.2 中断下半部 (Bottom Half)
- 目的: 处理上半部中不能完成的耗时任务。
- 机制: 有多种实现方式,包括软中断(Softirq)、tasklet、工作队列(Workqueue)和线程化中断(Threaded IRQ)。
- 运行环境: 运行在进程上下文中,可以睡眠,可以调用大多数内核函数。
4. 驱动代码详解
4.1 核心数据结构
4.1.1 struct key_desc
该结构体描述了单个按键的信息:
struct key_desc {char name[10]; // 按键名称,用于注册中断int gpio; // GPIO引脚号int irqnum; // 中断号unsigned char value; // 按键按下时返回的值irqreturn_t (*handler)(int, void *); // 中断处理函数指针struct tasklet_struct tasklet; // 用于tasklet方式的下半部struct work_struct work; // 用于工作队列方式的下半部
};
- 在
imx6uirq.c
中,tasklet
和work
成员未被使用。 - 在
tasklet.c
中,work
成员未被使用,tasklet
成员被初始化。 - 在
work.c
中,tasklet
成员未被使用,work
成员被初始化。
4.1.2 struct imx6uirq_dev
这是整个驱动的核心设备结构体:
struct imx6uirq_dev {dev_t devid; // 设备号int major; // 主设备号int minor; // 次设备号struct cdev cdev; // 字符设备结构体struct class *class; // 设备类struct device *device; // 设备struct device_node *key_nd; // 设备树节点指针struct key_desc key[KEY_NUM]; // 按键描述符数组struct timer_list timer; // 用于消抖的定时器atomic_t keyvalue; // 原子变量,存储按键值atomic_t release; // 原子变量,表示按键是否释放
};
- 使用
atomic_t
确保在多处理器环境下对keyvalue
和release
的访问是原子的,避免竞态条件。
4.2 初始化流程 (imx6uirq_init
)
- 分配设备号: 使用
alloc_chrdev_region
动态分配主设备号。 - 初始化字符设备: 调用
cdev_init
和cdev_add
将设备添加到内核。 - 创建设备类和设备节点: 使用
class_create
和device_create
在/sys/class/
和/dev/
下创建相应的条目,用户空间程序可以通过/dev/imx6uirq
访问设备。 - 初始化按键硬件: 调用
key_init
函数。
4.3 按键硬件初始化 (key_init
)
- 查找设备树节点:
of_find_node_by_path("/key")
。 - 获取GPIO:
of_get_named_gpio(dev->key_nd, "key-gpios", i)
。 - 申请GPIO:
gpio_request(dev->key[i].gpio, dev->key[i].name)
。 - 配置GPIO为输入:
gpio_direction_input(dev->key[i].gpio)
。 - 获取中断号:
gpio_to_irq(dev->key[i].gpio)
将GPIO号转换为中断号。 - 请求中断:
request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &imx6uirq)
。IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING
: 双边沿触发。- 最后一个参数
&imx6uirq
作为dev_id
传递给中断处理函数,用于在中断发生时找到对应的设备结构体。
4.4 中断处理函数
4.4.1 直接处理 (imx6uirq.c
)
static irqreturn_t key0_handler(int irq, void *filp) {struct imx6uirq_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
- 上半部非常简洁,只修改了定时器参数并启动了定时器。实际的按键值读取和去抖动由定时器的回调函数
timer_func
完成。
4.4.2 Tasklet方式 (tasklet.c
)
static irqreturn_t key0_handler(int irq, void *filp) {struct imx6uirq_dev *dev = filp;tasklet_schedule(&dev->key[0].tasklet); // 调度taskletreturn IRQ_HANDLED;
}static void key_tasklet(unsigned long data) {struct imx6uirq_dev *dev = (struct imx6uirq_dev *)data;printk("Kernel: key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));
}
- 上半部调用
tasklet_schedule()
,将key_tasklet
函数标记为可执行。 key_tasklet
运行在软中断上下文中,不能睡眠,但比上半部有更多的时间。它启动了一个定时器来完成最终的按键处理。
4.4.3 工作队列方式 (work.c
)
static irqreturn_t key0_handler(int irq, void *filp) {struct imx6uirq_dev *dev = filp;schedule_work(&dev->key[0].work); // 调度工作return IRQ_HANDLED;
}static void key_work(struct work_struct *work) {printk("Kernel: key_work\r\n");imx6uirq.timer.data = (unsigned long)&imx6uirq;mod_timer(&imx6uirq.timer, jiffies + msecs_to_jiffies(20));
}
- 上半部调用
schedule_work()
,将key_work
函数添加到默认的工作队列system_wq
中排队。 key_work
运行在进程上下文中,可以睡眠,可以执行更复杂的操作。它同样启动了一个定时器。
4.5 定时器处理 (timer_func
)
无论采用哪种下半部机制,最终都通过定时器来完成按键的去抖动和状态读取:
static void timer_func(unsigned long arg) {struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio); // 读取GPIO电平if (value == 0) {atomic_set(&dev->keyvalue, dev->key[0].value); // 按下} else {atomic_set(&dev->release, 1); // 释放}
}
- 定时器延迟20ms是为了消除按键的机械抖动。
4.6 文件操作 (imx6uirq_read
)
用户空间程序通过read()
系统调用读取按键状态:
ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) {struct imx6uirq_dev *dev = filp->private_data;u8 keyvalue, release;int ret = 0;keyvalue = atomic_read(&dev->keyvalue);release = atomic_read(&dev->release);if (release) { // 只有在按键释放时才返回数据ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));if (ret) {ret = -EFAULT;goto fail_copy_user;}atomic_set(&dev->release, 0); // 重置release标志} else {ret = -EAGAIN; // 按键未释放,返回-EAGAIN,用户程序可非阻塞读取}return sizeof(keyvalue);
}
- 该函数实现了“事件驱动”模型:只有在按键被按下并释放后,
read()
才会返回按键值。 - 如果使用
O_NONBLOCK
标志打开设备,当没有按键事件时,read()
会立即返回-EAGAIN
,这符合poll
/select
/epoll
的预期行为。
5. 三种中断下半部机制对比
特性 | Tasklet | 工作队列 (Workqueue) |
---|---|---|
运行上下文 | 软中断上下文 | 进程上下文 |
能否睡眠 | 不能 | 能 |
并发性 | 同一个tasklet不能在多个CPU上同时运行,但不同tasklet可以。 | 工作可以在不同的工作队列或不同CPU上并发执行。 |
使用场景 | 需要快速执行,且不能睡眠的简单任务。 | 需要执行复杂任务、可能睡眠或调用阻塞函数的任务。 |
API | tasklet_init() , tasklet_schedule() | INIT_WORK() , schedule_work() |
5.1 选择建议
- 简单、快速、不睡眠: 选择 Tasklet。
- 复杂、可能睡眠、需要调度: 选择 工作队列。
- 本例中的选择: 本例中,下半部的任务是启动一个定时器,这是一个非常轻量级的操作。因此,使用
tasklet
或workqueue
在此场景下并无显著优劣。但在更复杂的应用中,如需要访问文件系统或网络,工作队列是唯一的选择。
6. 总结
本文档详细解析了基于i.MX6ULL的中断驱动开发,涵盖了设备树配置、中断处理的上下半部概念、三种下半部实现机制(直接、tasklet、workqueue)的代码实现和对比。理解这些核心概念对于开发稳定、高效的嵌入式Linux驱动至关重要。
源码仓库位置: https://gitee.com/dream-cometrue/linux_driver_imx6ull