提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、中断的相关概念
- 1.中断号
- 2.中断的申请和释放
- 申请API函数如下:
- 释放API函数如下:
- 中断处理函数如下:
- 使能和禁止中断
- 二、上半部分和下本部分
- 1.tasklet
- 2.工作队列
- 三、设备树中对于中断的描述
- 三、获取中断号
- 四、实例如下
前言
使用Linux内核提供的API函数,完成中断的驱动开发。
一、中断的相关概念
1.中断号
中断号的作用是用来区分不同的中断类型,数据结构是int类型。
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因
此可以通过 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 对应的中断号。
2.中断的申请和释放
Linux中使用中断要向内核提交申请,使用完成后要释放掉中断。
申请API函数如下:
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 的话表示中断已经
被申请了
下图提供常用的中断标志
注意:request_irq函数可能会导致休眠,因此不能在中断上下文或者其他禁止休眠的代码段中使用 request_irq 函数。request_irq 函数会自动激活(使能)中断,所以不需要我们手动去使能中断。
释放API函数如下:
void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。
共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
注意:如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断
中断处理函数如下:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就
是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,
dev 也可以指向设备数据结构。
中断处理函数的返回值为 irqreturn_t 类型。其定义如下:
10 enum irqreturn {
11 IRQ_NONE = (0 << 0),
12 IRQ_HANDLED = (1 << 0),
13 IRQ_WAKE_THREAD = (1 << 1),
14 };
15
16 typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值
一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
使能和禁止中断
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。那如何处理上述情况的使用如下函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
上述三个函数是使能或者禁止某一中断,有时候需要使能和关闭整个系统的中断,使用如下函数:
local_irq_enable()
local_irq_disable()
在使用全局中断的时候有个问题,比如当程序A执行关闭10S中断,当2秒后程序B也执行了关闭全局中断,但过了3秒后,B程序又启用了全局中断,但这时候程序A关闭10S还没有结束,这种情况严重会导致系统崩溃。那该怎么解决呢,Linux中提供如下函数解决整个问题
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是成对出现的,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。
二、上半部分和下本部分
当发生中断的时候,会进入中断处理函数,在函数中,我们希望时间越小越好,但往往事与愿违,在时间驱动编写的时候,在中断处理函数中,需要花费的时间不小,比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅响应中断,然后清除中断标志位即可,这种方式就是Linux处理中断的过程:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可
以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部
去执行,这样中断处理函数就会快进快出。
比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,当然了也是有些经验可以参考的:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
那如何将上半部分和下半部分连接起来呢,Linux提供许多方法,可以百度查看一下,下面提供两种常用的方式
1.tasklet
Linux 内核使用 tasklet_struct 结构体来表示 tasklet:
484 struct tasklet_struct
485 {
486 struct tasklet_struct *next; /* 下一个 tasklet */
487 unsigned long state; /* tasklet 状态 */
488 atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
489 void (*func)(unsigned long); /* tasklet 执行的函数 */
490 unsigned long data; /* 函数 func 的参数 */
491 };
第 489 行的 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。
返回值:没有返回值
使用tasklet的格式如下:
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 tasklet */tasklet_schedule(&testtasklet);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 tasklet */tasklet_init(&testtasklet, testtasklet_func, data);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);......
}
2.工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的
工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原
型如下所示:
bool schedule_work(struct work_struct *work)
函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。
格式如下:
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 work */schedule_work(&testwork);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 work */INIT_WORK(&testwork, testwork_func_t);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);......
}
三、设备树中对于中断的描述
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。
在imx6ull中描述中断控制器的节点在imx6ull.dtsi文件中
intc: interrupt-controller@00a01000 {compatible = "arm,cortex-a7-gic";#interrupt-cells = <3>;interrupt-controller;reg = <0x00a01000 0x1000>,<0x00a02000 0x100>;};
compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到中断控制器驱动文件。
#interrupt-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 掩码。
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>;};
interrupts描述中断源信息,属性值大小按照intc中设置填写,对于 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。
interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。
将#interrupt-cells 修改为 2,为什么是2呢,查看绑定信息
此绑定信息在目录\Documentation\devicetree\bindings\gpio
上边是NXP官方写的设备树,如果用户要使用该如何使用呢,下面举个例子:
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 对应的中断号。
四、实例如下
#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/export.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>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/sched.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : imx6uirq.c
作者 : 车文超
版本 : V1.0
描述 : Linux中断驱动实验
其他 : 无
论坛 :
日志 :
***************************************************************/
#define IMX6ULL_CNT 1 /* 设备号个数 */
#define IMX6ULL_NAME "imx6ull" /* 名字 */#define KEY0VALUE 0X01 /* KEY0 按键值 */
#define INVAKEY 0XFF /* 无效的按键值 */
#define KEY_NUM 1 /* 按键数量 */
/*按键设备*/
struct key_dev{int gpioid; /*gpio编号*/unsigned char value; /*按键保存值*/char name[10]; /*按键名字*/int irqnum; /*中断号*/irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};/* imx6uirq设备结构体 */
struct imx6ull_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct device_node *nd; /* 设备节点 */struct key_dev key0dev; /*按键设备*/struct timer_list timerdev;/*定时器设备*/atomic_t keyvalue;/*按键值*/atomic_t release;/*按键释放值*/};struct imx6ull_dev imx6ulldev; /* imx6ulldev */
/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int imx6ull_open(struct inode *inode, struct file *filp)
{filp->private_data = &imx6ulldev; /* 设置私有数据 */return 0;
}/** @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符)* @param - buf : 返回给用户空间的数据缓冲区* @param - cnt : 要读取的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t imx6ull_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{struct imx6ull_dev *dev = (struct imx6ull_dev *)filp->private_data;int ret = 0;int value = 0;int release = 0;value = atomic_read(&dev->keyvalue);release = atomic_read(&dev->release);if(release ==1){if(value & 0x80){value &= ~0x80;ret = copy_to_user(buf,&value,sizeof(value));}else{goto data_error;}atomic_set(&dev->release,0);}else{return -EINVAL;}return ret;
data_error:return -EINVAL;
}/* 设备操作函数 */
static const struct file_operations imx6ull_fops = {.owner = THIS_MODULE,.open = imx6ull_open,.read = imx6ull_read,
};
/** @description : 中断处理函数* @param : 无* @return : 无*/
static irqreturn_t key0_irq(int irq, void *dev_id){struct imx6ull_dev *dev = (struct imx6ull_dev *)dev_id;
#if 0value = gpio_get_value(dev->key0dev.gpioid);printk("value=%d\n\r", value);if(value == 0){printk("key0按下");}if(value ==1){printk("key0抬起");}
#endifdev->timerdev.data = (unsigned long)dev_id;mod_timer(&dev->timerdev, jiffies + msecs_to_jiffies(10));return 0;
}
/** @description : 定时器中断函数* @param : 无* @return : 无*/
static void timer_func(unsigned long data)
{int value = 0;struct imx6ull_dev *dev = (struct imx6ull_dev *)data;value = gpio_get_value(dev->key0dev.gpioid);printk("value=%d\n\r", value);if(value == 0){printk("key0按下");atomic_set(&dev->keyvalue, dev->key0dev.value);}if(value ==1){printk("key0抬起");atomic_set(&dev->keyvalue, (dev->key0dev.value)|0x80);atomic_set(&dev->release,1);}}/** @description : 按键初始化函数* @param : 无* @return : 无*/
static int key_myinit(struct imx6ull_dev *dev_id){int ret = 0 ;struct imx6ull_dev *dev = (struct imx6ull_dev *)dev_id;/*1.获取节点*/dev->nd = of_find_node_by_path("/key");if(dev->nd == NULL){printk("of_find_node_by_path_erro\n\r");return -1;}/*2.获取GPIO编号*/dev->key0dev.gpioid = of_get_named_gpio(dev->nd, "key-gpios", 0);if(dev->key0dev.gpioid < 0){printk("of_get_named_gpio_erro\n\r");return -1;}/*3.申请IO*/memset(dev->key0dev.name, 0, sizeof(dev->key0dev.name));//sprintf(dev->keydev[i].name,"KEY%d",i);sprintf(dev->key0dev.name,"key%d",0);ret = gpio_request(dev->key0dev.gpioid,dev->key0dev.name);if(ret < 0){printk("gpio_request_erro\n\r");return -1;}gpio_direction_input(dev->key0dev.gpioid);/*4中断号获取*///dev->keydev[i].irqnum = gpio_to_irq(dev->keydev[i].gpioid);dev->key0dev.irqnum = irq_of_parse_and_map(dev->nd,0);printk("dev->key0dev.irqnum=%d\n\r",dev->key0dev.irqnum);dev->key0dev.handler = key0_irq;dev->key0dev.value = KEY0VALUE;/*5.注册中断*/ret = request_irq(dev->key0dev.irqnum, dev->key0dev.handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,dev->key0dev.name, &imx6ulldev);printk("ret=%d\n\r",ret);if(ret){printk("request_irq_erro\n\r");//goto file_irq;return -1;}/*初始化定时器*/init_timer(&imx6ulldev.timerdev);imx6ulldev.timerdev.function = timer_func;//file_irq:// free_irq(dev->key0dev.irqnum, &imx6ulldev);
return ret;
}
/** @description : 驱动入口函数* @param : 无* @return : 无*/
static int __init imx6ull_init(void)
{/* 1、构建设备号 */if (imx6ulldev.major) {imx6ulldev.devid = MKDEV(imx6ulldev.major, 0);register_chrdev_region(imx6ulldev.devid, IMX6ULL_CNT, IMX6ULL_NAME);} else {alloc_chrdev_region(&imx6ulldev.devid, 0, IMX6ULL_CNT, IMX6ULL_NAME);imx6ulldev.major = MAJOR(imx6ulldev.devid);imx6ulldev.minor = MINOR(imx6ulldev.devid);}/* 2、注册字符设备 */cdev_init(&imx6ulldev.cdev, &imx6ull_fops);cdev_add(&imx6ulldev.cdev, imx6ulldev.devid, IMX6ULL_CNT);/* 3、创建类 */imx6ulldev.class = class_create(THIS_MODULE, IMX6ULL_NAME);if (IS_ERR(imx6ulldev.class)) {return PTR_ERR(imx6ulldev.class);}/* 4、创建设备 */imx6ulldev.device = device_create(imx6ulldev.class, NULL, imx6ulldev.devid, NULL, IMX6ULL_NAME);if (IS_ERR(imx6ulldev.device)) {return PTR_ERR(imx6ulldev.device);}/*key初始化*/key_myinit(&imx6ulldev);atomic_set(&imx6ulldev.keyvalue, INVAKEY);atomic_set(&imx6ulldev.release, 0);return 0;
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit imx6ull_exit(void)
{/*释放中断号,释放IO*/free_irq(imx6ulldev.key0dev.irqnum, &imx6ulldev);gpio_free(imx6ulldev.key0dev.gpioid);/*删除定时器*/del_timer_sync(&imx6ulldev.timerdev);/*删除设备 *//*删除字符设备 */cdev_del(&imx6ulldev.cdev);/*删除设备号 */unregister_chrdev_region(imx6ulldev.devid, IMX6ULL_CNT);/*删除设备 */device_destroy(imx6ulldev.class, imx6ulldev.devid);/*删除类 */class_destroy(imx6ulldev.class);
}module_init(imx6ull_init);
module_exit(imx6ull_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chewenchao");