【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写

文章目录

  • 前言
  • 1、异常与中断的概念及处理流程
    • 1.1、异常与中断的概念
    • 1.2、中断的处理流程
    • 1.3、异常向量表
  • 2、Linux系统对中断的处理
    • 2.1、进程、线程、中断的核心:栈
      • 2.1.1、ARM处理器是如何工作的?
      • 2.1.2、中断时如何保存现场?
      • 2.1.3、进程和线程
    • 2.2、Linux系统对中断的演进
      • 2.2.1、硬件中断和软件中断
      • 2.2.2、中断处理原则:不能嵌套
      • 2.2.3、中断处理原则:越快越好
      • 2.2.4、中断的上半部分和下半部分
      • 2.2.5、下半部要做的事情耗时不是太长:tasklet
      • 2.2.6、下半部要做的事情太多并且很复杂:工作队列
      • 2.2.7、新技术:threaded irq
    • 2.3、Linux中断系统中重要的数据结构
      • 2.3.1、struct irq_desc数组
      • 2.3.2、irqaction结构体
      • 2.3.3、 irq_data结构体
      • 2.3.4、irq_domain结构体
      • 2.3.4、irq_chip结构体
    • 2.4、设备树中指定中断 代码中获得中断
      • 2.4.1、设备树里中断节点的语法
        • 2.4.1.1、设备中的中断控制器
        • 2.4.1.2、设备树中使用中断
      • 2.4.2、设备树里中断节点的示例
      • 2.4.3、在驱动中获得中断
        • 2.4.3.1、 对于platform_device
        • 2.4.3.2、 对于I2C设备、SPI设备
        • 2.4.3.3、 上述两种情况都不符合
        • 2.4.3.3、 对于GPIO
    • 2.5、编写按键中断驱动程序、修改设备树
      • 2.5.1、设备树
      • 2.5.2、按键中断驱动程序

前言

韦东山嵌入式Linux驱动开发基础知识学习笔记
文章中大多内容来自韦东山老师的文档,还有部分个人根据自己需求补充的内容
视频教程地址:https://www.bilibili.com/video/BV14f4y1Q7ti

1、异常与中断的概念及处理流程

1.1、异常与中断的概念

异常:当异常发生时,CPU会根据异常向量表跳转,比如reset发生时,CPU会跳转到软件指定好的代码段去执行reset程序
中断:中断是异常的子集,是一种特殊的异常,其包含一个中断控制器来判断是哪种中断,并且对其进行排序再通知CPU

在嵌入式系统的异常、中断处理模型是这样的:
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第1张图片

▲嵌入式系统的异常、中断处理模型

CPU在运行的过程中,也会被各种“异常”打断。这些“异常”有:
  ① 指令未定义
  ② 指令、数据访问有问题
  ③ SWI(软中断)
  ④ 快中断
  ⑤ 中断

中断也属于一种“异常”,导致中断发生的情况有很多,比如:
  ① 按键
  ② 定时器
  ③ ADC转换完成
  ④ UART发送完数据、收到数据
  ⑤ 等等
这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知CPU。

1.2、中断的处理流程

arm对异常(中断)处理过程:
  ① 初始化:
    a. 设置中断源,让它可以产生中断
    b. 设置中断控制器(可以屏蔽某个中断,优先级)
    c. 设置CPU总开关(使能中断)
  ② 执行其他程序:正常程序
  ③ 产生中断:比如按下按键—>中断控制器—>CPU
  ④ CPU 每执行完一条指令都会检查有无中断/异常产生
  ⑤ CPU发现有中断/异常产生,开始处理。
    对于不同的异常,跳去不同的地址执行程序。
    这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。
③④⑤都是硬件做的。
  ⑥ 这些函数做什么事情?
    软件做的:
      a. 保存现场(各种寄存器)
      b. 处理异常(中断):
      分辨中断源,再调用不同的处理函数
      c. 恢复现场

1.3、异常向量表

