0%

异常和中断

1.异常和中断

1.1 异常和中断的概念

异常不可屏蔽,中断可以屏蔽,中断一种特殊的异常。

image-20240105105633741

  1. 初始化,设置中断源(设置中断源、设置中断控制器、使能CPU总开关)
  2. 执行程序
  3. 产生中断:中断源 -> 中断控制器 -> CPU
  4. CPU每执行完一条指令,都会检查有无中断/异常产生
  5. CPU发现有中断/异常产生,开始处理
  6. 对于不同的异常,跳去不同的地址执行指令(中断向量表),这些指令只是一条跳转指令,跳去执行某个函数
  7. 处理函数:保存现场,分辨中断源并调用相应的处理函数,恢复现场

1.2 异常向量表

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

1
2
3
4
5
6
7
8
_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会由硬件执行向量表中对应位置的指令。

这些指令存放的位置是固定的,比如对于 ARM9 芯片中断向量的地址是 0x18。当发生中断时,CPU 就强制跳去执行 0x18 处的代码。

在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU 就会执行向量表中的跳转指令,去调用更复杂的函数。

当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000, 指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址是固定的:复位向量偏移地址是 0,中断是 0x18。

对于 ARM 的中断控制器,称为 GIC (Generic Interrupt Controller),到目前已经更新到 v4 版本了。

各个版本的差别可以看这里:https://developer.arm.com/ip-products/system-ip/systemcontrollers/interrupt-controllers

简单地说,GIC v3/v4 用于 ARMv8 架构,即 64 位 ARM 芯片。 而 GIC v2 用于 ARMv7 和其他更低的架构。

2. Linux 对于中断的处理

(1) 进程/线程/中断的核心:栈

当 Linux 在执行进程、线程的切换或者中断函数的处理时,需要保护现场,也就是将CPU内寄存器的值先保存在内存中,将寄存器的空间“腾”出来给待执行程序用。而保存现场这部分内存就是栈结构。

(2) 进程/线程的概念

  • 进程的引入:为例实现程序的并发执行

  • 线程的引入:进程之间传递资源消耗过大,而一个进程内的多个线程可以共享资源(如全局变量)。

  • 进程是资源分配的单位,线程是调度的单位,所以每一个线程都有自己的“栈”,以便在调度时保存/恢复现场。

2.1 Linux 中断的演进

2.1.1 硬件中断和软件中断

硬件中断我们比较熟悉,由硬件本身产生中断信号触发,执行对应的处理函数。而软件中断是一种灵活的,人为的制造中断。

(1)硬件中断

对于硬件中断,触发信号由硬件产生,产生之后会尽快被处理。Linux系统对于硬件中断的处理原则:硬件中断不能嵌套,并且硬件中断的处理越快越好

不能嵌套是为了防止突发的大量中断将栈空间耗尽。

在中断的处理过程中,CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理,例如进程调度靠定时器中断来实现。

在 Linux 为某一虚拟中断号 irq 配置处理函数:

1
2
static inline int __must_check 
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

虚拟中断号 irq 的处理函数 handler 的代码尽可能高效,耗时越短越好。

为什么是虚拟中断号,后面会讲(硬件中断号 hwirq ←→ irq 虚拟中断号)

(2)软件中断

而对于软件中断,需要知道软件中断的触发如何产生,以及何时进行软件中断的处理:

  1. 由软件来触发,对于 X 号软件中断,只需要将它的 flag 设置为 1 就表示发生了该中断。
  2. 软件中断不像硬件中断那样急迫,不必立刻处理;在 Linux 系统中因为各种硬件中断频发,例如定时器中断(心跳)就每 10ms 发生一次;所以在处理完硬件中断后,检查是否有软件中断被触发,如果有触发就去处理相应的软件中断。

Linux 的软件中断,参见内核源码include/linux/interrupt.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
};

配置软件中断的处理函数:

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

触发软件中断:

1
extern void raise_softirq(unsigned int nr);

2.1.2 耗时中断

有些硬件中断不可能在短时间内处理完成,例如按键中断,需要等待几十毫秒的时间来消除抖动,不可能在硬件中断的处理函数中等待这么长的时间,使得其他硬件中断不能够及时响应,因此需要一些方法来解决此类问题。

可以将中断拆分成上半部和(硬件中断)下半部(软件中断是一种实现方式),在上半部中快速处理一些紧急的操作,在下半部中进行耗时的,可以被打断的操作:

image-20240105162631985

主要讲两种中断下半部的处理方式:tasklet(小任务)、work queue (工作队列)

2.1.3 下半部耗时不长-tasklet

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

