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;
}

 

相关推荐

  1. Linux嵌入驱动开发-内核定时器

    2024-04-20 14:36:08       34 阅读
  2. 嵌入linux驱动开发之网络设备驱动

    2024-04-20 14:36:08       45 阅读
  3. 嵌入Linux:如何进行嵌入Linux开发

    2024-04-20 14:36:08       54 阅读
  4. Linux驱动开发——(五)内核中断

    2024-04-20 14:36:08       25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-20 14:36:08       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-20 14:36:08       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-20 14:36:08       82 阅读
  4. Python语言-面向对象

    2024-04-20 14:36:08       91 阅读

热门阅读

  1. Excel遇上VBA,开启智能联网新篇章!

    2024-04-20 14:36:08       34 阅读
  2. 开源AI Agent框架的选择

    2024-04-20 14:36:08       34 阅读
  3. 驱动开发platform传地址,led点灯

    2024-04-20 14:36:08       31 阅读
  4. MySQL -事务、锁

    2024-04-20 14:36:08       33 阅读
  5. C# 截图并保存为图片

    2024-04-20 14:36:08       38 阅读
  6. 整洁代码:定义和原则

    2024-04-20 14:36:08       34 阅读
  7. 嵌入式开发中模板方法模式实现

    2024-04-20 14:36:08       36 阅读
  8. 1013 数素数

    2024-04-20 14:36:08       34 阅读
  9. Linux常见命令总结

    2024-04-20 14:36:08       26 阅读
  10. python读写文件学习笔记

    2024-04-20 14:36:08       35 阅读
  11. BOOT和UBOOT区别与联系

    2024-04-20 14:36:08       34 阅读
  12. C 练习实例23

    2024-04-20 14:36:08       27 阅读
  13. Docker入门

    2024-04-20 14:36:08       34 阅读