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

嵌入式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.dtcms.com/a/356331.html

相关文章:

  • 深入解析Qt节点编辑器框架:交互逻辑与样式系统(二)
  • C++基础(⑤删除链表中的重复节点(链表 + 遍历))
  • 储能变流器之LLC
  • MySQL数据库精研之旅第十四期:索引的 “潜规则”(上)
  • Unity、Unreal Engine与Godot中纹理元数据管理的比较分析
  • 嵌入式Linux LED驱动开发
  • Ubuntu22.04系统安装Opencv,无法定位包libjasper-dev libdc1394-22-dev的解决办法
  • 【C++】C++入门——(上)
  • GTSAM中gtsam::LinearContainerFactor因子详解
  • 【C++八股文】计算机网络篇
  • 【YOLO学习笔记】数据增强mosaic、Mixup、透视放射变换
  • flutter-使用url_launcher打开链接/应用/短信/邮件和评分跳转等
  • leetcode 338 比特位计数
  • rockchip温控及cpu降频配置
  • 事务和锁(进阶)
  • 使用 Docker 部署 Squid 为 Kubernetes 中的 Nexus3 提供公网代理访问
  • Windows12概念曝光,巧用远程控制工具抢先体验
  • 人脸识别“不备案“有哪些后果?
  • 公司内网部署离线deepseek+docker+ragflow本地模型实战
  • Day15 Logurs框架学习
  • Elasticsearch核心配置与性能优化
  • Linux 线程调度核心要点
  • 期权合约作废了怎么处理?
  • AI共链·智存未来 | 绿算技术受邀出席华为AI SSD发布会
  • 若依微服务一键部署(RuoYi-Cloud):Nacos/Redis/MySQL + Gateway + Robot 接入(踩坑与修复全记录)
  • 吱吱企业通讯软件可私有化部署,构建安全可控的通讯办公平台
  • C++异常处理指南:构建健壮程序的错误处理机制
  • 2025年渗透测试面试题总结-39(题目+回答)
  • FDTD_mie散射_仿真学习(2)
  • AWS集成开发最佳实践:构建高效可靠的云管理平台