嵌入式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节点定义了按键硬件的配置:

dts 复制代码
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引脚的电气特性:

dts 复制代码
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

该结构体描述了单个按键的信息:

c 复制代码
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

这是整个驱动的核心设备结构体:

c 复制代码
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)
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)
c 复制代码
static irqreturn_t key0_handler(int irq, void *filp) {
    struct imx6uirq_dev *dev = filp;
    tasklet_schedule(&dev->key[0].tasklet); // 调度tasklet
    return 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)
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)

无论采用哪种下半部机制,最终都通过定时器来完成按键的去抖动和状态读取:

c 复制代码
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()系统调用读取按键状态:

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

6. 总结

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

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

相关推荐
沉在嵌入式的鱼1 分钟前
STM32--HX711称重传感器
stm32·单片机·嵌入式硬件·hx711·称重传感器
richxu202510018 分钟前
嵌入式学习之路>单片机核心原理>(3)定时器
单片机·嵌入式硬件·学习
minji...15 分钟前
linux 进程控制(一) (fork进程创建,exit进程终止)
linux·运维·服务器·c++·git·算法
I · T · LUCKYBOOM18 分钟前
21.Linux网络设置
linux·运维·网络
Likeyou719 分钟前
关于Linux下的Oracle的rman备份操作指南
linux·运维·oracle
峰顶听歌的鲸鱼23 分钟前
13.docker部署
linux·运维·笔记·docker·容器·云计算
橘子编程26 分钟前
仓颉语言变量与表达式解析
java·linux·服务器·开发语言·数据库·python·mysql
虚神界熊孩儿31 分钟前
linux下创建用户和用户组
linux·运维·服务器
hhwyqwqhhwy34 分钟前
linux 驱动 rtc
linux·运维·实时音视频
python百炼成钢35 分钟前
53.Linux regmap驱动框架
linux·运维·服务器·驱动开发