Linux中断——嵌入式Linux驱动开发

参考正点原子I.MX6U嵌入式Linux驱动开发指南

一、简介

先来简单了解一般中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找 具体的中断处理函数,找到以后执行相应的中断处理函数。 Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的 API 函数:
1 、中断号
        每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。
2 request_irq 函数
        在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断, request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 数。 request_irq 函数会激活 ( 使能 ) 中断,所以不需要我们手动去使能中断, request_irq 函数原型 如下:
int request_irq(unsigned int irq,
                        irq_handler_t  handler,
                        unsigned long flags,
                        const char *name,
                        void *dev)
函数参数和返回值含义如下:
irq :要申请中断的中断号。
handler :中断处理函数,当中断发生以后就会执行此中断处理函数。
flags :中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们
介绍几个常用的中断标志:

name :中断名字,设置以后可以在 /proc/interrupts 文件中看到对应的中断名字。
dev 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY 的话表示中断已经被申请了。
3 free_irq 函数
        使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。 free_irq 函数原型如下所示:
void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq 要释放的中断。
dev :如果中断设置为共享 (IRQF_SHARED) 的话,此参数用来区分具体的中断。共享中断
只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。。
4 、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
        第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就 是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
        中断处理函数的返回值为 irqreturn_t 类型, irqreturn_t 类型定义如下所示:
enum irqreturn {
    IRQ_NONE = (0 << 0),
    IRQ_HANDLED = (1 << 0),
    IRQ_WAKE_THREAD = (1 << 1),
};

typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
5 、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
        enable_irq 和 disable_irq 用于使能和禁止指定的中断, irq 就是要禁止的中断号。disable_irq
函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外 一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
        disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就 是在学习 STM32 的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
        local_irq_enable 用于使能当前处理器中断系统, local_irq_disable 用于禁止当前处理器中断 系统。假如 A 任务调用 local_irq_disable 关闭全局中断 10S ,当关闭了 2S 的时候 B 任务开始运 行, B 任务也调用 local_irq_disable 关闭全局中断 3S 3 秒以后 B 任务调用 local_irq_enable 数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时 A 任务要 关闭 10S 全局中断的愿望就破灭了,然后 A 任务就“生气了”,结果很严重,可能系统都要被 A 任务整崩溃。为了解决这个问题, B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打 开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下 面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
        这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。 local_irq_restore 用于恢复中断,将中断到 flags 状态。

二、上半部与下半部

        在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用 request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么 中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实 往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理 函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生, SOC 响应中断,然后 通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有 400Kbit/S ,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据 的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。
        这个时候中断处理 过程就分为了两部分:
        上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以
                      放在上半部完成。
        下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去
                      执行,这样中断处理函数就会快进快出。
        因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所 有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就 可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定, 一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的 参考点:
        ①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
        ②、如果要处理的任务对时间敏感,可以放到上半部。
        ③、如果要处理的任务与硬件有关,可以放到上半部
        ④、除了上述三点以外的其他任务,优先考虑放到下半部。
        上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。
1 、软中断
        一开始 Linux 内核提供了“ bottom half ”机制来实现下半部,简称“ BH ”。后面引入了软中 断和 tasklet 来替代“ BH ”机制,完全可以使用软中断和 tasklet 来替代 BH ,从 2.5 版本的 Linux 内核开始 BH 已经被抛弃了。 Linux 内核使用结构体 softirq_action 表示软中断, softirq_action 结构体定义在文件 include/linux/interrupt.h 中,内容如下:
struct softirq_action
{
    void (*action)(struct softirq_action *);
};
kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
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
};
        可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 10 ,因此数组 softirq_vec 10 元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个 全局数组,因此所有的 CPU( 对于 SMP 系统而言 ) 都可以访问到,每个 CPU 都有自己的触发和 控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同 的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注 册对应的软中断处理函数, open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
函数参数和返回值含义如下:
nr :要开启的软中断
action :软中断对应的处理函数。
返回值: 没有返回值。
注册好软中断以后需要通过 raise_softirq 函数触发, raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
函数参数和返回值含义如下:
nr :要触发的软中断
返回值: 没有返回值。
        软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断, softirq_init 函数定义在 kernel/softirq.c 文件里面,函数内容如下:
void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu)
    {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ
2 tasklet
        tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使 tasklet Linux 内核使用 tasklet_struct 结构体来表示 tasklet
struct tasklet_struct
{
    struct tasklet_struct *next; /* 下一个 tasklet */
    unsigned long state;         /* tasklet 状态 */
    atomic_t count;              /* 计数器,记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数 */
    unsigned long data;          /* 函数 func 的参数 */
};
        func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理 函数。如果要使用 tasklet ,必须先定义一个 tasklet ,然后使用 tasklet_init 函数初始化 tasklet taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct *t,
                        void (*func)(unsigned long),
                        unsigned long data);
