博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【嵌入式Linux驱动开发】十二、一文带你了解Linux开发中的中断
阅读量:2028 次
发布时间:2019-04-28

本文共 18401 字,大约阅读时间需要 61 分钟。

  一个能思想的人,才真是一个力量无边的人。


文章目录

一、Cortex-A7系列中断介绍

  Cortex-A7内核只有8个异常中断,这8个异常中断的中断向量表如下:

向量地址 终端类型 中断模式
0x00 复位中断(Rest) 特权模式(SVC)
0x04 未定义指令中断(Undefined Instruction) 未定义指令中止模式(Undef)
0x08 软中断(Software Interrupt,SWI) 特权模式(SVC)
0x0C 指令预取中止中断(Prefetch Abort) 中止模式
0x10 数据访问中止中断(Data Abort) 中止模式
0x14 未使用(Not Used) 未使用
0x18 IRQ 中断(IRQ Interrupt) 外部中断模式(IRQ)
0x1C FIQ 中断(FIQ Interrupt) 快速中断模式(FIQ)

  Cortex-A7的中断控制器叫做GIC。【类比STM32的NVIC】GIC可以开关中断,设置中断优先级!Cortex-A7用到的版本是GIC V2,该版本最多支持8个核。GIC 将众多的中断源分为分为三类:SPI、PPI、SGI。我们重点关注SPI(Shared Peripheral Interrupt,共享中断)。那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。

  Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,所以我们重点关注IRQ中断!

  为了区分不同的中断,引入了中断号的概念。ARM的SPI(共享中断)中断号范围是ID32~ID1019,一共998个中断号。像 GPIO 中断、串口中断等这些外部中断都在这里面 ,至于具体到某个 ID 对应哪个中断,那就由半导体厂商根据实际情况去定义了。

  比如I.MX6U 的总共使用了 128 个中断 ID(IRQ0~IRQ127),加上前面属于 PPI 和 SGI 的 32 个中断ID,所以I.MX6U 的中断源共有 128+32=160个,这 128 个中断 ID 对应的中断在《 I.MX6ULL 参考手册》的“ 3.2 Cortex A7 interrupts”小节可以看到!截取部分表格如下:

在这里插入图片描述

二、Linux中断处理

  Linux 系统对中断处理的演进,是使用内核线程来处理中断。关于进程和线程的概念,说起来蛮多的,这里仅简单介绍下进程和线程中断中会用到的知识。

  • ①、Linux中资源分配的单位是进程,调度的单位是线程。
  • ②、在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。
  • ③、而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。

  Linux 系统中不仅含有硬件中断,也有软件中断。我们一一道来!

2.1 硬件中断与软件中断

在这里插入图片描述

  Linux中对于硬件产生的中断,称之为“硬件中断”(hard irq)。如按键中断,串口中断,并且每个硬件中断都有对应的处理函数。

