当前位置: 首页 > ops >正文

嵌入式Linux驱动开发:i.MX6ULL中断处理

嵌入式Linux驱动开发:i.MX6ULL中断处理

1. 概述

本文档基于提供的imx6uirq.ctasklet.cwork.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-namespinctrl-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_userkmalloc with GFP_KERNELmutex_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中,taskletwork成员未被使用。
  • 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确保在多处理器环境下对keyvaluerelease的访问是原子的,避免竞态条件。

4.2 初始化流程 (imx6uirq_init)

  1. 分配设备号: 使用alloc_chrdev_region动态分配主设备号。
  2. 初始化字符设备: 调用cdev_initcdev_add将设备添加到内核。
  3. 创建设备类和设备节点: 使用class_createdevice_create/sys/class//dev/下创建相应的条目,用户空间程序可以通过/dev/imx6uirq访问设备。
  4. 初始化按键硬件: 调用key_init函数。

4.3 按键硬件初始化 (key_init)

  1. 查找设备树节点: of_find_node_by_path("/key")
  2. 获取GPIO: of_get_named_gpio(dev->key_nd, "key-gpios", i)
  3. 申请GPIO: gpio_request(dev->key[i].gpio, dev->key[i].name)
  4. 配置GPIO为输入: gpio_direction_input(dev->key[i].gpio)
  5. 获取中断号: gpio_to_irq(dev->key[i].gpio)将GPIO号转换为中断号。
  6. 请求中断: 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上并发执行。
使用场景需要快速执行,且不能睡眠的简单任务。需要执行复杂任务、可能睡眠或调用阻塞函数的任务。
APItasklet_init(), tasklet_schedule()INIT_WORK(), schedule_work()

5.1 选择建议

  • 简单、快速、不睡眠: 选择 Tasklet
  • 复杂、可能睡眠、需要调度: 选择 工作队列
  • 本例中的选择: 本例中,下半部的任务是启动一个定时器,这是一个非常轻量级的操作。因此,使用taskletworkqueue在此场景下并无显著优劣。但在更复杂的应用中,如需要访问文件系统或网络,工作队列是唯一的选择。

6. 总结

本文档详细解析了基于i.MX6ULL的中断驱动开发,涵盖了设备树配置、中断处理的上下半部概念、三种下半部实现机制(直接、tasklet、workqueue)的代码实现和对比。理解这些核心概念对于开发稳定、高效的嵌入式Linux驱动至关重要。

源码仓库位置: https://gitee.com/dream-cometrue/linux_driver_imx6ull

http://www.xdnf.cn/news/19088.html

相关文章:

  • 【面试场景题】怎么做业务领域划分
  • 163.在 Vue3 中使用 OpenLayers 解析 GeoJSON,并给 Feature 填充 pattern(图案)颜色
  • 交叉编译 手动安装 libzip 库 移植ARM 需要 zlib的
  • mysql安全运维之安全模型与原则-构建坚不可摧的数据库防护体系
  • 《AI智脉速递》2025 年 8 月22 日 - 29 日
  • 面向马赛克战的未来智能化作战体系发展展望
  • Linux设备驱动
  • Allegro X PCB设计小诀窍系列--26.如何在Allegro X中加密保护PCB文件?
  • Pycharm打包PaddleOCR过程及问题解决方法
  • 【Mentor Xpedition】预习一下
  • 投资之路:财富积累与人生规划的智慧
  • UART和SPI区别
  • ros2--topic/话题--接口
  • 多线程图像发送处理器的设计与实现
  • 12、做中学 | 初一上期 Golang函数 包 异常
  • cssword属性
  • ubuntu 安装 vllm
  • Linux笔记13——shell编程基础-7
  • 基于SpringBoot和Thymeleaf开发的英语学习网站
  • ubuntu24.04 QT中配置opencv4.12
  • FreeRTOS基础知识记录
  • MySQL 中有哪些锁类型?
  • 华为交换机S5700设置acl
  • 衡石SENSE 6.0技术解析:Workflow到Agent模式如何重塑计算框架
  • ADC模数转换
  • Android init 进程部分理论
  • 解决使用OSS的multipartUpload方法上传大文件导致内存溢出的问题
  • 设计模式-行为型模式-命令模式
  • 【编号513】2025年全国地铁矢量数据
  • 从混乱到高效:ITSM软件如何重塑企业IT管理的新格局