函数参数和返回值含义如下:
t :要初始化的 tasklet
func tasklet 的处理函数。
data 要传递给 func 函数的参数
返回值: 没有返回值。
        也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化, DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下 :
DECLARE_TASKLET(name, func, data)
        其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的变量, func 就是 tasklet 的处理函数, data 是传递给 func 函数的参数。
        在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运 行, tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
t :要调度的 tasklet ,也就是 DECLARE_TASKLET 宏里面的 name
返回值: 没有返回值。
2 、工作队列
        工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的 工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重 新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软 中断或 tasklet
      本文不对工作队列进行详细讲解,想深入了解的可以参考 正点原子I.MX6U嵌入式Linux驱动开发指南 51.1小节。

三、设备树中断节点

        如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树 中 的中断属性信息来配置中断。 对于中断控制器而言 ,设备树绑定信息参考文档 Documentation/devicetree/bindings/arm/gic.txt 。打开 imx6ull.dtsi 文件,其中的 intc 节点就是 I.MX6ULL 的中断控制器节点,节点内容如下所示:

intc: interrupt-controller@00a01000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0x00a01000 0x1000>,
          <0x00a02000 0x100>;
};

2 行, compatible 属性值为“ arm,cortex-a7-gic ”在 Linux 内核源码中搜索“ arm,cortex-a7-gic ”即可找到 GIC 中断控制器驱动文件。
3 行, #interrupt-cells #address-cells #size-cells 一样。表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属 性的 cells 大小,也就是一条信息有几个 cells 。每个 cells 都是 32 位整形值,对于 ARM 处理的 GIC 来说,一共有 3 cells ,这三个 cells 的含义如下:
        第一个 cells :中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。
        第二个 cells :中断号,对于 SPI 中断来说中断号的范围为 0~987 ,对于 PPI 中断来说中断号
                               的范围为 0~15。
        第三个 cells :标志, bit[3:0] 表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表
                               示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触
                               发.bit[15:8]为 PPI 中断的 CPU 掩码。
4 行, interrupt-controller 节点为空,表示当前节点是中断控制器。
        对于 gpio 来说, 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>;
};

        第 4 行, interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI 触发电平都是 IRQ_TYPE_LEVEL_HIGH 。不同之处在于中断源,一个是 74 ,一个是 75 ,打开 可以打开《 IMX6ULL 参考手册》的“ Chapter 3 Interrupts and DMA Events ”章节,找到表 3-1

        可以看出,GPIO5 一共用了 2 个中断号,一个是 74 ,一个是 75 。其中 74 GPIO5_IO00~GPIO5_IO15 这低 16 IO 75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 IO
8 行, interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断。
9 行,将 #interrupt-cells 修改为 2 。打开 imx6ull-lyh-emmc.dts 文件,找到如下所示内容:
 
1 fxls8471@1e {
2   compatible = "fsl,fxls8471";
3   reg = <0x1e>;
4   position = <0>;
5   interrupt-parent = <&gpio5>;
6   interrupts = <0 8>;
7 };
        fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片, fxls8471 有一个中断引脚链接到了 I.MX6ULL SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00
        第 5 行, interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
        第 6 行, interrupts 设置中断信息, 0 表示 GPIO5_IO00 8 表示低电平触发。
        简单总结一下与中断有关的设备树属性信息:
        ①、#interrupt-cells ,指定中断源的信息 cells 个数。
        ②、interrupt-controller ,表示当前节点为中断控制器。
        ③、interrupts ,指定中断号,触发方式等。
        ④、interrupt-parent ,指定父中断,也就是中断控制器。
获取中断号 :
        编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因 此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,  int index)
函数参数和返回值含义如下:
dev 设备节点。
index :索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如
下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio 要获取的 GPIO 编号。
返回值: GPIO 对应的中断号。

 四、示例实验

4.1 设备树

添加完成以后的 key 节点和 pinctrl 内容如下所示:

key {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "lyh-key";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_key>;
        led-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
        interrupt-parent = <&gpio1>;
        interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
        status = "okay";
    };

…………

pinctrl_key: keygrp {
            fsl,pins = <
                MX6UL_PAD_UART1_CTS_B__GPIO1_IO18    0xF080  /* KEY */
            >;
        };

