1. I.MX6U Linux驱动开发概述
在嵌入式Linux开发中,驱动开发是最核心也最具挑战性的部分之一。I.MX6U作为NXP推出的高性能处理器,广泛应用于工业控制、物联网网关等领域。掌握其Linux驱动开发技术,对于嵌入式开发者而言至关重要。
中断处理是驱动开发中最常见的场景之一。与裸机开发不同,Linux内核提供了完善的中断管理框架,开发者需要遵循内核的编程规范来实现中断处理。本次实验将重点讲解按键中断的实现,并结合定时器消抖和设备树配置,展示一个完整的驱动开发流程。
2. 中断基础与Linux中断框架
2.1 中断的基本概念
中断是处理器响应外部事件的重要机制。当外设需要CPU处理时,会通过中断信号通知CPU。CPU暂停当前任务,转去执行中断服务程序(ISR),执行完毕后再返回原任务。
Linux内核将中断处理分为两部分:
- 上半部(top half):快速处理关键操作,如清除中断标志
- 下半部(bottom half):处理耗时操作,如数据处理
这种设计避免了长时间关中断导致系统响应延迟。
2.2 Linux中断API
内核提供了一系列中断相关API:
c复制// 申请中断
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
// 释放中断
void free_irq(unsigned int irq, void *dev);
// 中断处理函数原型
irqreturn_t (*irq_handler_t)(int, void *);
其中,flags常用的选项包括:
- IRQF_TRIGGER_RISING:上升沿触发
- IRQF_TRIGGER_FALLING:下降沿触发
- IRQF_SHARED:共享中断
2.3 下半部处理机制
Linux提供了三种下半部处理机制:
- 软中断(Softirq):内核预定义的几种软中断类型,执行优先级高
- Tasklet:基于软中断实现,适合大部分场景
- 工作队列(Workqueue):在进程上下文执行,可以睡眠
本次实验将展示tasklet和工作队列两种方式的实现。
3. 设备树中断配置
3.1 设备树中的中断表示
在设备树中,中断控制器节点需要指定:
interrupt-controller:标识为中断控制器#interrupt-cells:描述中断所需的cell数量
对于I.MX6U的GPIO中断,通常需要2个cell:
- 第一个cell:GPIO编号
- 第二个cell:触发方式
3.2 按键中断设备树配置
在imx6ull-alientek-emmc.dts中添加按键节点:
dts复制key {
compatible = "alientek,key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
status = "okay";
};
其中:
interrupt-parent:指定中断控制器interrupts:GPIO18,双边沿触发
3.3 GPIO引脚复用配置
在iomuxc节点中添加引脚复用配置:
dts复制pinctrl_key: keygrp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
>;
};
4. 按键中断驱动实现
4.1 驱动整体架构
驱动主要包含以下部分:
- 字符设备框架
- 中断初始化与处理
- 定时器消抖
- 下半部处理(tasklet或工作队列)
4.2 关键数据结构
c复制struct irq_keydesc {
int gpio;
int irqnum;
unsigned char value;
char name[10];
irqreturn_t (*handler)(int, void *);
struct tasklet_struct tasklet; // 或 struct work_struct work;
};
struct irq {
dev_t devid;
struct cdev cdev;
struct device *device;
struct class *class;
struct device_node *nd;
struct irq_keydesc irqkey[KEY_NUM];
struct timer_list timer;
atomic_t releasekey;
atomic_t keyvalue;
};
4.3 中断处理流程
- 按键触发中断,进入上半部处理函数
- 上半部记录当前电平,调度下半部
- 下半部启动定时器(消抖)
- 定时器到期后检查电平,确认有效按键事件
4.4 核心代码实现
4.4.1 中断初始化
c复制static int keyio_init(struct irq *dev)
{
// 获取设备树节点
dev->nd = of_find_node_by_path("/key");
// 获取GPIO和中断号
for(i=0; i<KEY_NUM; i++) {
dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i);
dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);
// 申请GPIO
gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name);
gpio_direction_input(dev->irqkey[i].gpio);
// 注册中断
ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[i].handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
dev->irqkey[i].name, &key_irq);
// 初始化tasklet或工作队列
tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);
// 或 INIT_WORK(&dev->irqkey[i].work, key_work);
}
}
4.4.2 中断处理函数
c复制static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct irq *dev = (struct irq*)dev_id;
int current_level = gpio_get_value(dev->irqkey[0].gpio);
atomic_set(&dev->keyvalue, current_level);
tasklet_schedule(&dev->irqkey[0].tasklet); // 或 schedule_work(&dev->irqkey[0].work);
return IRQ_HANDLED;
}
4.4.3 Tasklet处理函数
c复制static void key_tasklet(unsigned long data)
{
struct irq* dev = (struct irq*)data;
dev->timer.data = data;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); // 20ms消抖
}
4.4.4 定时器处理函数
c复制static void timer_func(unsigned long arg)
{
struct irq* dev = (struct irq*)arg;
int current_value = gpio_get_value(dev->irqkey[0].gpio);
if (current_value == atomic_read(&dev->keyvalue)) {
if (current_value == 1) { // 释放
printk("KEY0 release!\r\n");
atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value);
atomic_set(&dev->releasekey, 1);
} else { // 按下
printk("KEY0 Push!\r\n");
atomic_set(&dev->keyvalue, current_value);
}
}
}
5. 测试与应用
5.1 编译与加载驱动
bash复制make
sudo cp irq.ko /lib/modules/$(uname -r)/ -f
depmod
modprobe irq
5.2 测试应用程序
c复制int main(int argc, char *argv[])
{
int fd = open("/dev/timer", O_RDWR);
unsigned char data;
while(1) {
ret = read(fd, &data, sizeof(data));
if (ret > 0) {
printf("key value = %#x\r\n", data);
}
}
close(fd);
return 0;
}
5.3 测试结果
按下和释放按键时,应能看到如下输出:
code复制KEY0 Push!
KEY0 release!
6. 常见问题与调试技巧
6.1 中断无法触发
- 检查设备树配置是否正确
- 确认interrupt-parent和interrupts属性
- 检查GPIO复用配置
- 确认中断号获取正确
- 使用
gpio_to_irq()获取正确的中断号
- 使用
- 检查中断标志位设置
- 确保与设备树中的触发方式一致
6.2 按键抖动严重
- 调整消抖时间
- 20ms是常用值,可根据实际硬件调整
- 检查硬件电路
- 确保有适当的滤波电容
- 尝试不同的消抖算法
- 多次采样确认
6.3 下半部处理延迟
- Tasklet与工作队列选择
- Tasklet执行在中断上下文,不能睡眠
- 工作队列执行在进程上下文,可以睡眠
- 避免在下半部执行耗时操作
- 考虑使用线程化中断(request_threaded_irq)
7. 进阶优化
7.1 使用devm_系列函数
内核提供了资源自动管理的devm_系列函数,可以简化错误处理和资源释放:
c复制int devm_request_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, unsigned long irqflags,
const char *devname, void *dev_id);
7.2 线程化中断
对于耗时中断处理,可以使用线程化中断:
c复制int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
7.3 中断统计信息
通过/proc/interrupts可以查看中断统计信息:
bash复制cat /proc/interrupts
8. 总结与扩展
本实验完整展示了Linux下中断驱动的开发流程,涵盖了设备树配置、中断处理、定时器消抖和下半部处理等关键技术点。在实际项目中,可以根据需求选择合适的下半部处理机制:
- 对于简单快速的操作,使用tasklet
- 对于可能睡眠或较耗时的操作,使用工作队列
- 对于复杂的中断处理,考虑线程化中断
后续可以扩展的方向包括:
- 多按键支持
- 长按/短按识别
- 组合键处理
- 中断性能优化