在这里插入图片描述```

我们带着问题来学习软件中断:

  • 软件中断何时生产?
    • 由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断。
  • 软件中断何时处理?
    • 软件中断嘛,并不是那么十万火急,有空再处理它好了。什么时候有空?不能让它一直等吧?Linux 系统中,各种硬件中断频繁发生,至少定时器中断每 10ms 发生一次,那取个巧?在处理完硬件中断后,再去处理软件中断?就这么办!
  • 有哪些软中断
    • 查内核源码 include/linux/interrupt.h
    • 一共10 个软中断,对应NR_SOFTIRQS 为 10
    • 软中断用软件中断数组softirq_vec表示,该数组是个全局数组,实现原型如下
      • static struct softirq_action softirq_vec[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};
  • 怎么触发软件中断?

    • 最核心的函数是 raise_softirq,简单地理解就是设置 softirq_veq[nr]的标记位
  • 软件中断使用流程?

    • 必须先使用 open_softirq 函数注册对应的软中断处理函数
      • void open_softirq(int nr, void (*action)(struct softirq_action *))
      • nr - 要开启的软中断;action - 软中断对应的处理函数
    • 注册好软中断以后需要通过 raise_softirq 函数触发
      • void raise_softirq(unsigned int nr)

2.2 Linux中断 API 函数介绍

2.2.1 request_irq 函数

  在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用request_irq 函数。 request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:

static inline int __must_checkrequest_irq(unsigned int irq, 			irq_handler_t handler, 			unsigned long flags,	    	const char *name, 	   	 	void *dev)

其中参数含义:

  • nrq:要申请中断的中断号。
  • handler:中断处理函数,当中断发生以后就会执行此中断处理函数
  • flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们
    介绍几个常用的中断标志,这些标志可以通过“ |”来实现多种组合。常用标志如下表所示:
  • name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
  • dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 是传递给中断处理函数 irq_handler_t 的第二个参数。
  • 返回值: 0 中断申请成功
标志 描述
RQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq 函数的 dev 参数就是唯一区分他们的标志。
IRQF_ONESHOT 单次中断,中断执行一次就结束。
IRQF_TRIGGER_NONE 无触发。
IRQF_TRIGGER_RISING 上升沿触发。
IRQF_TRIGGER_FALLING 下降沿触发。
IRQF_TRIGGER_HIGH 高电平触发。
IRQF_TRIGGER_LOW 低电平触发。

2.2.2 free_irq 函数

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

void free_irq(unsigned int irq,			void *dev)

其中参数含义:

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

2.2.3 中断处理函数

  使用 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)

2.2.4 中断使能与禁止使能

  • ①、开关某个中断,其中irq表示中断号
void enable_irq(unsigned int irq)void disable_irq(unsigned int irq)//要等到当前正在执行的中断处理函数执行完才返回void disable_irq_nosync(unsigned int irq) //调用以后立即返回,不会等待当前中断处理程序执行完毕
  • ②、开关全局中断
local_irq_enable()local_irq_disable()

2.3 中断处理原则

2.3.1 中断处理不能嵌套

  中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。

2.3.1 中断处理越快越好

  在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。在 SMP 系统中,假设中断处理很慢,那么正在处理这个中断的 CPU 上的其他线程也无法执行。在中断的处理过程中,该 CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现

  但是,处理某个中断要做的事情就是很多,没办法加快。 比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在 handler 中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?

  这时,“下半部”的处理思想便由此产生。

2.4 要处理的事情实在太多,拆分为:上半部、下半部

  当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。 换句话说, 在这段时间内,系统是关中断的。如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。

在这里插入图片描述

  哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,以借鉴的参考点:

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

  中断下半部的实现有很多种方法,先来了解两种主要的: tasklet(小任务)、 work queue(工作队列)。

2.4.1 下半部要做的事情耗时不是太长: tasklet

  假设我们把中断分为上半部、下半部。 发生中断时,上半部下半部的代码何时、如何被

调用?

  当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用 tasklet 来处理下半部。 tasklet 是使用软件中断来实现

在这里插入图片描述

贴代码,一目了然:

在这里插入图片描述

使用流程图简化一下:

在这里插入图片描述

注:流程图中的问号,是在判断preempt_count是不是等于0?

  假设硬件中断 A 的上半部函数为 irq_top_half_A,下半部为 irq_bottom_half_A。使用情景化的分析,才能理解上述代码的精华。

  • a. 硬件中断 A 处理过程中,没有其他中断发生

    一开始, preempt_count = 0;
    上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。

  • b. 硬件中断 A 处理过程中,又再次发生了中断 A

    一开始, preempt_count = 0;
    执行到第⑥时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。
    注意:这时 preempt_count 等于 1,并且中断下半部的代码并未执行。
    CPU 又从①开始再次执行中断 A 的上半部代码:
    在第①步 preempt_count 等于 2;
    在第③步 preempt_count 等于 1;
    在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;
    注意:重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理。当第 2 次中断 A处理完毕, CPU 会继续去执行第⑦步。

可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只

执行了一次。所以,同一个中断的上半部、下半部,在执行时是多对一的关系。

  • c. 硬件中断 A 处理过程中,又再次发生了中断 B
    一开始, preempt_count = 0;
    执行到第⑥时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。
    注意:这时 preempt_count 等于 1,并且中断 A 下半部的代码并未执行。
    CPU 又从①开始再次执行中断 B 的上半部代码:
    在第①步 preempt_count 等于 2;
    在第③步 preempt_count 等于 1;
    在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;
    注意:重点来了,第 2 次中断发生后,打断了第一次中断 A 的第⑦步处理。当第 2 次中断 B处理完毕, CPU 会继续去执行第⑦步。

可以看到,在第⑦步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。

所以,多个中断的下半部,是汇集在一起处理的。

总结:

  • a. 中断的处理可以分为上半部,下半部
  • b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
  • c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
  • d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
  • e. 中断上半部执行完后,触发中断下半部的处理
  • f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

2.4.2 tasklet的使用

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 的参数 */};

tasklet使用流程

  • ①、先定义一个tasklet,再使用 tasklet_init 初始化tasklet。
    • 当然定义和初始化还可以用DECLARE_TASKLET合二为一。
  • ②、在中断上半部调用 tasklet_schedule 函数,这样 tasklet 就可以在合适的时间运行。

上述提到的三个函数原型如下:

  • void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
    • t表示tasklet变量,func表示tasklet的处理函数,data表示要传递给func函数的参数
  • DECLARE_TASKLET(name, func, data)
    • 参数含义同上,不过是把t改成了name
  • void tasklet_schedule(struct tasklet_struct *t)

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.4.3 下半部要做的事情太多并且很复杂:工作队列(work queue)

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

  在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。假设下半部要执行 1、 2 分钟,在这 1、 2 分钟里 APP 都是无法响应的。

  这谁受得了?

  所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。

在这里插入图片描述

  这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:

在这里插入图片描述

  kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。

那我们怎么使用 work、 work queue 呢?

  • a. 创建 work:

    • 你得先写出一个函数,然后用这个函数填充一个 work 结构体。比如:
      在这里插入图片描述
  • b. 要执行这个函数时,把 work 提交给 work queue 就可以了:

    • 下述函数会把 work 提供给系统默认的 work queue: system_wq,它是一个队列。
      在这里插入图片描述
  • c. 谁来执行 work 中的函数?

    • 不用我们管, schedule_work 函数不仅仅会把 work 放入队列,还会把 kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
  • d. 谁把 work 提交给 work queue

    • 在中断场景中,可以在中断上半部调用 schedule_work 函数。

总结:

  • a. 很耗时的中断处理,应该放到线程里去
  • b. 可以使用 work、 work queue
  • c. 在中断上半部调用 schedule_work 函数,触发 work 的处理
  • d. 既然是在线程中运行,那对应的函数可以休眠。

2.4.4 工作队列的使用

  • ①、work_struct 结构体表示一个工作,定义如下
struct work_struct {
atomic_long_t data; struct list_head entry; work_func_t func; /* 工作队列处理函数 */};
  • ②、这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下
struct workqueue_struct {
...};
  • ③、Linux 内核使用工作者线程(worker thred)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程, worker 结构体内容如下
struct worker {
...};

  上面三个定义可以得出,每一个工作者线程(worker)都有一个工作队列(workqueue_struct)。工作者线程处理自己工作队列中的所有工作!在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。

工作队列使用流程

  • ①、先定义一个work_struct,再使用 INIT_WORK 初始化work_struct。
    • 当然定义和初始化还可以用 DECLARE_WORK 合二为一。
  • ②、在中断上半部调用 schedule_work 函数,这样 work_struct 就可以在合适的时间运行。

上述提到的三个函数原型如下:

#define INIT_WORK(_work, _func) //work 表示要初始化的工作, _func 是工作对应的处理函数。#define DECLARE_WORK(n, f) //n 表示定义的工作(work_struct), f 表示工作对应的处理函数。bool schedule_work(struct work_struct *work) //work: 要调度的工作

work_struct 使用示例

/* 定义工作(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); ......}

2.5 新技术: threaded irq

  使用线程来处理中断,并不是什么新鲜事。 使用 work 就可以实现,但是需要定义 work、调用 schedule_work,好麻烦啊。

  太懒了太懒了,就这 2 步你们都不愿意做。好,内核是为懒人服务的,再杀出一个函数:

在这里插入图片描述

  你可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。

  说懒是开玩笑,内核开发者也不会那么在乎懒人。

  以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个 CPU 上?

  新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行, 这样便提高了执行效率。

三、Linux中断系统中的重要数据

在这里插入图片描述

  能弄清楚上面这个图,对 Linux 中断系统的掌握也基本到位了。最核心的结构体是 irq_desc,之前为了易于理解,我们前面说在 Linux 内核中有一个中断数组,对于每一个硬件中断,都有一个数组项, 这个数组就是 irq_desc 数组。

注意: 如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc 数组。 SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛? 所以在中断比较“稀疏”的情况下可以用基数树来代替数组。

3.1 irq_desc 数组

  irq_desc 结构体在 include/linux/irqdesc.h 中定义,主要内容如下图:

在这里插入图片描述

  每一个 irq_desc 数组项中都有一个函数: handle_irq, 还有一个 action 链表。 要理解它们,需要先看中断结构图:

在这里插入图片描述

  关注上图中的A号中断和B号中断位置!

  外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC(通用中断控制器)的 A 号中断, GIC 再去中断 CPU。那么软件处理时就是反过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断。

  所以, 中断的处理函数来源有三:

  • ① GIC 的处理函数:

  假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家), 这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是 B),再去调用 irq_desc[B].handle_irq。

  注意: irq_desc[A].handle_irq 细分出中断后 B,调用对应的irq_desc[B].handle_irq。显然中断 A 是 CPU 感受到的顶层的中断, GIC 中断 CPU 时, CPU 读取 GIC 状态得到中断 A。

  • ② 模块的中断处理函数:

  比如对于 GPIO 模块向 GIC 发出的中断 B, 它的处理函数是irq_desc[B].handle_irq。BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。

  注意:导致 GPIO 中断 B 发生的原因很多,可能是外部设备 1,可能是外部设备 n,可能只是某一个设备,也可能是多个设备。所以 irq_desc[B].handle_irq 会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生, 若是则处理。

  • ③ 外部设备提供的处理函数:

  这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”: 它知道如何判断设备是否发生了中断,如何处理中断。

  对于共享中断,比如 GPIO 中断 B, 它的中断来源可能有多个, 每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,然后一一执行。这个链表就是 action 链表。链表可以在本部分开头的第一张图片看到。

3.2 irqaction 结构体

  irqaction 结构体在 include/linux/interrupt.h 中定义,主要内容如下图:

在这里插入图片描述

  当调用 request_irq、 request_threaded_irq 注册中断处理函数时,内核就会构造一个irqaction 结构体。在里面保存 name、 dev_id 等,最重要的是 handler、 thread_fn、 thread。

  • handler 是中断处理的上半部函数, 用来处理紧急的事情。
  • thread_fn 对应一个内核线程 thread,当 handler 执行完毕, Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
  • 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。(只有上半部)
  • 可以不提供 handler 只提供 thread_fn, 完全由内核线程来处理中断。(只有下半部)
  • 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

  里面还有一个名为 sedondary 的 irqaction 结构体,它的作用以后再分析。在 reqeust_irq 时可以传入 dev_id, 为何需要 dev_id? 作用有 2:

  • ① 中断处理函数执行时,可以使用 dev_id
  • ② 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项。所以在共享中断中必须提供 dev_id, 非共享中断可以不提供。

3.3 irq_data 结构体

  irq_data 结构体在 include/linux/irq.h 中定义,主要内容如下图:

在这里插入图片描述

  它就是个中转站,里面有 irq_chip 指针、irq_domain 指针,都是指向别的结构体。

  比较有意思的是 irq和hwirq,其中irq 是软件中断号, hwirq 是硬件中断号。 比如上面我们举的例子,在 GPIO 中断 B 是软件中断号,可以找到 irq_desc[B]这个数组项; GPIO 里的第 x 号中断, 这就是 hwirq。

  谁来建立 irq、 hwirq 之间的联系呢?由 irq_domain 来建立。 irq_domain 会把本地的hwirq 映射为全局的 irq,什么意思?比如 GPIO 控制器里有第 1 号中断, UART 模块里也有第 1 号中断,这两个“第 1 号中断”是不一样的,它们属于不同的“域”──irq_domain。

3.4 irq_domain 结构体

irq_domain 结构体在 include/linux/irqdomain.h 中定义,主要内容如下图:

在这里插入图片描述

  当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被转换为 irq时, irq_domain 将会起到极大的作为。这里基于入门的解度简单讲讲,在设备树中你会看到这样的属性:

interrupt-parent = <&gpio1>;interrupts = <5 IRQ_TYPE_EDGE_RISING>;

  它表示要使用 gpio1 里的第 5 号中断, hwirq 就是 5。但是我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断, irq 是什么?它是软件中断号 ,它应该从“gpio1 的第 5 号中断”转换得来。

  谁把 hwirq 转换为 irq?由 gpio1 的相关数据结构,就是 gpio1 对应的 irq_domain 结构体。

  irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数,主要是:

  • ① xlate

    用来解析设备树的中断属性, 提取出 hwirq、 type 等信息。

  • ② map

    把 hwirq 转换为 irq。

3.5 irq_chip 结构体

  irq_chip 结构体在 include/linux/irq.h 中定义,主要内容如下图:

在这里插入图片描述

  这个结构体跟“chip”即芯片相关,里面各成员的作用在头文件中也列得很清楚, 摘录部分如下:

* @irq_startup: start up the interrupt (defaults to ->enable if NULL)* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)* @irq_disable: disable the interrupt* @irq_ack: start of a new interrupt* @irq_mask: mask an interrupt source* @irq_mask_ack: ack and mask an interrupt source* @irq_unmask: unmask an interrupt source* @irq_eoi: end of interrupt

  我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。

  我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。

  但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”, 外设备千变万化,内核里可没有对应的清除中断操作。

四、在设备树中指定中断

4.1 设备树里中断节点的语法

可参考文档:

内核 Documentation\devicetree\bindings\interrupt-controller\interrupts.txt

4.1.1 设备树里的中断控制器

在这里插入图片描述

   在硬件上,“中断控制器”只有 GIC 这一个,但是我们在软件上也可以把上图中的“GPIO”称为“中断控制器”。 因为芯片有多个 GPIO 模块,比如 GPIO1、 GPIO2 等等。所以软件上的“中断控制器”就有很多个: GIC、 GPIO1、 GPIO2 等等。
  GPIO1 连接到 GIC, GPIO2 连接到 GIC,所以 GPIO1 的父亲是 GIC, GPIO2 的父亲是GIC。
  假设 GPIO1 有 32 个中断源,但是它把其中的 16 个汇聚起来向 GIC 发出一个中断,把另外 16 个汇聚起来向 GIC 发出另一个中断。这就意味着 GPIO1 会用到 GIC 的两个中断,会涉及 GIC 里的 2 个 hwirq

  这些层级关系、中断号(hwirq),都会在设备树中有所体现。

  在设备树中,中断控制器节点中必须有一个属性: interrupt-controller,表明它是“中断控制器”。还必须有一个属性: #interrupt-cells,表明引用这个中断控制器的话需要多少个 cell。#interrupt-cells 的值一般有如下取值:

  • ① #interrupt-cells=<1>
    别的节点要使用这个中断控制器时,只需要一个 cell 来表明使用“哪一个中断”。
  • ② #interrupt-cells=<2>
    别的节点要使用这个中断控制器时,需要一个 cell 来表明使用“哪一个中断”;还需要另一个 cell 来描述中断,一般是表明触发类型:
第 2 个 cell 的 bits[3:0] 用来表示中断触发类型(trigger type and level flags):1 = low-to-high edge triggered,上升沿触发2 = high-to-low edge triggered,下降沿触发4 = active high level-sensitive,高电平触发8 = active low level-sensitive,低电平触发

示例如下:

vic: intc@10140000 {
compatible = "arm,versatile-vic"; interrupt-controller; #interrupt-cells = <1>; reg = <0x10140000 0x1000>;};

  如果中断控制器有级联关系,下级的中断控制器还需要表明它的“interrupt-parent”是谁,用了 interrupt-parent”中的哪一个“interrupts”。

4.1.2 设备树里使用中断

  一个外设,它的中断信号接到哪个“中断控制器”的哪个“中断引脚”,这个中断的触发方式是怎样的?这 3 个问题,在设备树里使用中断时,都要有所体现。

  • ① interrupt-parent=<&XXXX>
    你要用哪一个中断控制器里的中断?
  • ② interrupts
    你要用哪一个中断?中断触发方式?

  Interrupts 里要用几个 cell,由 interrupt-parent 对应的中断控制器决定。在中断控制器里有“#interrupt-cells”属性,它指明了要用几个 cell 来描述中断。

比如:

2c@7000c000 {
gpioext: gpio-adnp@41 {
compatible = "ad,gpio-adnp"; interrupt-parent = <&gpio>; interrupts = <160 1>; gpio-controller; #gpio-cells = <1>; interrupt-controller; #interrupt-cells = <2>; };......};

③ 新写法: interrupts-extended

  一个“interrupts-extended”属性就可以既指定“interrupt-parent”,也指定“interrupts”,比如:

interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;

4.2 设备树里中断节点的示例

  在 arch/arm/boot/dts 目录下可以看到 2 个文件:imx6ull.dtsi、 100ask_imx6ull-14x14.dts,把里面有关中断的部分内容抽取出来。

在这里插入图片描述

  从设备树反推 IMX6ULL 的中断体系,如下,比之前的框图多了一个“GPC INTC”:

A

  GPC INTC 的英文是: General Power Controller, Interrupt Controller。它提供中断屏蔽、中断状态查询功能,实际上这些功能在 GIC 里也实现了,个人觉得有点多余。除此之外,它还提供唤醒功能,这才是保留它的原因。

简单总结一下与中断有关的设备树属性信息:

  • ①、 #interrupt-cells,指定中断源的信息 cells 个数。
  • ②、 interrupt-controller,表示当前节点为中断控制器。
  • ③、 interrupts,指定中断号,触发方式等。
  • ④、 interrupt-parent,指定父中断,也就是中断控制器。

4.3 在代码中获得中断

  之前我们提到过,设备树中的节点有些能被转换为内核里的 platform_device,有些不能,回顾如下:

  • 情形A - 父节点是根节点:该节点含有compatile 属性即可转化为 platform_device
  • 情形B - 父节点非根节点:需要满足两个条件 ①、该节点还有compatile 属性 ②父节点 compatile 属性为下面4个之一:“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”,才能转化为platform_device。
  • 情形C - 还需要记住一个:总线 I2C、SPI 节点下的子节点:不转换为 platform_device
    • 某个总线下的子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。

4.3.1 对于 platform_device

  一个节点能被转换为 platform_device,如果它的设备树里指定了中断属性,那么可以从platform_device 中获得“中断资源”, 函数如下,可以使用下列函数获得 IORESOURCE_IRQ 资源,即中断号:

/* * platform_get_resource - get a resource for a device * @dev: platform device * @type: resource type // 取哪类资源? IORESOURCE_MEM、 IORESOURCE_REG、IORESOURCE_IRQ 等 * @num: resource index // 这类资源中的哪一个? */struct resource *platform_get_resource(struct platform_device *dev,									unsigned int type, unsigned int num);

4.3.2 对于 I2C 设备、 SPI 设备

  对于 I2C 设备节点, I2C 总线驱动在处理设备树里的 I2C 子节点时,也会处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq成员里,代码如下(drivers/i2c/i2c-core.c):

在这里插入图片描述

  对于 SPI 设备节点, SPI 总线驱动在处理设备树里的 SPI 子节点时,也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq成员里,代码如下(drivers/spi/spi.c):

在这里插入图片描述

4.3.3 调用 of_irq_get 获得中断号

  如果你的设备节点既不能转换为 platform_device,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号。

4.3.4 调用 irq_of_parse_and_map

函数从 interupts 属性中提取到对应的设备号

unsigned int irq_of_parse_and_map(struct device_node *dev,								int index)
  • dev: 设备节点。
  • index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
  • 返回值:中断号。

4.3.5 对于 GPIO

  参考: drivers/input/keyboard/gpio_keys.c,可以使用 gpio_to_irq 或 gpiod_to_irq 获得中断号。

  举例,假设在设备树中有如下节点:

gpio-keys {
compatible = "gpio-keys"; pinctrl-names = "default"; user {
label = "User Button"; gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>; gpio-key,wakeup; linux,code =
; };};

  那么可以使用下面的函数获得引脚和 flag:

button->gpio = of_get_gpio_flags(pp, 0, &flags);bdata->gpiod = gpio_to_desc(button->gpio);

再去使用 gpiod_to_irq 获得中断号:

irq = gpiod_to_irq(bdata->gpiod);

转载地址:http://cbnaf.baihongyu.com/

你可能感兴趣的文章
算法----五大算法之动态规划
查看>>
算法----五大算法之分治法
查看>>
数据结构----各种排序算法的比较
查看>>
数据结构---表达式求值
查看>>
数据结构----二叉树的遍历
查看>>
数据结构----栈和队列的综合应用
查看>>
数据结构----线性表的应用
查看>>
设计模式----代理模式(Proxy)
查看>>
设计模式-----观察者模式(Obsever)
查看>>
设计模式深入浅出-----策略模式(Strategy Pattern)
查看>>
UML的组成与UML建模一般流程
查看>>
项目管理----项目进度管理
查看>>
项目管理----项目范围管理
查看>>
SQL性能优化
查看>>
SQL表连接查询(inner join、full join、left join、right join)
查看>>
SQL语言学习心得
查看>>
有趣的编程----控制自己电脑的CPU
查看>>
Windows程序运行原理
查看>>
图解JAVA中Spring Aop作用
查看>>
组件化、模块化、集中式、分布式、服务化、面向服务的架构、微服务架构
查看>>