8 行,设置 interrupt-parent 属性值为“ gpio1 ”,因为 KEY0 所使用的 GPIO 为 GPIO1_IO18 ,也就是设置 KEY0 GPIO 中断控制器为 gpio1
9 行,设置 interrupts 属性,也就是设置中断源,第一个 cells 18 表示 GPIO1 组的 18 号 IO IRQ_TYPE_EDGE_BOTH 定义在文件 include/linux/irq.h 中。IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效。

4.2 中断驱动

#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.
文件名		 : keyirq.c
作者	  	: lyh
版本	   	: V1.0
描述	   	: LED驱动文件。
其他	   	: 无
日志	   	: 初版V1.0 2024/4/14 lyh创建
***************************************************************/
#define KEY_CNT 	1 		/* 设备号个数 */
#define KEY_NAME 	"key" 	/* 名字 */
#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 *); /* 中断服务函数 */
};

/* key设备结构体 */
struct key_dev{
	dev_t devid;								/* 设备号 	 */
	struct cdev cdev;							/* cdev 	*/
	struct class *class;						/* 类 		*/
	struct device *device;						/* 设备 	 */
	int major;									/* 主设备号	  */
	int minor;									/* 次设备号   */
	struct device_node *nd; 					/* 设备节点 */
	struct timer_list timer;
	atomic_t key_value;
	atomic_t releasekey;
	struct irq_keydesc irqkeydesc[KEY_NUM];		/* 按键描述数组 */
	unsigned char curkeynum; 					/* 当前的按键号 */
};

struct key_dev key;	/* key设备 */

/* @description : 中断服务函数,开启定时器,延时 10ms,
 * 定时器用于按键消抖。
 * @param - irq : 中断号
 * @param - dev_id : 设备结构。
 * @return : 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct key_dev *dev = (struct key_dev *)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (unsigned long)dev;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
	return IRQ_RETVAL(IRQ_HANDLED);
}

void timer_func(unsigned long arg)
{
	struct key_dev *dev = (struct key_dev *)arg;
	unsigned char value;
	int num = dev->curkeynum;
	struct irq_keydesc *keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio);
	if(value == 0){			//按下按键
		atomic_set(&dev->key_value, keydesc->value);
	} else {				//松开按键
		atomic_set(&dev->key_value, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);
	}
	return;
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int key_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &key; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t key_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 key_dev *dev = (struct key_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->key_value);
	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;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	

	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = 	key_release,
};

static int keyio_init(void)
{
	int ret;
	int i;
	/* 找到设备节点 */
	key.nd = of_find_node_by_path("/key");
	if(key.nd == NULL) {
		printk("key node cant not found!\r\n");
		return -EINVAL;
	} else {
		printk("key node has been found!\r\n");
	}

	/* 获取设备树中的 gpio 编号 */
	for(i = 0; i < KEY_NUM; i++){
		key.irqkeydesc[i].gpio = of_get_named_gpio(key.nd, "key-gpio", i);
		if(key.irqkeydesc[i].gpio < 0){
			printk("can't get key-gpio %d", i);
			return -EINVAL;
		}
	}

	/* gpio 初始化,申请中断号 */
	for(i = 0; i < KEY_NUM; i++){
		memset(key.irqkeydesc[i].name, 0, sizeof(key.irqkeydesc[i].name));
		sprintf(key.irqkeydesc[i].name, "key%d", i);
		gpio_request(key.irqkeydesc[i].gpio, key.irqkeydesc[i].name);
		ret = gpio_direction_input(key.irqkeydesc[i].gpio);
		if(ret < 0) {
			printk("can't set gpio %d direction!\r\n", i);
			return -EINVAL;
		}

		key.irqkeydesc[i].irqnum = gpio_to_irq(key.irqkeydesc[i].gpio);
		#if 0
		key.irqkeydesc[i].irqnum = irq_of_parse_and_map(key.nd, i);
		#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n", i, key.irqkeydesc[i].gpio, 
												key.irqkeydesc[i].irqnum);
	}
		/* 申请中断 */
	key.irqkeydesc[0].handler = key0_handler;
	key.irqkeydesc[0].value = KEY0VALUE;
	for (i = 0; i < KEY_NUM; i++){
		ret = request_irq(key.irqkeydesc[i].irqnum,
						  key.irqkeydesc[i].handler,
						  IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
						  key.irqkeydesc[i].name, 
						  &key);
		if (ret < 0){
			printk("irq %d request failed!\r\n", key.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

 		/* 创建定时器 */
	init_timer(&key.timer);
	key.timer.function = timer_func;

	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyirq_init(void)
{

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (key.major) {		/*  定义了设备号 */
		key.devid = MKDEV(key.major, 0);
		register_chrdev_region(key.devid, KEY_CNT, KEY_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
		key.major = MAJOR(key.devid);	/* 获取分配号的主设备号 */
		key.minor = MINOR(key.devid);	/* 获取分配号的次设备号 */
	}
	printk("key major=%d,minor=%d\r\n",key.major, key.minor);	
	
	/* 2、初始化cdev */
	key.cdev.owner = THIS_MODULE;
	cdev_init(&key.cdev, &key_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&key.cdev, key.devid, KEY_CNT);

	/* 4、创建类 */
	key.class = class_create(THIS_MODULE, KEY_NAME);
	if (IS_ERR(key.class)) {
		return PTR_ERR(key.class);
	}

	/* 5、创建设备 */
	key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);
	if (IS_ERR(key.device)) {
		return PTR_ERR(key.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&key.key_value, INVAKEY);
	atomic_set(&key.releasekey, 0);
	
	keyio_init();

	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&key.timer); 

	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(key.irqkeydesc[i].irqnum, &key);
		gpio_free(key.irqkeydesc[i].gpio);
	}

	/* 注销字符设备驱动 */
	cdev_del(&key.cdev);/*  删除cdev */
	unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */

	device_destroy(key.class, key.devid);
	class_destroy(key.class);
}

module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lyh");

4.3 测试应用

#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){ 
            /* 数据读取错误或者无效 */
        }
        else{             /* 数据读取正确 */
            if (data) /* 读取到数据 */
                printf("key value = %#X\r\n", data);
        }
    }
    close(fd);
    return ret;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/547678.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