u-boot或是Linux内核,都有类似如下的代码:

_start: b	reset
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18**
	ldr	pc, _fiq

这就是异常向量表,每一条指令对应一种异常。
发生复位时,CPU就去 执行第1条指令:b reset。
发生中断时,CPU就去执行“ldr pc, _irq”这条指令。
这些指令存放的位置是固定的,比如对于ARM9芯片中断向量的地址是0x18。
当发生中断时,CPU就强制跳去执行0x18处的代码。

在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU就会执行向量表中的跳转指令,去调用更复杂的函数。
当然,向量表的位置并不总是从0地址开始,很多芯片可以设置某个vector base寄存器,指定向量表在其他位置,比如设置vector base为0x80000000,指定为DDR的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是0,中断是0x18。

2、Linux系统对中断的处理

2.1、进程、线程、中断的核心:栈

要想知道中断是如何在ARM架构的CPU上实现的就必须理解ARM架构CPU是如何完成工作的

2.1.1、ARM处理器是如何工作的?

ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
  ① 对内存只有读、写指令
  ② 对于数据的运算是在CPU内部实现
  ③ 使用RISC指令的CPU复杂度小一点,易于设计
比如计算a=a+b这样的算式,需要经过下面4个步骤才可以实现:
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第2张图片

▲a=a+b

这一操作会涉及到一系列寄存器
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第3张图片

▲a=a+b涉及到的寄存器

CPU运行时,先去取得指令,再执行指令:
  ① 把内存a的值读入CPU寄存器R0
  ② 把内存b的值读入CPU寄存器R1
  ③ 把R0、R1累加,存入R0
  ④ 把R0的值写入内存a

2.1.2、中断时如何保存现场?

什么是现场:程序执行时寄存器中的值
如何保存现场:将寄存器中的值存入栈内存中
如何恢复现场:将栈内存中的值存入寄存器中
保存现场的场景并不局限于中断,下图可以概括程序A、B的切换过程,其他情况是类似的:
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第4张图片

▲程序A、B的切换过程

a. 函数调用:
  在函数A里调用函数B,实际就是中断函数A的执行。
  那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;
  再去执行函数B;
  函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。
b. 中断处理
  进程A正在执行,这时候发生了中断。
  CPU强制跳到中断异常向量地址去执行,
  这时就需要保存进程A被中断瞬间的CPU寄存器值,
  可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。
  中断处理完毕,要继续运行进程A之前,恢复这些值。

c. 进程切换
  在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
  如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。
怎么切换?
  切换过程是发生在内核态里的,跟中断的处理类似。
  进程A的被切换瞬间的CPU寄存器值保存在某个地方;
  恢复进程B之前保存的CPU寄存器值,这样就可以运行进程B了。

  所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。
进程的调度也是使用栈来保存、恢复现场:

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第5张图片

▲进程调度保护、恢复现场操作

2.1.3、进程和线程

在Linux中:资源分配的单位是进程,调度的单位是线程,一个进程中可以有多个线程,进程间通信效率较低但是拥有独立的资源,线程间通信效率高因为一个进程中的所有线程共享打开的文件句柄、全局变量等等
线程之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。如下图示:
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第6张图片

▲线程的栈

2.2、Linux系统对中断的演进

Linux系统中有硬件中断,也有软件中断。
对硬件中断的处理有2个原则:不能嵌套,越快越好。

2.2.1、硬件中断和软件中断

Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
为方便理解,可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
在这里插入图片描述

▲硬件中断数组--简化图

当发生A中断时,对应的irq_function_A函数被调用。硬件导致该函数被调用。
相对的Linux提供软件中断:
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第7张图片

▲软件中断(soft irq)数组--简化图

软件中断何时产生?
由软件决定,对于X号软件中断,只需要把它的flag设置为1就表示发生了该中断。
软件中断何时处理?
在Linux系统中处理完硬件中断后就会去处理已经发生的软件中断,其中“硬件中断”包含每10ms发生一次的定时器中断
有哪些软件中断?

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */

enum
{
	HI_SOFTIRQ=0,
	TIMER_SOFTIRQ,
	NET_TX_SOFTIRQ,
	NET_RX_SOFTIRQ,
	BLOCK_SOFTIRQ,
	IRQ_POLL_SOFTIRQ,
	TASKLET_SOFTIRQ,
	SCHED_SOFTIRQ,
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
			    numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS
};

如何使用软中断?
软中断的触发

void raise_softirq(unsigned int nr);

软中断服务函数的设置

void open_softirq(int nr, void (*action)(struct softirq_action *));

后面讲到的中断下半部tasklet就是使用软件中断实现的。

2.2.2、中断处理原则:不能嵌套

中断发生时需要保存现场,这就需要栈内存,如果中断可以嵌套的会发生什么?
对,会栈溢出,无论多大的栈也会有用尽的一刻

2.2.3、中断处理原则:越快越好

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

如何使用中断呢?

//为中断irq注册中断处理函数handler
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)

但是并不是所有的中断服务都可以快速处理的,那么怎么办?
这时候就可以将中断分为上下两部分,上半部使用硬件中断服务函数,下半部分使用tasklet软件中断或者其他办法

2.2.4、中断的上半部分和下半部分

中断的上下两部分对应于紧急和不紧急的事情,当然这个紧急需要开发者去衡量
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第8张图片

▲中断的上半部分和下半部分

中断下半部的实现有很多种方法,讲2种主要的:tasklet(小任务)、work queue(工作队列)

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

如果下半部要做的事情耗时不是太长,那么可以使用tasklet来处理
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第9张图片

▲软件中断中的tasklet

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第10张图片

▲中断的上下半部分逻辑流程图

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第11张图片

▲中断上半部和下半部处理代码示意图--软硬件中断

硬件中断A处理过程中,又再次发生了中断A
从流程图分析可以发现这么做可以起到中断上半部分一对多的效果,即在硬件中断发生多次时,上半部分会多次执行但只会执行一次中断下半部分
硬件中断A处理过程中,又再次发生了中断B
这时会先处理中断A的上半部然后处理中断B的上半部,最后处理中断A和中断B的下半部,多个中断的下半部,是汇集在一起处理的

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

2.2.6、下半部要做的事情太多并且很复杂:工作队列

  在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间APP是无法执行的,假设下半部要执行1、2分钟,在这1、2分钟里APP都是无法响应的,这谁受得了?
  所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第12张图片

▲kworker线程

kwoker线程时内核系统创建的,开发者通过向工作队列(work queue)中塞入工作(work)来使用这个内核线程

那我们怎么使用work、work queue呢?
a. 创建work:

/* *
* mem_error_work - work结构体
* mem_error_handler - 函数
*  */
static DECLARE_WORK(mem_error_work, mem_error_handler);

b.将work塞入工作队列

schedule_work(&mem_error_work);

/**
 * schedule_work - put work task in global workqueue
 * @work: job to be done
 *
 * Returns %false if @work was already on the kernel-global workqueue and
 * %true otherwise.
 *
 * This puts a job in the kernel-global workqueue if it was not already
 * queued and leaves it in the same position on the kernel-global
 * workqueue otherwise.
 */
static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}**

c. 谁来执行work中的函数?
不用我们管,schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。
d. 谁把work提交给work queue?
在中断场景中,可以在中断上半部调用schedule_work函数。

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

2.2.7、新技术:threaded irq

/**
* irq - 中断号
* handler - 上半部函数,可以为空
* thread_fn - 在线程中运行的函数
* flags - 中断类型
* name - 名称
* dev- 数据
*/
request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev);

可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
以前用work来线程化地处理中断,一个worker线程只能由一个CPU执行,多个中断的work都由同一个worker线程来处理,在单CPU系统中也只能忍着了。但是在SMP系统中,明明有那么多CPU空着,你偏偏让多个中断挤在这个CPU上?
新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。

