关注

I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门: linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏
⭐️流水不争先,争的是滔滔不绝

 📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、Linux 中断简介

1.1 核心中断 API 函数

1.1.1 中断号

1.1.2 中断申请与释放

1.1.3 中断处理函数

1.1.4 中断使能 / 禁止(入门必备工具)

1.2 中断上半部与下半部(核心优化思路,零基础也能懂)

1.2.1、上半部

1.2.2、下半部

软中断(静态注册的轻量级下半部)

tasklet(推荐优先使用的下半部)

工作队列(支持睡眠的下半部)

二、设备树中断配置(KEY0 为例,零基础手把手教学)

2.1 中断控制器节点(GIC 控制器示例)

2.2 GPIO 作为中断控制器(GPIO5 节点示例)

2.3 设备中断节点(以 fxls8471 磁力计为例)

2.4、设备树中断属性总结

三、获取中断号(驱动开发必备)

3.1 从设备树解析中断号

3.2 从 GPIO 获取中断号

四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)

4.1、硬件原理图

4.2、程序编写

4.2.1、修改设备树文件

4.2.2、驱动代码编写(imx6uirq.c)

4.2.3、驱动代码分段分析

结构体定义(33-58 行)

中断处理函数(66-74 行)

定时器消抖函数(81-99 行)

按键 IO 初始化函数(106-155 行)

字符设备操作函数(164-210 行)

驱动入口 / 出口函数(217-272 行)

4.2.3、测试 APP 代码

五、运行测试

5.1、编译驱动程序和测试 APP

 5.2、运行测试

总结


前言

裸机开发中使用中断需要手动配置寄存器、映射中断向量表,步骤繁琐;而 Linux 内核提供了完善的中断框架,开发者只需申请中断 + 注册中断处理函数,无需关注底层寄存器操作,极大简化了开发流程。


一、Linux 中断简介

先来回顾一下裸机实验里面中断的处理方法:

①、使能中断,初始化相应的寄存器。

②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数。

②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的API 函数。

1.1 核心中断 API 函数

1.1.1 中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。

1.1.2 中断申请与释放

Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断
函数作用关键参数说明
request_irq(irq, handler, flags, name, dev)申请并使能中断

irq:中断号

handler:中断处理函数。当中断发生以后就会执行此中断处理函数。

flags:中断标志(触发方式)

name:中断名(/proc/interrupts 可查)

dev:一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。

free_irq(irq, dev)释放中断共享中断需通过dev区分不同设备

常用中断标准如下所示:

1.1.3 中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数
irqreturn_t (*irq_handler_t) (int irq, void *dev_id)
// 返回值:IRQ_RETVAL(IRQ_HANDLED) 表示中断已处理

第一个参数是要中断处理函数要相应的中断号。

第二个参数是一个指向 void 的指针,也就是个通用指针。

需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。

中断处理函数的返回值为 irqreturn_t 类型。
enum irqreturn {
    IRQ_NONE        = (0 << 0),  // 中断不是本设备触发
    IRQ_HANDLED     = (1 << 0),  // 中断已被本设备处理
    IRQ_WAKE_THREAD = (1 << 1),  // 唤醒中断线程(线程化中断场景)
};
typedef enum irqreturn irqreturn_t;

1.1.4 中断使能 / 禁止(入门必备工具)

Linux 内核提供了两类中断控制函数:

单中断控制:enable_irq() / disable_irq() / disable_irq_nosync(),使能/禁止指定的单个中断;
全局中断控制:local_irq_enable() / local_irq_disable(),用于开启 / 关闭当前处理器的整个中断系统。

注意:⚠️ 全局中断的坑(零基础一定要看!)

直接使用local_irq_disable+local_irq_enable会引发严重问题:

假设 A 任务调用local_irq_disable关闭全局中断 10 秒,执行到第 2 秒时 B 任务开始运行,B 也调用local_irq_disable关闭 3 秒。3 秒后 B 调用local_irq_enable直接打开全局中断,此时仅过去 2+3=5 秒,A 任务要关闭 10 秒的愿望就破灭了,可能导致系统崩溃!

为了避免这种 “互相干扰” 的问题,必须使用状态保存 + 恢复的成对函数:

