嵌入式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

相关推荐
czhc11400756633 小时前
Linux 830 shell:expect,ss -ant ,while IFS=read -r line,
linux·运维·r语言
贾亚超3 小时前
【STM32外设】ADC
stm32·单片机·嵌入式硬件
2006yu3 小时前
从零开始学习单片机18
单片机·嵌入式硬件·学习
滴滴滴嘟嘟嘟.4 小时前
嵌入式Linux驱动开发:蜂鸣器驱动
linux·运维·驱动开发
梅见十柒4 小时前
UNIX网络编程笔记:共享内存区和远程过程调用
linux·服务器·网络·笔记·tcp/ip·udp·unix
溯光笔记5 小时前
服务器内网穿透NPS搭建过程 - 服务端linux服务器 客户端windows系统 - 溯光笔记
linux·服务器·windows
意法半导体STM325 小时前
STM32 USBx Device MSC standalone 移植示例 LAT1488
单片机·嵌入式硬件·device·msc·standalone·usbx
2501_930124705 小时前
Linux之Shell编程(三)流程控制
linux·前端·chrome
MThinker5 小时前
k230 按键拍照后,将摄像头拍照的1920*1080分辨率的图片以jpg文件格式,保存到板载TF存储卡的指定文件夹目录中
python·嵌入式硬件·智能硬件·micropython·canmv·k230
Sadsvit7 小时前
Ansible 自动化运维工具:介绍与完整部署(RHEL 9)
linux·运维·centos·自动化·ansible