1
TASKLET_SOFTIRQ

通过下图中的函数调用关系,可以比较清楚的看出中断上半部和下半部的处理流程:

image-20240105163909446

在上半部中,处于关中断状态,会根据硬件中断触发源来执行对应的中断处理函数;

如果当前没有下半部的软件中断正在执行并且有软件中断被触发,就根据 softirq_vec 数组来执行对应的软件中断处理函数(注意,多个中断的下半部是汇集在一起进行处理的,即sfotirq_vec数组中可能有多个信号被触发)。

中断上半部、下半部的执行过程中,不能执行休眠函数:在中断中休眠的话,以后谁来调度进程啊

2.1.4 下半部复杂耗时长-工作队列

在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但毕竟整个中断的处理还没走完,这期间 APP 是无法执行的,因此如果下半部耗时过长,并且使用软件中断的方式实现,则 APP 在中断执行过程中将无法相应,这同样是不可接收的,因此这种情况下,下半部可以使用内核线程来实现:在中断上半部唤醒内核线程。内核线程和 APP 都 一样竞争执行,APP 有机会执行,系统不会卡顿

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

image-20240105172133397

kworker 线程要去 work queue 上取出一个一个 work, 来执行它里面的函数。使用方式如下:

  1. 创建 work,将 work 和对应的处理函数绑定:

    1
    2
    #define DECLARE_WORK(n, f)						\
    struct work_struct n = __WORK_INITIALIZER(n, f)

    将一个 work_struct 结构体和对应的处理函数 f 绑定

  2. 要执行处理函数时,仅需要将 work 提交给 work queue 即可:

    1
    2
    3
    4
    static inline bool schedule_work(struct work_struct *work)
    {
    return queue_work(system_wq, work);
    }

    上述函数会把 work 提供给系统默认的 work queue:system_wq,它是一 个队列。

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

  3. schedule_work 函数不仅仅是把 work 放入队列,还会把 kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。

很耗时的中断处理,应该放到线程里去,从而防止 APP 不能够及时响应

既然是在线程中运行,那对应的函数可以休眠

2.1.5 新技术 - threaded irq

以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行, 多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中没有问题,但是在SMP(Symmetric multiprocessing)系统中,这种方式不能够完全发挥多核的性能。因此,新技术 threaded irq 干脆为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。threaded irq 使用方法:

1
2
3
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
  • irq:指示哪个中断
  • handler:表示中断处理的上半部函数,可以为空
  • thread_fn:表示在线程中运行的函数

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

Linux 中断系统最核心的结构体是 irq_desc,它是一个结构体数组,该结构体内通过成员 action 构建了一个链表,该数组用以记录所有硬件中断的相关数据。

image-20240105194202446

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

2.2.1 irq_desc 数组

irq_desc 结构体在 include/linux/irqdesc.h 中定义,它的主要成员如下:

1
2
3
4
struct irq_data irq_data;
irq_flow_handler_t handle_irq;
const char *name;
struct irqaction *action;

我们首先通过一个例子来理解 irq_desc 数组的整体结构:

image-20240105200529422

外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC (Generic Interrupt Controller)的 A 号中断,GIC 再去中断 CPU。所以中断触发信号在图中一路从左向右传递到CPU。

当CPU收到中断触发信号之后,软件应该在图中从右到左依次询问,来定位到底是那个硬件触发了中断,具体来说:

  1. GIC 的处理函数:

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

  2. 模块的中断处理函数:

    irq_desc[B]. handle_irq,BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。该函数会去判断哪个设备触发了中断,可能有某个设备触发了中断,也可能有多个设备触发了中断,因此irq_desc[B].handle_irq 会遍历 action 成员指向的链表,并调用链表元素中的“具体的中断处理函数”,这些函数自行判断该中断是否由自己产生,若是则处理。

  3. 外部设备提供的中断处理函数:

    对应 irqaction 结构体中的 handler 或者 thread_fn 成员,这些处理函数由自己驱动程序提供,应该具有判断设备是否发生了中断,如何处理中断的能力。

总的来说,irq_desc 数组对应“中断控制器模块”,链表中 irqaction 结构体对应产生中断源的外部设备

上述过程中忽略了一点:当执行中断处理函数时,从中获取的是硬件中断号 (hwirq),而irq_desc 数组的寻址使用的是虚拟中断号 (irq) ,二者的映射在 2.2.3 & 2.2.4 节中讨论