local_irq_save(flags)   // 禁止中断,并将当前中断状态保存到flags中
local_irq_restore(flags) // 恢复中断到flags保存的状态

    local_irq_save:不仅关闭中断,还会把当前 CPU 的中断状态(是否已关闭)存到flags变量里;
    local_irq_restore:不是简单打开中断,而是把中断状态恢复到flags记录的样子 —— 如果之前是关闭的,就保持关闭;如果之前是打开的,才打开。


    ✅ 最佳实践:永远用 local_irq_save / local_irq_restore 替代 local_irq_disable / local_irq_enable ,保证多任务环境下中断状态的安全,零基础也能写出稳定代码!

    总结:

    函数作用适用场景
    enable_irq/ disable_irq使能 / 禁止指定中断
    irq 就是要禁止的中断号
    单个中断控制
    local_irq_enable/ local_irq_disable开启 / 关闭全局中断临界区保护
    local_irq_save/ local_irq_restore保存 / 恢复中断状态避免全局中断被误修改

    1.2 中断上半部与下半部(核心优化思路,零基础也能懂)

    中断处理的核心要求是快进快出,因此 Linux 将中断分为两部分:

    上半部:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);
    下半部:处理耗时操作(如数据解析、硬件交互),内核提供 3 种实现方式:
    软中断:静态注册,适用于高频场景(如网络、定时器);
    tasklet:基于软中断实现,推荐优先使用(接口简单);
    工作队列:运行在进程上下文,支持睡眠(如 IIC/SPI 数据读取)。

    1.2.1、上半部

    主要为:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);

    1.2.2、下半部

    软中断(静态注册的轻量级下半部)

    早期 Linux 内核使用 “bottom half(BH)” 实现下半部,后被软中断和 tasklet 替代(2.5 版本后 BH 已被废弃)。

    Linux 内核用softirq_action结构体表示软中断:

    struct softirq_action {
        void (*action)(struct softirq_action *); // 软中断服务函数
    };

    内核中定义了 10 个全局软中断(softirq_vec[NR_SOFTIRQS]),NR_SOFTIRQS是枚举类型:

    enum {
        HI_SOFTIRQ=0,        /* 高优先级软中断 */
        TIMER_SOFTIRQ,       /* 定时器软中断 */
        NET_TX_SOFTIRQ,      /* 网络数据发送软中断 */
        NET_RX_SOFTIRQ,      /* 网络数据接收软中断 */
        BLOCK_SOFTIRQ,
        BLOCK_IOPOLL_SOFTIRQ,
        TASKLET_SOFTIRQ,     /* tasklet软中断 */
        SCHED_SOFTIRQ,       /* 调度软中断 */
        HRTIMER_SOFTIRQ,     /* 高精度定时器软中断 */
        RCU_SOFTIRQ,         /* RCU软中断 */
        NR_SOFTIRQS
    };

    使用软中断的核心步骤:

    注册软中断:open_softirq(nr, action),指定软中断类型和处理函数;
    触发软中断:raise_softirq(nr),在中断上半部中调用;
    内核初始化:softirq_init()会默认打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

     注意:软中断必须编译时静态注册,适合高频、轻量的耗时操作。

    tasklet(推荐优先使用的下半部)

    tasklet 是基于软中断实现的下半部机制,接口更简单,零基础推荐优先使用

    Linux 内核用tasklet_struct结构体表示 tasklet:

    struct tasklet_struct {
        struct tasklet_struct *next;  // 下一个tasklet
        unsigned long state;          // tasklet状态
        atomic_t count;               // 引用计数
        void (*func)(unsigned long);  // tasklet执行函数
        unsigned long data;           // 传递给func的参数
    };

    使用步骤:

    定义 + 初始化 tasklet:
    方式 1:先定义struct tasklet_struct,再用tasklet_init()初始化;
    方式 2:用宏DECLARE_TASKLET(name, func, data)一次性完成定义和初始化;
    调度 tasklet:在中断上半部调用tasklet_schedule(&tasklet),让内核在合适时间执行;

    示例代码

    // 定义tasklet
    struct tasklet_struct testtasklet;
    // tasklet处理函数
    void testtasklet_func(unsigned long data) {
        /* 耗时操作放在这里,比如数据解析、IIC读取 */
    }
    // 中断处理函数(上半部)
    irqreturn_t test_handler(int irq, void *dev_id) {
        tasklet_schedule(&testtasklet); // 触发下半部
        return IRQ_RETVAL(IRQ_HANDLED);
    }
    // 驱动入口初始化
    static int __init xxxx_init(void) {
        tasklet_init(&testtasklet, testtasklet_func, data);
        request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
        return 0;
    }
    工作队列(支持睡眠的下半部)

    工作队列是唯一运行在进程上下文的下半部机制,允许睡眠 / 重调度,适合需要阻塞的耗时操作(如 IIC/SPI 慢速设备读取)。

    Linux 内核用work_struct表示一个工作:

    struct work_struct {
        atomic_long_t data;
        struct list_head entry;
        work_func_t func; // 工作队列处理函数
    };

    使用步骤:
    定义 + 初始化工作:
    方式 1:先定义struct work_struct,再用INIT_WORK(work, func)初始化;
    方式 2:用宏DECLARE_WORK(n, f)一次性完成创建和初始化;
    调度工作:调用schedule_work(&work),让内核线程处理;

    示例代码

    // 定义工作
    struct work_struct testwork;
    // work处理函数
    void testwork_func_t(struct work_struct *work) {
        /* 可睡眠的耗时操作,比如SPI读取Flash数据 */
    }
    // 中断处理函数(上半部)
    irqreturn_t test_handler(int irq, void *dev_id) {
        /* 调度 work */
        schedule_work(&testwork); // 触发下半部
        return IRQ_RETVAL(IRQ_HANDLED);
    }
    // 驱动入口初始化
    static int __init xxxx_init(void) {
        /* 初始化 work */
        INIT_WORK(&testwork, testwork_func_t);
        /* 注册中断处理函数 */
        request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);  
        return 0;
    }

    二、设备树中断配置(KEY0 为例,零基础手把手教学)

    使用设备树时,需要在设备树中配置中断相关属性,Linux 内核会自动读取并配置中断。其中的 intc 节点就是 I.MX6ULL 的中断控制器节点。

    2.1 中断控制器节点(GIC 控制器示例)

    I.MX6ULL 的 GIC 中断控制器节点(imx6ull.dtsi中的intc节点)定义了中断解析规则:

    intc: interrupt-controller@00a01000 {
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;  // 每个中断源用3个cells描述
        interrupt-controller;    // 表示当前节点是中断控制器
        reg = <0x00a01000 0x1000>,
              <0x00a02000 0x100>;
    };

    compatible = "arm,cortex-a7-gic":匹配 GIC 中断控制器驱动;
    #interrupt-cells = <3>:表示此控制器下的中断源需要 3 个 cell 来描述:
    第 1 个 cell:中断类型(0=SPI,1=PPI);
    第 2 个 cell:中断号(SPI:0~987;PPI:0~15);
    第 3 个 cell:触发类型 + PPI CPU 掩码(bit [3:0]:1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平);
    interrupt-controller:空属性,标记当前节点为中断控制器。

    2.2 GPIO 作为中断控制器(GPIO5 节点示例)

    GPIO 节点也可作为中断控制器,以imx6ull.dtsi中的gpio5节点为例:

    gpio5: gpio@020ac000 {
        compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
        reg = <0x020ac000 0x4000>;
        interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
                     <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };

    interrupts:包含两条中断信息,均为 SPI 类型、高电平触发,中断号分别为 74、75;
    74 对应 GPIO5_IO00~GPIO5_IO15;
    75 对应 GPIO5_IO16~GPIO5_IO31;
    interrupt-controller:标记gpio5为中断控制器,管理其所有 IO 的中断;
    #interrupt-cells = <2>:表示此控制器下的中断源用 2 个 cell 描述(GPIO 编号 + 触发类型)。

    2.3 设备中断节点(以 fxls8471 磁力计为例)

    以 NXP 官方开发板的磁力计芯片fxls8471为例,其设备节点配置:

    fxls8471@1e {
        compatible = "fsl,fxls8471";
        reg = <0x1e>;
        position = <0>;
        interrupt-parent = <&gpio5>;  // 指定父中断控制器为gpio5
        interrupts = <0 8>;           // 0=GPIO5_IO00,8=低电平触发
    };

    关键属性解析:
    interrupt-parent:指定中断控制器(这里是gpio5);
    interrupts:中断信息,0对应 GPIO5_IO00,8对应低电平触发(IRQ_TYPE_LEVEL_LOW)。

    2.4、设备树中断属性总结

    属性名含义
    #interrupt-cells指定中断源的信息 cell 个数
    interrupt-controller标记当前节点为中断控制器
    interrupts指定中断号、触发方式等信息
    interrupt-parent指定父中断(即中断控制器)

    三、获取中断号(驱动开发必备)

    中断信息已写入设备树,驱动中可通过以下函数获取中断号:

    3.1 从设备树解析中断号

    irq_of_parse_and_map():从设备节点的interrupts属性中提取中断号:

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

    dev:设备节点指针;
    index:interrupts属性可能包含多条中断信息,用index指定要获取的条目;
    返回值:对应的中断号。

    3.2 从 GPIO 获取中断号

    gpio_to_irq():将 GPIO 编号转换为对应的中断号(GPIO 作为中断时使用):

    int gpio_to_irq(unsigned int gpio)

    gpio:要获取的 GPIO 编号;
    返回值:GPIO 对应的中断号。

    四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)

    4.1、硬件原理图

    按键 KEY0 的原理图如下:

    图中可以看出,按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的,KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。

    4.2、程序编写

    本章实验我们驱动 I.MX6U-ALPHA 开发板上的 KEY0 按键,不过我们采用中断的方式, 并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。

    4.2.1、修改设备树文件

    实验使用到了按键 KEY0 ,按键 KEY0 使用中断模式,因此需要在“ key ”节点下添加 中断相关属性,添加完成以后的“key ”节点内容如下所示:
    key {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "atkalpha-key";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_key>;
        key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
        interrupt-parent = <&gpio1>;           /* 中断控制器为gpio1 */
        interrupts = <18 IRQ_TYPE_EDGE_BOTH>;  /* 上升沿+下降沿触发 */
        status = "okay";
    };

    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>:指定 KEY0 为 GPIO1_IO18,低电平有效;
    interrupt-parent = <&gpio1>:KEY0 的中断控制器为gpio1;
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>:
    18:GPIO1 组的 18 号 IO(即 GPIO1_IO18);
    IRQ_TYPE_EDGE_BOTH:上升沿和下降沿同时触发(KEY0 按下和释放都会触发中断)。

    4.2.2、驱动代码编写(imx6uirq.c

    #include <linux/types.h>
    #include <linux/kernel.h>
    #include <linux/delay.h>
    #include <linux/ide.h>
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/errno.h>
    #include <linux/gpio.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/of.h>
    #include <linux/of_address.h>
    #include <linux/of_gpio.h>
    #include <linux/semaphore.h>
    #include <linux/timer.h>
    #include <linux/of_irq.h>
    #include <linux/irq.h>
    #include <asm/mach/map.h>
    #include <asm/uaccess.h>
    #include <asm/io.h>
    /***************************************************************
    Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
    文件名		: imx6uirq.c
    描述	   	: Linux中断驱动实验
    其他	   	: 无
    ***************************************************************/
    #define IMX6UIRQ_CNT		1			/* 设备号个数 	*/
    #define IMX6UIRQ_NAME		"imx6uirq"	/* 名字 		*/
    #define KEY0VALUE			0X01		/* KEY0按键值 	*/
    #define INVAKEY				0XFF		/* 无效的按键值 */
    #define KEY_NUM				1			/* 按键数量 	*/
    
    /* 中断IO描述结构体 */
    struct irq_keydesc {
    	int gpio;								/* gpio */
    	int irqnum;								/* 中断号     */
    	unsigned char value;					/* 按键对应的键值 */
    	char name[10];							/* 名字 */
    	irqreturn_t (*handler)(int, void *);	/* 中断服务函数 */
    };
    
    /* imx6uirq设备结构体 */
    struct imx6uirq_dev{
    	dev_t devid;			/* 设备号 	 */
    	struct cdev cdev;		/* cdev 	*/
    	struct class *class;	/* 类 		*/
    	struct device *device;	/* 设备 	 */
    	int major;				/* 主设备号	  */
    	int minor;				/* 次设备号   */
    	struct device_node	*nd; /* 设备节点 */
    	atomic_t keyvalue;		/* 有效的按键键值 */
    	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
    	struct timer_list timer;/* 定义一个定时器*/
    	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键描述数组 */
    	unsigned char curkeynum;				/* 当前的按键号 */
    };
    
    struct imx6uirq_dev imx6uirq;	/* irq设备 */
    
    /* @description		: 中断服务函数,开启定时器,延时10ms,
     *				  	  定时器用于按键消抖。
     * @param - irq 	: 中断号 
     * @param - dev_id	: 设备结构。
     * @return 			: 中断执行结果
     */
    static irqreturn_t key0_handler(int irq, void *dev_id)
    {
    	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
    
    	dev->curkeynum = 0;
    	dev->timer.data = (volatile long)dev_id;
    	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
    	return IRQ_RETVAL(IRQ_HANDLED);
    }
    
    /* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
     *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
     * @param - arg	: 设备结构变量
     * @return 		: 无
     */
    void timer_function(unsigned long arg)
    {
    	unsigned char value;
    	unsigned char num;
    	struct irq_keydesc *keydesc;
    	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
    
    	num = dev->curkeynum;
    	keydesc = &dev->irqkeydesc[num];
    
    	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
    	if(value == 0){ 						/* 按下按键 */
    		atomic_set(&dev->keyvalue, keydesc->value);
    	}
    	else{ 									/* 按键松开 */
    		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
    		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
    	}	
    }
    
    /*
     * @description	: 按键IO初始化
     * @param 		: 无
     * @return 		: 无
     */
    static int keyio_init(void)
    {
    	unsigned char i = 0;
    	int ret = 0;
    	
    	imx6uirq.nd = of_find_node_by_path("/key");
    	if (imx6uirq.nd== NULL){
    		printk("key node not find!\r\n");
    		return -EINVAL;
    	} 
    
    	/* 提取GPIO */
    	for (i = 0; i < KEY_NUM; i++) {
    		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
    		if (imx6uirq.irqkeydesc[i].gpio < 0) {
    			printk("can't get key%d\r\n", i);
    		}
    	}
    	
    	/* 初始化key所使用的IO,并且设置成中断模式 */
    	for (i = 0; i < KEY_NUM; i++) {
    		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name));	/* 缓冲区清零 */
    		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
    		gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
    		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
    		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
    #if 0
    		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
    #endif
    		printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, 
                                             imx6uirq.irqkeydesc[i].irqnum);
    	}
    	/* 申请中断 */
    	imx6uirq.irqkeydesc[0].handler = key0_handler;
    	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
    	
    	for (i = 0; i < KEY_NUM; i++) {
    		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
    		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
    		if(ret < 0){
    			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
    			return -EFAULT;
    		}
    	}
    
    	/* 创建定时器 */
    	init_timer(&imx6uirq.timer);
    	imx6uirq.timer.function = timer_function;
    	return 0;
    }
    
    /*
     * @description		: 打开设备
     * @param - inode 	: 传递给驱动的inode
     * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
     * 					  一般在open的时候将private_data指向设备结构体。
     * @return 			: 0 成功;其他 失败
     */
    static int imx6uirq_open(struct inode *inode, struct file *filp)
    {
    	filp->private_data = &imx6uirq;	/* 设置私有数据 */
    	return 0;
    }
    
     /*
      * @description     : 从设备读取数据 
      * @param - filp    : 要打开的设备文件(文件描述符)
      * @param - buf     : 返回给用户空间的数据缓冲区
      * @param - cnt     : 要读取的数据长度
      * @param - offt    : 相对于文件首地址的偏移
      * @return          : 读取的字节数,如果为负值,表示读取失败
      */
    static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    {
    	int ret = 0;
    	unsigned char keyvalue = 0;
    	unsigned char releasekey = 0;
    	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
    
    	keyvalue = atomic_read(&dev->keyvalue);
    	releasekey = atomic_read(&dev->releasekey);
    
    	if (releasekey) { /* 有按键按下 */	
    		if (keyvalue & 0x80) {
    			keyvalue &= ~0x80;
    			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
    		} else {
    			goto data_error;
    		}
    		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
    	} else {
    		goto data_error;
    	}
    	return 0;
    	
    data_error:
    	return -EINVAL;
    }
    
    /* 设备操作函数 */
    static struct file_operations imx6uirq_fops = {
    	.owner = THIS_MODULE,
    	.open = imx6uirq_open,
    	.read = imx6uirq_read,
    };
    
    /*
     * @description	: 驱动入口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init imx6uirq_init(void)
    {
    	/* 1、构建设备号 */
    	if (imx6uirq.major) {
    		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
    		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    	} else {
    		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    		imx6uirq.major = MAJOR(imx6uirq.devid);
    		imx6uirq.minor = MINOR(imx6uirq.devid);
    	}
    
    	/* 2、注册字符设备 */
    	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
    	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
    
    	/* 3、创建类 */
    	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
    	if (IS_ERR(imx6uirq.class)) {
    		return PTR_ERR(imx6uirq.class);
    	}
    
    	/* 4、创建设备 */
    	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
    	if (IS_ERR(imx6uirq.device)) {
    		return PTR_ERR(imx6uirq.device);
    	}
    	
    	/* 5、初始化按键 */
    	atomic_set(&imx6uirq.keyvalue, INVAKEY);
    	atomic_set(&imx6uirq.releasekey, 0);
    	keyio_init();
    	return 0;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit imx6uirq_exit(void)
    {
    	unsigned int i = 0;
    	/* 删除定时器 */
    	del_timer_sync(&imx6uirq.timer);	/* 删除定时器 */
    		
    	/* 释放中断 */
    	for (i = 0; i < KEY_NUM; i++) {
    		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
    		gpio_free(imx6uirq.irqkeydesc[i].gpio);
    	}
    	cdev_del(&imx6uirq.cdev);
    	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
    	device_destroy(imx6uirq.class, imx6uirq.devid);
    	class_destroy(imx6uirq.class);
    }
    
    module_init(imx6uirq_init);
    module_exit(imx6uirq_exit);
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("duan");
    

    4.2.3、驱动代码分段分析

    结构体定义(33-58 行)
    33 /* 中断IO描述结构体 */
    34 struct irq_keydesc {
    35 	int gpio;								/* gpio */
    36 	int irqnum;								/* 中断号     */
    37 	unsigned char value;					/* 按键对应的键值 */
    38 	char name[10];							/* 名字 */
    39 	irqreturn_t (*handler)(int, void *);	/* 中断服务函数 */
    40 };
    
    42 /* imx6uirq设备结构体 */
    43 struct imx6uirq_dev{
    44 	dev_t devid;			/* 设备号 	 */
    45 	struct cdev cdev;		/* cdev 	*/
    46 	struct class *class;	/* 类 		*/
    47 	struct device *device;	/* 设备 	 */
    48 	int major;				/* 主设备号	  */
    49 	int minor;				/* 次设备号   */
    50 	struct device_node	*nd; /* 设备节点 */
    51 	atomic_t keyvalue;		/* 有效的按键键值 */
    52 	atomic_t releasekey;	/* 标记是否完成一次完成的按键,包括按下和释放 */
    53 	struct timer_list timer;/* 定义一个定时器*/
    54 	struct irq_keydesc irqkeydesc[KEY_NUM];	/* 按键描述数组 */
    55 	unsigned char curkeynum;				/* 当前的按键号 */
    56 };
    
    58 struct imx6uirq_dev imx6uirq;	/* irq设备 */

    irq_keydesc 结构体(34 行):
    封装单个按键的所有关键信息,包括 GPIO 号、中断号、键值、中断处理函数;
    便于扩展多按键(只需增加数组元素,无需修改核心逻辑)。
    imx6uirq_dev 结构体(43 行):
    字符设备驱动标准成员(44-49 行):设备号、cdev、类 / 设备节点;
    设备树成员(50 行):指向设备树中 key 节点的指针;
    原子变量(51-52 行):keyvalue存储按键值,releasekey标记完整按键过程(按下 + 释放),原子变量避免多线程竞态;
    定时器成员(53 行):用于按键消抖的定时器;
    按键描述数组(54 行):支持多按键扩展;
    全局设备实例(58 行):整个驱动的核心数据载体。

    中断处理函数(66-74 行)
    66 static irqreturn_t key0_handler(int irq, void *dev_id)
    67 {
    68 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
    69 
    70 	dev->curkeynum = 0;
    71 	dev->timer.data = (volatile long)dev_id;
    72 	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
    73 	return IRQ_RETVAL(IRQ_HANDLED);
    74 }

    函数作用:KEY0 中断触发后执行的上半部处理函数,遵循 “快进快出” 原则;
    关键逻辑:
    68 行:将dev_id转换为设备结构体指针,获取全局设备实例;
    70 行:标记当前触发中断的是第 0 个按键(KEY0);
    71 行:设置定时器私有数据为设备结构体指针;
    72 行:启动 10ms 定时器(msecs_to_jiffies将毫秒转换为内核节拍数),用于按键消抖;
    73 行:标准中断返回值,告知内核中断已处理完成。

    定时器消抖函数(81-99 行)
    81 void timer_function(unsigned long arg)
    82 {
    83 	unsigned char value;
    84 	unsigned char num;
    85 	struct irq_keydesc *keydesc;
    86 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
    87 
    88 	num = dev->curkeynum;
    89 	keydesc = &dev->irqkeydesc[num];
    90 
    91 	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
    92 	if(value == 0){ 						/* 按下按键 */
    93 		atomic_set(&dev->keyvalue, keydesc->value);
    94 	}
    95 	else{ 									/* 按键松开 */
    96 		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
    97 		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
    98 	}	
    99 }

    函数作用:10ms 定时到期后执行,读取 GPIO 电平确认按键真实状态(消抖核心);
    关键逻辑:
    86 行:将定时器参数转换为设备结构体指针;
    91 行:读取 GPIO 电平(0 = 按下,1 = 松开);
    92-94 行:按键按下时,设置keyvalue为 KEY0VALUE(0x01);
    95-98 行:按键松开时,keyvalue最高位置 1(0x81),并设置releasekey为 1,标记一次完整按键过程;
    原子操作:atomic_set保证多线程下按键状态的原子性,避免竞态。

    按键 IO 初始化函数(106-155 行)
    106 static int keyio_init(void)
    107 {
    108 	unsigned char i = 0;
    109 	int ret = 0;
    110 	
    111 	imx6uirq.nd = of_find_node_by_path("/key");
    112 	if (imx6uirq.nd== NULL){
    113 		printk("key node not find!\r\n");
    114 		return -EINVAL;
    115 	} 
    116 
    117 	/* 提取GPIO */
    118 	for (i = 0; i < KEY_NUM; i++) {
    119 		imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i);
    120 		if (imx6uirq.irqkeydesc[i].gpio < 0) {
    121 			printk("can't get key%d\r\n", i);
    122 		}
    123 	}
    124 	
    125 	/* 初始化key所使用的IO,并且设置成中断模式 */
    126 	for (i = 0; i < KEY_NUM; i++) {
    127 		memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name));	/* 缓冲区清零 */
    128 		sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
    129 		gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name);
    130 		gpio_direction_input(imx6uirq.irqkeydesc[i].gpio);	
    131 		imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i);
    132 #if 0
    133 		imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio);
    134 #endif
    135 		printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, 
    136                                          imx6uirq.irqkeydesc[i].irqnum);
    137 	}
    138 	/* 申请中断 */
    139 	imx6uirq.irqkeydesc[0].handler = key0_handler;
    140 	imx6uirq.irqkeydesc[0].value = KEY0VALUE;
    141 	
    142 	for (i = 0; i < KEY_NUM; i++) {
    143 		ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 
    144 		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq);
    145 		if(ret < 0){
    146 			printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum);
    147 			return -EFAULT;
    148 		}
    149 	}
    150 
    151 	/* 创建定时器 */
    152 	init_timer(&imx6uirq.timer);
    153 	imx6uirq.timer.function = timer_function;
    154 	return 0;
    155 }

    核心解析
    设备树节点查找(111 行):
    of_find_node_by_path("/key"):查找设备树中路径为/key的节点;
    失败则返回-EINVAL,驱动初始化失败。
    GPIO 提取(118-123 行):
    of_get_named_gpio:从key节点的key-gpio属性中提取 GPIO 编号;
    支持多按键扩展(只需修改KEY_NUM和设备树)。
    GPIO 初始化(126-137 行):
    129 行:申请 GPIO 资源,避免资源冲突;
    130 行:设置 GPIO 为输入模式(按键检测);
    131 行:从设备树解析中断号(推荐方式);
    133 行:备选方案(GPIO 转中断号),适合无设备树场景。
    中断申请(142-149 行):
    request_irq参数说明:
    参数 1:中断号;
    参数 2:中断处理函数(key0_handler);
    参数 3:触发方式(上升沿 + 下降沿);
    参数 4:中断名(/proc/interrupts 可查);
    参数 5:传递给中断处理函数的私有数据(设备结构体);
    触发方式与设备树中IRQ_TYPE_EDGE_BOTH对应。
    定时器初始化(152-153 行):
    init_timer:初始化定时器;
    设置定时器处理函数为timer_function(消抖核心)。

    字符设备操作函数(164-210 行)
    164 static int imx6uirq_open(struct inode *inode, struct file *filp)
    165 {
    166 	filp->private_data = &imx6uirq;	/* 设置私有数据 */
    167 	return 0;
    168 }
    
    178 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
    179 {
    180 	int ret = 0;
    181 	unsigned char keyvalue = 0;
    182 	unsigned char releasekey = 0;
    183 	struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
    184 
    185 	keyvalue = atomic_read(&dev->keyvalue);
    186 	releasekey = atomic_read(&dev->releasekey);
    187 
    188 	if (releasekey) { /* 有按键按下 */	
    189 		if (keyvalue & 0x80) {
    190 			keyvalue &= ~0x80;
    191 			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
    192 		} else {
    193 			goto data_error;
    194 		}
    195 		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
    196 	} else {
    197 		goto data_error;
    198 	}
    199 	return 0;
    200 	
    201 data_error:
    202 	return -EINVAL;
    203 }
    
    205 /* 设备操作函数 */
    206 static struct file_operations imx6uirq_fops = {
    207 	.owner = THIS_MODULE,
    208 	.open = imx6uirq_open,
    209 	.read = imx6uirq_read,
    210 };

    open 函数(164 行):
    将设备结构体指针赋值给filp->private_data,便于后续 read 函数获取设备实例;
    字符设备驱动标准操作,无额外逻辑。
    read 函数(178 行):
    185-186 行:读取原子变量,获取按键值和按键完成标记;
    188 行:仅当releasekey=1(完成一次按键过程)时,才向用户空间返回数据;
    189-191 行:清除keyvalue最高位(松开标记),通过copy_to_user将按键值拷贝到用户空间;
    195 行:清零releasekey,准备下一次按键检测;
    核心注意:copy_to_user是内核空间向用户空间拷贝数据的标准函数,不能直接赋值(内存隔离)。

    驱动入口 / 出口函数(217-272 行)
    217 static int __init imx6uirq_init(void)
    218 {
    219 	/* 1、构建设备号 */
    220 	if (imx6uirq.major) {
    221 		imx6uirq.devid = MKDEV(imx6uirq.major, 0);
    222 		register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    223 	} else {
    224 		alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    225 		imx6uirq.major = MAJOR(imx6uirq.devid);
    226 		imx6uirq.minor = MINOR(imx6uirq.devid);
    227 	}
    228 
    229 	/* 2、注册字符设备 */
    230 	cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
    231 	cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
    232 
    233 	/* 3、创建类 */
    234 	imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
    235 	if (IS_ERR(imx6uirq.class)) {
    236 		return PTR_ERR(imx6uirq.class);
    237 	}
    238 
    239 	/* 4、创建设备 */
    240 	imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
    241 	if (IS_ERR(imx6uirq.device)) {
    242 		return PTR_ERR(imx6uirq.device);
    243 	}
    244 	
    245 	/* 5、初始化按键 */
    246 	atomic_set(&imx6uirq.keyvalue, INVAKEY);
    247 	atomic_set(&imx6uirq.releasekey, 0);
    248 	keyio_init();
    249 	return 0;
    250 }
    
    257 static void __exit imx6uirq_exit(void)
    258 {
    259 	unsigned int i = 0;
    260 	/* 删除定时器 */
    261 	del_timer_sync(&imx6uirq.timer);	/* 删除定时器 */
    262 		
    263 	/* 释放中断 */
    264 	for (i = 0; i < KEY_NUM; i++) {
    265 		free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq);
    266 		gpio_free(imx6uirq.irqkeydesc[i].gpio);
    267 	}
    268 	cdev_del(&imx6uirq.cdev);
    269 	unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
    270 	device_destroy(imx6uirq.class, imx6uirq.devid);
    271 	class_destroy(imx6uirq.class);
    272 }
    
    274 module_init(imx6uirq_init);
    275 module_exit(imx6uirq_exit);
    276 MODULE_LICENSE("GPL");
    277 MODULE_AUTHOR("duan");

    入口函数(imx6uirq_init)
    设备号处理(220-227 行):
    静态指定主设备号:若major不为 0,使用register_chrdev_region注册;
    动态分配设备号:若major为 0,使用alloc_chrdev_region分配,自动获取主 / 次设备号;
    动态分配是推荐方式,避免设备号冲突。
    字符设备注册(230-231 行):
    cdev_init:初始化 cdev 结构体,绑定设备操作集;
    cdev_add:将 cdev 添加到内核,完成字符设备注册。
    类 / 设备创建(234-243 行):
    class_create:创建类,用于自动生成设备节点;
    device_create:创建设备节点(/dev/imx6uirq);
    IS_ERR:检查创建是否失败,避免空指针操作。
    按键初始化(246-248 行):
    初始化原子变量为默认值;
    调用keyio_init完成 GPIO / 中断 / 定时器初始化。
    出口函数(imx6uirq_exit)
    资源释放顺序:
    先释放定时器(261 行):del_timer_sync等待定时器完成,避免竞态;
    再释放中断 / GPIO(264-267 行):free_irq释放中断,gpio_free释放 GPIO 资源;
    最后释放字符设备资源(268-271 行):注销 cdev、设备号、销毁设备 / 类;
    核心原则:与初始化顺序相反,避免资源泄漏。

    4.2.3、测试 APP 代码

    #include "stdio.h"
    #include "unistd.h"
    #include "sys/types.h"
    #include "sys/stat.h"
    #include "fcntl.h"
    #include "stdlib.h"
    #include "string.h"
    #include "linux/ioctl.h"
    
    int main(int argc, char *argv[])
    {
        int fd;
        int ret = 0;
        char *filename;
        unsigned char data;
    
        if (argc != 2) {
            printf("Error Usage!\r\n");
            return -1;
        }
    
        filename = argv[1];
        fd = open(filename, O_RDWR);
        if (fd < 0) {
            printf("Can't open file %s\r\n", filename);
            return -1;
        }
    
        while (1) {
            ret = read(fd, &data, sizeof(data));
            if (ret >= 0 && data) { /* 读取到有效按键值 */
                printf("key value = %#X\r\n", data);
            }
        }
    
        close(fd);
        return ret;
    }
    45~53 行的 while 循环用于不断的读取按键值,如果读取到有效的按键值就将其输出到终端上。

    五、运行测试

    5.1、编译驱动程序和测试 APP

    编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为imx6uirq.o,Makefile 内容如下所示:

    KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
    CURRENT_PATH := $(shell pwd)
    
    obj-m :=imx6uirq.o
    
    build: kernel_modules
    
    kernel_modules:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
    clean:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    	

    第 4 行,设置 obj-m 变量的值为imx6uirq.o

    输入如下命令编译出驱动模块文件:

    make -j32

    编译成功以后就会生成一个名为“imx6uirq.ko”的驱动模块文件。

    输入如下命令编译出驱动模块文件:

    make -j32

    编译成功以后就会生成一个名为“key.ko”的驱动模块文件。

    编译测试 APP

    输入如下命令编译测试试imx6uirqApp.c这个测试程序:

    arm-linux-gnueabihf-gcc imx6uirqApp.c -o  imx6uirqApp

    编译成功以后就会生成 keyApp 这个应用程序。

     5.2、运行测试

    将上一小节编译出来的imx6uirq.ko imx6uirqApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

    sudo cp imx6uirq.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
    sudo cp  imx6uirqApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

    进入到目录 lib/modules/4.1.15 中,输入如下命令加载imx6uirq.ko 驱动模块:

    depmod //第一次加载驱动的时候需要运行此命令
    modprobe imx6uirq.ko //加载驱动

    驱动加载成功可通过查看/proc/interrupts 来检查一下对应的中断有没有被注册上,输入如下命令:

    cat /proc/interrupts
    接下来使用如下命令来测试中断:
    ./imx6uirqApp /dev/imx6uirq

    按下开发板上的 KEY0 键,终端就会输出按键值,如图 51.4.2.2 所示:

    从图 51.4.2.2 可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:
    rmmod imx6uirq.ko

    总结

    完成了I.MX6U Linux 驱动中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)。

    转载自CSDN-专业IT技术社区

    原文链接:https://blog.csdn.net/weixin_61186812/article/details/159004404

    评论

    赞0

    评论列表

    微信小程序
    QQ小程序

    关于作者

    点赞数:0
    关注数:0
    粉丝:0
    文章:0
    关注标签:0
    加入于:--