2.3、Linux中断系统中重要的数据结构

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第13张图片

▲中断功能涉及到的数据结构
  1. struct irq_desc:该结构体映射一个中断,在上图中它可能是GIC 接收的A号中断也可能是GPIO 接收的B号中断
  2. irqaction结构体:该结构体是irq_desc的成员之一包含了一个中断的设备信息和函数指针(name、dev_id等,handler、thread_fn、thread),在中断来源于GPIOB后会调用的其中函数指针
  3. irq_data结构体:该结构体是一个中转站,里面包含irq_domain和irq_chip的结构体指针,还有软件中断号irq,硬件中断号hwirq等4. Linux中断系统相关数据
  4. irq_domain结构体:该结构体内部包含了一些函数,有些可以解析设备树中断并将数据传入内核,有些可以完成软硬件中断转换操作
  5. irq_chip结构体:该结构体内被包含了一些函数用于管理中断的使能情况

2.3.1、struct irq_desc数组

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第14张图片

▲中断功能实现模型

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第15张图片

▲struct irq_desc

  外部设备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链表。

2.3.2、irqaction结构体

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第16张图片

▲irqaction结构体

  当调用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,非共享中断可以不提供。

2.3.3、 irq_data结构体

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第17张图片

▲ irq_data结构体

  它就是个中转站,里面有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。

2.3.4、irq_domain结构体

在这里插入图片描述

▲irq_domain结构体

  当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被转换为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。

2.3.4、irq_chip结构体

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第18张图片

▲irq_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”,外设备千变万化,内核里可没有对应的清除中断操作。

2.4、设备树中指定中断 代码中获得中断

2.4.1、设备树里中断节点的语法

2.4.1.1、设备中的中断控制器

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第19张图片

▲中断硬件框架图

  上面介绍数据结构时也提到GIC模块 和 GPIO模块都有对应的 irq_desc,其实硬件上的中断控制器只有GIC一个,但是因为芯片拥有多个GPIO模块,所以软件上的“中断控制器”就有很多个GIC、GPIO1、GPIO2 等
  这也就意味这,如果要用到一个连接到GPIOA的设备中断就会有需要使用到两个(GPIOA、GIC)中断控制器,具体到开发者所需要做到的工作:开发者需要指定中断控制器的级联关系
  假设GPIO1有32个中断源,但是它把其中的16个汇聚起来向GIC发出一个中断,把另外16个汇聚起来向GIC发出另一个中断。这就意味着GPIO1会用到GIC的两个中断,会涉及GIC里的2个hwirq。

vic: intc@10140000 {
	compatible = "arm,versatile-vic";
	interrupt-controller;		//表明这个节点对应中断控制器
	#interrupt-cells = <1>;	//表明引用这个中断控制器需要多少个cell,即用多少个cell表明一个中断
	reg = <0x10140000 0x1000>;
};

一般如果 #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,低电平触发

如果控制器有级联关系,下级还需要表明它的interrupt-parent以及用了interrupt-parent的哪一个interrupt

2.4.1.2、设备树中使用中断

i2c@7000c000 {
	gpioext: gpio-adnp@41 {
		compatible = "ad,gpio-adnp";

		interrupt-parent = <&gpio>;	//表明使用gpio控制器中的中断
		interrupts = <160 1>;					//表明使用160号中断,上升沿触发

		gpio-controller;
		#gpio-cells = <1>;

		interrupt-controller;
		#interrupt-cells = <2>;
	};
......
};

新写法:既指定interrupt-parent,也指定interrupts

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

2.4.2、设备树里中断节点的示例

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第20张图片

▲imx6ull.dtsi、100ask_imx6ull-14x14.dts

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第21张图片

▲IMX6ULL的中断体系

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

2.4.3、在驱动中获得中断