YOLOv8改进 添加大核卷积序列注意力机制LSK

一、Large Separable Kernel Attention论文 论文地址:2309.01439.pdf (arxiv.org) 二、Large Separable Kernel Attention注意力结构 LSK通过使用大型可分离卷积核来提升注意力机制的效果。在传统的注意力机制中,常用的是小型卷积核,如1x1卷积,来计算注意力权重和特征表示…

坦桑尼亚公司注册

哈喽大家好我是小鱼~ 很多朋友想了解坦桑尼亚公司的注册信息&#xff0c;今天来给大家分享&#xff1a; 坦桑尼亚随着投资环境的不断改善&#xff0c;成为非洲区域贸易集团成员&#xff0c;更好的为公司提供了广阔的非洲市场基础和劳动力优势&#xff0c;与20多个国家签订了避…

海外云手机怎么解决tiktok运营难题?

最近打算做TikTok的商家越来越多了&#xff0c;而做TikTok的第一步就面临如何养号、涨粉的困境&#xff0c;本文将介绍如何通过海外云手机轻松解决这些问题。 早期大家用的比较多的&#xff0c;是真机科学上网的方法。但是这种方法&#xff0c;需要自己搭建海外环境&#xff0c…

【小程序】生成短信中可点击的链接

文章目录 前言一、如何生成链接二、仔细拜读小程序开发文档文档说明1文档说明2 总结 前言 由于线上运营需求&#xff0c;需要给用户发送炮轰短信&#xff0c;用户通过短信点击链接直接跳转进入小程序 一、如何生成链接 先是找了一些三方的&#xff0c;生成的倒是快速&#xf…

BCLinux8U6系统部署oceanbase分布式数据库社区版之二、数据库服务器准备

本文是在完成步骤一、准备 OBD 中控机后的第二步&#xff0c;准备3台oceanbase分布式数据库服务器。 前序步骤&#xff1a;BCLinux8U6系统部署oceanbase分布式数据库社区版之一、准备 OBD 中控机 一、服务器配置 1、服务器硬件配置 本例采用vmware虚拟机来构建测试平台&…

c++ 多文件编程

1.结构目录 声明类:用于声明方法,方便方法管理和调用&#xff1b; 实现类:用于实现声明的方法; 应用层:调用方法使用 写过java代码的兄弟们可以这么理解&#xff1a; 声明类 为service层 实现类 为serviceimpl层 应用层 为conlloter层 2.Dome 把函数声明放在头文件xxx.h中&…

端口映射软件可以做什么? 快解析如何设置端口映射?

说到端口映射&#xff0c;首先说说nat。简单地说&#xff0c;nat就是在局域网内部网络中使用内部地址&#xff0c;而当内部节点要与外部网络进行通讯时&#xff0c;就在网关处&#xff0c;将内部地址替换成公用地址&#xff0c;从而在外部公网&#xff08;internet&#xff09;…

自定义类似微信效果Preference