2.2.2 irqaction 结构体

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct irqaction {
irq_handler_t handler;
irq_handler_t thread_fn;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;

当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体,该结构体最重要的是 handler、thread_fn、thread 成员:

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

其次是 dev_id 成员,在 reqeust_irq 时可以传入 dev_id,它的作用如下:

  1. 中断处理函数执行时,可以使用 dev_id
  2. 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项

2.2.3 irq_data 结构体

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

image-20240105204536400

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

2.2.4 irq_domain 结构体

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

image-20240105205204566

当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被 转换为 irq 时,irq_domain 将会起到极大的作用,举例来说,假设在设备树中有这样的属性:

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

它表示要使用 gpio1 里的第 5 号中断,因此,当我们调用 GPIO1 的中断处理函数,会获取到的 hwirq = 5。但是我们在 irq_desc 数组中寻址使用的是全局的虚拟中断号 irq。二者之间的转换就是由 GPIO1 对应的 irq_domain 结构体中的相关成员实现的。具体来说,irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数:

  1. xlate:用来解析设备树的中断属性,提取出 hwirq、type 等信息。
  2. map:把 hwirq 转换为 irq。

2.2.5 irq_chip 结构体

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

image-20240105210040202

这个结构体封装了主控芯片有关中断的相关硬件操作,例如我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断;我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。

但是对于外部设备相关的清中断操作,还是需要我们自己做的,外部设备对应我们编写的驱动程序。

2.3 配置中断资源

2.3.1 设备树中断节点的语法

参考文档:内核\Documentation\devicetree\bindings\interrupt-controller\interrupts.txt

image-20240106101640026

上图为 IMX6ULL 的中断体系,比之前多了一个 GPC INTC (General Power Controller,Interrupt Controller),我们参考 imx6ull.dtsi 中有关中断的部分内容来学习设备树中有关中断的语法:

image-20240106102152360
(1) intc
1
2
#interrupt-cells = <3>;
#interrupt-controller;
  1. 凡是包含 #interrupt-controller; 表明该节点是一个中断控制器节点
  2. #interrupt-cells = <3>;指明想要指定该中断控制器的某个中断源应该使用多少个 cell 来表示

对于顶层中断控制器 GIC 而言,3 个 cell 分别表示:

  1. 第一个 cell 表示中断类别,可选GIC_PPI,GIC_SPI,GIC_SGI
    • Private Peripheral Interrupt (PPI):以单个特定CPU核为目标的外围中断
    • Shared Peripheral Interrrupt (SPI):可以以任意CPU核为目标的外围中断
    • Software-generated interrupt (SGI):用于CPU核之间通信的中断
  2. 第二个 cell 表示中断控制器内的中断序号
  3. 第三个 cell 表示触发条件

详情可参考GIC架构规格书:IHI0069E_gic_architecture_specification.pdf

(2) gpc
1
2
3
4
#interrupt-controller;
#interrupt-cells = <3>;
interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>
interrupts-parent = <&intc>;

这也是一个中断控制器,并且它的子节点声明中断时要使用 3 个 cell

  1. interrupts-parent = :指明该中断设备的父节点
  2. interrupts =:描述本中断节点的中断触发条件
(3) gpio1
1
2
3
4
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, 					     
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-controller;
#interrupt-cells = <2>;

这是一个中断控制器,并且它的中断信号,直接传递到 GIC 的 66 和 67 号中断上。此外,它的子节点声明中断时要使用 2 个 cell。参考内核文档interrupt.txt 可知两个 cell 分别表示:

  1. 第一个 cell 表示中断控制器内的中断序号

  2. 第二个 cell 表示中断触发条件:

    1
    2
    3
    4
    5
    - 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

此外,它的属性中没有指明该节点的中断父节点,因此在该节点直接继承其父节点的 interrupts-parent 属性表明其中断父节点为 gpc

此外,通过查询IMX6ULL的数据手册,可知GPIO的共享中断触发源信息:

image-20240106114012876

(4) spidev
1
2
interrupt-parent = <&gpio1>;         
interrupts = <1 1>;

该节点就不是中断控制器了,是设备节点的中断信息。它的中断父节点是 gpio1,它在 gpio1 内的中断序号是1,并且中断触发条件是上升沿。这种设备节点的相关信息是要由我们来设置的。

(5) 新写法 - interrupts-extended 属性

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

1
interrupts-extended = <&gpio1 5 1>, <&gpio1 1 1>;

2.3.2 在代码中获得中断

在 “使用设备树的LED驱动框架”文章的的 1.4.2 节记录了设备树的哪些 device_node 会被转换为 platform_device。

(1)对于 platform_device

一个节点能被转换为 platform_device,并且它的设备树里指定了中断属性,那么可以从 platform_device 中获得“中断资源”:

1
2
3
4
5
6
7
/**  
* 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)对于 I2C 设备、SPI 设备

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int i2c_device_probe(struct device *dev)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
int status;

if (!client)
return 0;

if (!client->irq) {
int irq = -ENOENT;

if (dev->of_node) {
irq = of_irq_get_byname(dev->of_node, "irq");
if (irq == -EINVAL || irq == -ENODATA)
irq = of_irq_get(dev->of_node, 0);
} else if (ACPI_COMPANION(dev)) {
irq = acpi_dev_gpio_irq_get(ACPI_COMPANION(dev), 0);
}
if (irq == -EPROBE_DEFER)
return irq;
if (irq < 0)
irq = 0;

client->irq = irq;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int spi_drv_probe(struct device *dev)
{
const struct spi_driver *sdrv = to_spi_driver(dev->driver);
struct spi_device *spi = to_spi_device(dev);
int ret;

ret = of_clk_set_defaults(dev->of_node, false);
if (ret)
return ret;

if (dev->of_node) {
spi->irq = of_irq_get(dev->of_node, 0);
if (spi->irq == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (spi->irq < 0)
spi->irq = 0;
}
(3)调用 of_irq_get 获得中断号

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

1
2
3
4
5
6
7
8
9
10
/**
* of_irq_get - Decode a node's IRQ and return it as a Linux IRQ number
* @dev: pointer to device tree node
* @index: zero-based index of the IRQ
*
* Returns Linux IRQ number on success, or 0 on the IRQ mapping failure, or
* -EPROBE_DEFER if the IRQ domain is not yet created, or error code in case
* of any other failure.
*/
int of_irq_get(struct device_node *dev, int index)
(3)对于 GPIO

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

首先使用该函数来获取第 i 个引脚的 GPIO 的编号,并将其 flags (例如 GPIO_ACTIVE_HIGH )保存下来:

1
static inline int of_get_gpio_flags(struct device_node *np, int index, enum of_gpio_flags *flags)

然后根据 GPIO 编号转换为该 GPIO 的描述符:

1
2
/**  * Convert a GPIO number to its descriptor  */ 
struct gpio_desc *gpio_to_desc(unsigned gpio)

最后使用 gpio_to_irq 或 gpiod_to_irq 获得中断号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* gpiod_to_irq() - return the IRQ corresponding to a GPIO
* @desc: gpio whose IRQ will be returned (already requested)
*
* Return the IRQ corresponding to the passed GPIO, or an error code in case of
* error.
*/
int gpiod_to_irq(const struct gpio_desc *desc)


#define gpio_to_irq __gpio_to_irq
static inline int __gpio_to_irq(unsigned gpio)
{
return gpiod_to_irq(gpio_to_desc(gpio));
}

3. 编写 GPIO 触发中断

(1)在设备树中添加节点

image-20240107150811885

1
2
3
4
5
6
7
8
9
10
11
12
krocz_gpio {
compatible = "krocz,buttondrv";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_krocz_button>;
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
};

pinctrl_krocz_button: krocz_buttongrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x000110A0
>;
};