之前我们提到过,设备树中的节点有些能被转换为内核里的platform_device,有些不能,回顾如下:
  A. 根节点下含有compatile属性的子节点,会转换为platform_device
  B. 含有特定compatile属性的节点的子节点,会转换为platform_device
  如果一个节点的compatile属性,它的值是这4者之一:“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”,
  那么它的子结点(需含compatile属性)也可以转换为platform_device。
  C. 总线I2C、SPI节点下的子节点:不转换为platform_device
某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。

2.4.3.1、 对于platform_device

既然可以被转换为platform_device,那么在驱动中就可以通过函数来获取中断号等资源

/**
 * 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);

2.4.3.2、 对于I2C设备、SPI设备

对于I2C设备节点,I2C总线驱动在处理设备树里的I2C子节点时,也会处理其中的中断信息。一个I2C设备会被转换为一个i2c_client结构体,中断号会保存在i2c_client的irq成员里
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第22张图片

▲对于I2C设备

对于SPI设备节点,SPI总线驱动在处理设备树里的SPI子节点时,也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体,中断号会保存在spi_device的irq成员里
【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第23张图片

▲对于SPI设备

2.4.3.3、 上述两种情况都不符合

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

2.4.3.3、 对于GPIO

假设设备树中有这些节点:

gpio-keys {
		compatible = "gpio-keys";
		pinctrl-names = "default";

		user {
				label = "User Button";
				gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
				gpio-key,wakeup;
				linux,code = <KEY_1>;
		};
};

可以使用下面的函数获得引脚和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);

2.5、编写按键中断驱动程序、修改设备树

【嵌入式Linux】嵌入式Linux驱动开发基础知识之Linux中断系统简介及按键中断设备树驱动编写_第24张图片

▲从原理图中可以看出PG2 PG3 都是下降沿触发

2.5.1、设备树

gpio_keys_100ask {
	compatible = "100ask,gpio_key";
	gpios = <&gpiog 3 IRQ_TYPE_EDGE_RISING
			 				&gpiog 2 IRQ_TYPE_EDGE_RISING>;
};

2.5.2、按键中断驱动程序

//从设备树获得GPIO
count = of_gpio_count(node);
for (i = 0; i < count; i++)
    gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
//从GPIO获得中断号
gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);
//申请中断
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, \ 
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
//中断函数
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	int val;
	val = gpiod_get_value(gpio_key->gpiod);
	

	printk("key %d %d\n", gpio_key->gpio, val);
	
	return IRQ_HANDLED;
}

gpio_key_drv.c

#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	int val;
	val = gpiod_get_value(gpio_key->gpiod);
	

	printk("key %d %d\n", gpio_key->gpio, val);
	
	return IRQ_HANDLED;
}

/* 1. 从platform_device获得GPIO
 * 2. gpio=>irq
 * 3. request_irq
 */
static int gpio_key_probe(struct platform_device *pdev)
{
	int err;
	struct device_node *node = pdev->dev.of_node;
	int count;
	int i;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
		
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	count = of_gpio_count(node);
	if (!count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	for (i = 0; i < count; i++)
	{
		/* 1. 从platform_device获得GPIO */
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
			printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
			return -1;
		}
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;

		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;

		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

		/* 2. gpio=>irq */
		gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}

	for (i = 0; i < count; i++)
	{
		/* 3. request_irq */
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
	}
        
    return 0;
    
}

static int gpio_key_remove(struct platform_device *pdev)
{
	//int err;
	struct device_node *node = pdev->dev.of_node;
	int count;
	int i;

	count = of_gpio_count(node);
	for (i = 0; i < count; i++)
	{
		free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
	}
	kfree(gpio_keys_100ask);
    return 0;
}


static const struct of_device_id ask100_keys[] = {
    { .compatible = "100ask,gpio_key" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {
    .probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "100ask_gpio_key",
        .of_match_table = ask100_keys,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{
    int err;
    
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    err = platform_driver_register(&gpio_keys_driver); 
	
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&gpio_keys_driver);
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");

你可能感兴趣的:(#,嵌入式Linux,STM32MP157,linux,总线设备驱动模型,驱动开发)