linux驱动开发学习笔记二十:linux内核中断

一、简介

不管是裸机实验还是 Linux 下的驱动实验,中断都是频繁使用的功能,在裸机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能 IRQ 等等。Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。

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

二、相关API函数

  • 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 的话表示中断已经被申请了。

  • free_irq 函数

使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:

void free_irq(unsigned int irq, void *dev) 

函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。

  • 中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

irqreturn_t (*irq_handler_t) (int, void *)

第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,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)
  • 中断使能与禁止函数

常用的中断使用和禁止函数如下所示:

扫描二维码关注公众号,回复: 11264317 查看本文章
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 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,这个时候可以使用如下两个函数:

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(unsigned long flags)
local_irq_restore(unsigned long flags)

这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。

三、上半部与下半部

在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅响应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:

上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:

①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。

上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 内核提供了多种下半部机制,包括软中断、tasklet和工作队列。

一般软中断是不建议使用的,所以我们可以根据需求选择后面两个机制使用,下面就来简单介绍一下应该如何使用,至于具体的原理不深究。

  • tasklet

tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。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);
	 ......
}
  • 工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择tasklet。

Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):

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

在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。

简单创建工作很简单,直接定义一个 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);
	 ......
}

四、设备树中断信息节点

在设备树种关于中断的信息主要有三层:第一层是在dtsi文件中,中断控制器的节点;第二层是在dtsi文件中某一个具体的中断控制器,如某一个引脚的节点;第三层就是在dts文件中关于某个具体设备的节点。这三层之间从上到下都有一定的联系,表现为上一层的某些设置信息会对下一层起作用,下面我们具体说明。

  • 第一层:中断控制器

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

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

第 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 节点为空,表示当前节点是中断控制器。

  • 第二层:某个具体的中断控制器,如某个引脚
1 gpio5: gpio@020ac000 { 
2 		compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; 
3 		reg = <0x020ac000 0x4000>;
4 		interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
5 					 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
6 		gpio-controller; 
7 		#gpio-cells = <2>;
8 		interrupt-controller; 
9 		#interrupt-cells = <2>;
10 };

第 4 行,interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75。
第 8 行,interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。
第 9 行,将#interrupt-cells 修改为 2。

  • 第三层:某个具体的设备,需要使用到某一个中断控制器
1 fxls8471@1e { 
2		compatible = "fsl,fxls8471"; 
3 		reg = <0x1e>;
4 		position = <0>;
5 		interrupt-parent = <&gpio5>;
6 		interrupts = <0 8>;
7 };

第 5 行,interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
第 6 行,interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。

简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。

它们之间的关系简单来说就是第一层的#interrupt-cells的值决定了第二层的interrupts中的信息个数;第二层的#interrupt-cells的值决定了第三层的interrupts中的信息个数。

五、获取中断号

编写驱动的时候需要用到中断号,中断信息已经写到了设备树里面,因此可以通过

  • 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 对应的中断号。

猜你喜欢

转载自blog.csdn.net/qq_39507748/article/details/105970246