1. 为自定义Preference 添加背景&#xff1a;custom_preference_background.xml <?xml version"1.0" encoding"utf-8"?> <selector xmlns:android"http://schemas.android.com/apk/res/android"><item><shape android:s…

nginx的启动,systemd管理

service unit file文件通常由三部分组成&#xff1a; [Unit]&#xff1a;定义与Unit类型无关的通用选项&#xff1b;用于提供unit的描述信息、unit行为及依赖关系等 [Service]&#xff1a;与特定类型相关的专用选项&#xff1b;此处为Service类型 [Install]&#xff1a;定义…

AI人工智能学习指南:入门必备的五大步骤

人工智能的发展正以前所未有的速度推动着技术、商业和社会的变革。在这个迅速发展的领域中&#xff0c;个人掌握新技能和知识至关重要。在这篇文章中&#xff0c;我将为您提供一份人工智能学习指南&#xff0c;帮助您打开人工智能的大门。 1.了解人工智能的基本概念和应用 人工…

表单自定义系统源码:自主创建表单 带完整的安装代码包以及搭建教程

在当今信息化社会&#xff0c;表单作为一种常见的数据收集工具&#xff0c;广泛应用于各类网站和系统中。然而&#xff0c;传统的表单系统往往功能单一&#xff0c;缺乏灵活性&#xff0c;难以满足用户多样化的需求。下面&#xff0c;小编给大家分享一款表单自定义系统源码&…

OpenAI官宣位于东京的首个亚洲办公室,并将发布专为日语优化的GPT-4定制模型!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

vscode和pycharm等idea编写protobuf文件格式化

想在pycharm或者goland等idea中开发protobuf文件的话&#xff0c;可以安装一个插件&#xff1a;protocol-buffers 安装之后&#xff0c;proto文件就会支持高亮和格式化了。 如果是vscode想要编写proto文件&#xff0c;可以安装另外一个插件&#xff1a;vscode-proto3 安装后&a…

202303青少年软件编程(scratch图形化)等级考试试卷(四级)

第1题&#xff1a;【 单选题】 编写一段程序&#xff0c;从26个英文字母中&#xff0c;随机选出10个加入列表a。空白处应填入的代码是&#xff1f;&#xff08;&#xff09; A: B: C: D: 【正确答案】: C 【试题解析】 : 第2题&#xff1a;【 单选题】 运行以下代码&…

Matlab 将数据写入excel文件

Matlab 将数据写入excel文件 函数&#xff1a;writematrix 功能&#xff1a;将数据写入文件 语法 writematrix(A) writematrix(A,filename) writematrix(___,Name,Value) 说明 writematrix(A) 将同构数组 A 写入以逗号分隔的文本文件。文件名为数组的工作区变量名称&…

20240415,构造函数和析构函数,拷贝构造函数调用时机规则

二&#xff0c;对象的初始化和清理 2.1 构造函数和析构函数 解决初始化和清理问题&#xff0c;编译器自动调用&#xff0c;如果不提供&#xff0c;编译器提供&#xff0c;但空实现 构造函数&#xff1a;类名&#xff08;&#xff09;{ }&#xff1b;没用返回值也不写VOID&…

spring03:bean的自动装配

spring03&#xff1a;bean的自动装配 文章目录 spring03&#xff1a;bean的自动装配前言&#xff1a;一、 在xml中显示的配置&#xff1a;分析&#xff1a; People类&#xff1a;Cat类&#xff1a;Dog类&#xff1a;1. 在xml中显示的配置&#xff1a; 二、 隐式的自动装配bean【…

2048天的创作旅程:坚持与成长

纪念成为创作者的2048天 一、引言二、坚持创作三、创作心情四、技术成长五、小结 &#x1f4a1;一个热爱分享高性能服务器后台开发知识的博主&#xff0c;目标是通过理论与代码实践的结合&#xff0c;让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域&#x…

R语言使用installr包对R包进行整体迁移

今天分享一个R语言的实用小技巧&#xff0c;如果咱们重新安装了电脑&#xff08;我重装了电脑&#xff09;或者因为需要卸载旧版本的R软件&#xff0c;安装新版本的R&#xff0c;那么必然会造成R包的库缺失&#xff0c;需要重新下载&#xff0c;有些还不是官方的R包&#xff0c…

页缓存(PageCache)和预读机制(readahead )

页缓存&#xff08;PageCache)和预读机制&#xff08;readahead &#xff09; 页缓存&#xff08;PageCache)是操作系统&#xff08;OS&#xff09;对文件的缓存&#xff0c;用于加速对文件的读写。 page 是内存管理分配的基本单位&#xff0c; Page Cache 由多个 page 构成&…