因为在内核中有 gpiod_to_irq 这个函数,所以不必在设备节点中指定中断。

(2)编写驱动程序

参考内核源码:drivers/input/keyboard/gpio_keys.c

注册内核模块的代码省略,主要是 platform_driver 的注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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);
reverse_led_status(); // 每触发一次,翻转一下LED引脚的输出

return IRQ_HANDLED;
}

static const struct of_device_id krocz_keys[] = {
{ .compatible = "krocz,buttondrv" },
{ },
};

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

static int gpio_key_probe(struct platform_device *pdev)
{
int err, count, i;
struct device_node *node = pdev->dev.of_node;
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) return -1;

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
for (i = 0; i < count; i++)
{
// 获取设备节点中第i个gpio引脚及其flag
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0) return -1;
// gpio引脚转gpio_desc结构体
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;
// 看函数描述好像是用来初始化GPIO引脚的
err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
// 设置GPIO引脚为输入
err = gpiod_direction_input(gpio_keys_100ask[i].gpiod);
// gpio引脚号转虚拟中断号
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
}
// 注册中断处理函数 gpio_key_isr,触发条件为下降沿
for (i = 0; i < count; i++)
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, "krocz_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, 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;
}

probe 函数的核心就是将中断处理函数 gpio_key_isr 进行注册,以便我们在按下按键时能够得到处理,因为硬件设计上有电容用来去抖动,所以不需要其他软件上的处理了。