LV06-13-中断-02-中断的申请流程

在linux中如何使用中断?基本流程是怎样的?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、中断相关API

1. 中断申请函数

1.1 request_irq()

1
2
3
4
5
6
7
// #include <linux/interrupt.h>
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

request_irq() 函数的主要功能是请求一个中断号, 并将一个中断处理程序与该中断号关联起来。 当中断事件发生时, 与该中断号关联的中断处理程序会被调用执行。request_irq() 函数可能会导致睡眠,因此不能在中断上下半部或者其他禁止睡眠的代码段中使用 request_irq() 函数。request_irq() 函数会激活(使能)中断,所以不需要我们手动去使能中断。

参数说明:

  • irq: 要请求的中断号(IRQ number) 。中断号需要通过 gpio_to_irq() 函数映射 GPIO 引脚来获得。
  • handler: irq_handler_t 类型,指向中断处理程序的函数指针。中断处理程序是在中断事件发生时调用的函数, 用于处理中断事件 。
  • flags: 标志位, 用于指定中断处理程序的行为和属性, 如中断触发方式、 中断共享等。可以看这里:interrupt.h - include/linux/interrupt.h
1
2
3
4
5
6
IRQF_TRIGGER_NONE   : 无触发方式, 表示中断不会被触发。
IRQF_TRIGGER_RISING : 上升沿触发方式, 表示中断在信号上升沿时触发。
IRQF_TRIGGER_FALLING: 下降沿触发方式, 表示中断在信号下降沿时触发。
IRQF_TRIGGER_HIGH : 高电平触发方式, 表示中断在信号为高电平时触发。
IRQF_TRIGGER_LOW : 低电平触发方式, 表示中断在信号为低电平时触发。
IRQF_SHARED : 中断共享方式, 表示中断可以被多个设备共享使用。

比如 I.MX6U-ALPHA 开发板上的 KEY0 使用 GPIO1_IO18,按下 KEY0 以后为低电平,因此可以设置为下降沿触发,也就是将 flags 设置为 IRQF_TRIGGER_FALLING。

  • name: 中断的名称, 用于标识该中断,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
  • dev:如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。

返回值:成功: 0 或正数, 表示中断请求成功。失败: 负数, 表示中断请求失败, 返回的负数值表示错误代码,如果返回-EBUSY 的话表示中断已经被申请了。

1.2 free_irq()

free_irq() 函数用于释放之前通过 request_irq() 函数注册的中断处理程序。 它的作用是取消对中断的注册并释放相关的系统资源,包括中断号、 中断处理程序和设备标识等。

1
2
3
4
5
6
7
8
9
10
11
12
13
//#include <linux/interrupt.h>
const void *free_irq(unsigned int irq, void *dev_id)
{
struct irq_desc *desc = irq_to_desc(irq);
struct irqaction *action;
const char *devname;
//......
action = __free_irq(desc, dev_id);
//......
kfree(action);
return devname;
}
EXPORT_SYMBOL(free_irq);

参数说明:

  • irq: 要释放的中断号。
  • dev_id: 设备标识, 用于区分不同的中断请求。 它通常是在 request_irq 函数中传递的设备特定数据指针。(如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。)

返回值:

1.3 最后一个参数产生的崩溃

后面测试的过程中出现崩溃:

image-20250323100809900

这里分析一下原因,中断申请函数request_irq()与中断释放函数free_irq()的最后一个参数(void *dev 设备结构体)要保持一致,必须是同一个指针。我们可以看一下释放函数free_irq()的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const void *free_irq(unsigned int irq, void *dev_id)
{
struct irq_desc *desc = irq_to_desc(irq);
struct irqaction *action;
const char *devname;

if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return NULL;
//......
action = __free_irq(desc, dev_id);
if (!action)
return NULL;

devname = action->name;
kfree(action);
return devname;
}

再来看这个 __free_irq()

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
static struct irqaction *__free_irq(struct irq_desc *desc, void *dev_id)
{
//......
/*
* There can be multiple actions per IRQ descriptor, find the right
* one based on the dev_id:
*/
action_ptr = &desc->action;
for (;;) {
action = *action_ptr;

if (!action) {
WARN(1, "Trying to free already-free IRQ %d\n", irq);
raw_spin_unlock_irqrestore(&desc->lock, flags);
chip_bus_sync_unlock(desc);
mutex_unlock(&desc->request_mutex);
return NULL;
}

if (action->dev_id == dev_id)
break;
action_ptr = &action->next;
}
//......
}

可以看到,这里会判断request_irq 填入的 dev_id free_irq dev_id (也就是第二个参数),如果一致,才会退出循环。

2. 中断号获取

2.1 gpio_to_irq()

2.1.1 函数说明

gpio_to_irq() 是一个宏,使用的时候需要包含 <linux/gpio.h>,会定义成__gpio_to_irq()

1
2
3
4
static inline int __gpio_to_irq(unsigned gpio)
{
return gpiod_to_irq(gpio_to_desc(gpio));
}

该函数是一个用于将 GPIO 引脚映射到对应中断号的函数。 它的作用是根据给定的 GPIO 引脚号, 获取与之关联的中断号。

参数说明:

  • gpio: 要映射的 GPIO 引脚号。

返回值:成功,返回值为该 GPIO 引脚所对应的中断号。失败,返回值为负数, 表示映射失败或无效的 GPIO 引脚号。

2.1.2 怎么知道映射到哪个中断?

我们看一下gpiod_to_irq(),这个函数定义如下:

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
int gpiod_to_irq(const struct gpio_desc *desc)
{
struct gpio_chip *chip;
int offset;

/*
* Cannot VALIDATE_DESC() here as gpiod_to_irq() consumer semantics
* requires this function to not return zero on an invalid descriptor
* but rather a negative error number.
*/
if (!desc || IS_ERR(desc) || !desc->gdev || !desc->gdev->chip)
return -EINVAL;

chip = desc->gdev->chip;
offset = gpio_chip_hwgpio(desc);
if (chip->to_irq) {
int retirq = chip->to_irq(chip, offset);

/* Zero means NO_IRQ */
if (!retirq)
return -ENXIO;

return retirq;
}
return -ENXIO;
}
EXPORT_SYMBOL_GPL(gpiod_to_irq);

感觉有点复杂,目前的学习重点不在这里,以后有需要再补充。

2.2 irq_of_parse_and_map()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* irq_of_parse_and_map - Parse and map an interrupt into linux virq space
* @dev: Device node of the device whose interrupt is to be mapped
* @index: Index of the interrupt to map
*
* This function is a wrapper that chains of_irq_parse_one() and
* irq_create_of_mapping() to make things easier to callers
*/
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;

if (of_irq_parse_one(dev, index, &oirq))
return 0;

return irq_create_of_mapping(&oirq);
}
EXPORT_SYMBOL_GPL(irq_of_parse_and_map);

当中断信息写在设备树中的时候,可以通过此函数从 interupts 属性中提取到对应的设备号。

参数说明:

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

返回值:成功,返回值为该 GPIO 引脚所对应的中断号。失败,返回值为负数, 表示映射失败或无效的 GPIO 引脚号。

2.3 of_irq_get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int of_irq_get(struct device_node *dev, int index)
{
int rc;
struct of_phandle_args oirq;
struct irq_domain *domain;

rc = of_irq_parse_one(dev, index, &oirq);
if (rc)
return rc;

domain = irq_find_host(oirq.np);
if (!domain)
return -EPROBE_DEFER;

return irq_create_of_mapping(&oirq);
}

3. 中断服务函数

中断处理程序是在中断事件发生时自动调用的函数。 它负责处理与中断相关的操作, 例如读取数据、 清除中断标志、 更新状态等。中断服务函数的函数类型如下:

1
typedef irqreturn_t (*irq_handler_t)(int, void *);

我们可以定义一个中断服务函数如下:

1
irqreturn_t handler(int irq, void *dev_id);

handler 函数是一个中断服务函数, 用于处理特定中断事件。 它在中断事件发生时被操作系统或硬件调用, 执行必要的操作来响应和处理中断请求。

参数说明:

  • irq: 表示中断号或中断源的标识符。 它指示引发中断的硬件设备或中断控制器。

  • dev_id: 是一个 void 类型的指针, 用于传递设备特定的数据或标识符。需要与 request_irq() 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。

返回值:irqreturn_t 类型,是一个特定类型的枚举值, 用于表示中断服务函数的返回状态。它可以有以下几种取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
// IRQ_NONE: 表示中断服务函数未处理该中断, 中断控制器可以继续处理其他中断请求。
IRQ_NONE = (0 << 0),
// IRQ_HANDLED: 表示中断服务函数已成功处理该中断, 中断控制器无需进一步处理。
IRQ_HANDLED = (1 << 0),
// IRQ_WAKE_THREAD: 表示中断服务函数已处理该中断, 并且请求唤醒一个内核线程来继续执行进一步的处理。 这在一些需要长时间处理的中断情况下使用。
IRQ_WAKE_THREAD = (1 << 1),
};

一般中断服务函数返回值使用如下形式:

1
return IRQ_RETVAL(IRQ_HANDLED)

在处理程序中, 通常需要注意以下几个方面:

(1) 处理程序应该尽可能地快速执行, 以避免中断丢失或过多占用 CPU 时间。

(2) 如果中断源是共享的, 处理程序需要处理多个设备共享同一个中断的情况。

(3) 处理程序可能需要与其他部分的代码进行同步, 例如访问共享数据结构或使用同步机制来保护临界区域。

(4) 处理程序可能需要与其他线程或进程进行通信, 例如唤醒等待的线程或发送信号给其他进程。

4. 中断使能与禁止函数

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

1
2
void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);

enable_irq()disable_irq() 用于使能和禁止指定的中断, irq 就是要使能/禁止的中断号。 disable_irq() 函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数 disable_irq_nosync()

1
void disable_irq_nosync(unsigned int irq)

disable_irq_nosync() 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是常说的关闭全局中断,这个时候可以使用如下两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local_irq_enable()
local_irq_disable()

#ifdef CONFIG_TRACE_IRQFLAGS

#define local_irq_enable() \
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
#define local_irq_disable() \
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
// ......
#else /* !CONFIG_TRACE_IRQFLAGS */

#define local_irq_enable() do { raw_local_irq_enable(); } while (0)
#define local_irq_disable() do { raw_local_irq_disable(); } while (0)
// ......
#endif /* CONFIG_TRACE_IRQFLAGS */

这两个函数定义在 irqflags.h - include/linux/irqflags.h 中。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 函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数(定义在 irqflags.h - include/linux/irqflags.h 中):

1
2
local_irq_save(flags);
local_irq_restore(flags);

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

二、中断申请流程分析

接下来我们看一下中断申请函数,来了解一下中断的申请的大概流程。

1. request_irq()

中断申请使用的是 request_irq() 函数, 它用于请求一个中断号(IRQ number) 并将一个中断处理程序与该中断关联起来:

1
2
3
4
5
6
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

从上面的内容可以得到 request_irq() 函数实际上是调用了request_threaded_irq()函数来完成中断申请的过程。 request_threaded_irq()函数提供了线程化的中断处理方式, 可以在中断上下文中执行中断处理函数。

2. request_threaded_irq()

2.1 函数说明

request_threaded_irq()函数是 Linux 内核提供的一个功能强大的函数, 用于请求分配一个中断, 并将中断处理程序与该中断关联起来。 该函数的主要作用是在系统中注册中断处理函数,以响应对应中断的发生。函数定义如下:

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
84
85
86
87
88
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)
{
//(1) 声明变量和初始化
struct irqaction *action; // 中断动作结构体指针
struct irq_desc *desc; // 中断描述符指针
int retval; // 返回值
//(2) 参数检查:检查中断号是否为未连接状态
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;

/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
// 检查中断标志的有效性, 包括共享标志与设备 ID 的关联性, 条件挂起标志的有效性, 以及无挂起标志与条件挂起标志的关联性。
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
//(3) 获取中断描述符:根据中断号获取中断描述符
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
//(4) 检查中断设置:检查中断设置是否可以进行中断请求, 以及是否为每个 CPU 分配唯一设备 ID
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
//(5) 处理中断处理函数和线程处理函数:如果未指定中断处理函数, 则使用默认的主处理函数
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
//(6) 分配并初始化中断动作数据结构
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler; // 中断处理函数
action->thread_fn = thread_fn;// 线程处理函数
action->flags = irqflags; // 中断标志
action->name = devname; // 设备名称
action->dev_id = dev_id; // 设备 ID
//(7)获取中断的电源管理引用计数
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
//(8)设置中断并将中断动作与中断描述符关联
retval = __setup_irq(irq, desc, action);
// 处理中断设置失败的情况
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
//(9)可选的共享中断处理
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
// 如果设置中断成功且中断标志中包含共享标志(IRQF_SHARED)
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;

disable_irq(irq); // 禁用中断。
local_irq_save(flags); // 保存当前中断状态并禁用本地中断。

handler(irq, dev_id); // 调用主处理函数处理中断。

local_irq_restore(flags);// 恢复中断状态。
enable_irq(irq); // 重新使能中断。
}
#endif
return retval; // 返回设置中断的结果
}
EXPORT_SYMBOL(request_threaded_irq);

2.2 总结

request_threaded_irq()函数主要功能和作用如下:

(1) 中断请求: request_threaded_irq 函数用于请求一个中断。 它会向内核注册对应中断号的中断处理函数, 并为该中断分配必要的资源。 中断号是标识特定硬件中断的唯一标识符。

(2) 中断处理函数关联: 通过 handler 参数, 将中断处理函数与中断号关联起来。 中断处理函数是一个预定义的函数, 用于处理中断事件。 当中断发生时, 内核将调用该函数来处理中断事件。

(3) 线程化中断处理: request_threaded_irq 函数还支持使用线程化中断处理函数。 通过指定 thread_fn 参数, 可以在一个内核线程上下文中异步执行较长时间的中断处理或延迟敏感的工作。 这有助于避免在中断上下文中阻塞时间过长。

(4) 中断属性设置: 通过 irqflags 参数, 可以设置中断处理的各种属性和标志。 例如, 可以指定中断触发方式(上升沿、 下降沿、 边沿触发等) 、 中断类型(边沿触发中断、 电平触发中断等) 以及其他特定的中断行为。

(5) 设备标识关联: 通过 dev_id 参数, 可以将中断处理与特定设备关联起来。 这样可以在中断处理函数中访问与设备相关的数据。 设备标识符可以是指向设备结构体或其他与设备相关的数据的指针。

(6) 错误处理: request_threaded_irq 函数会返回一个整数值, 用于指示中断请求的结果。如果中断请求成功, 返回值为 0; 如果中断请求失败, 则返回一个负数错误代码, 表示失败的原因。

3. 中断申请demo

3.1 demo源码

demo源码可以看这里:13_interrupt/04_int_gpio · 苏木/imx6ull-driver-demo - 码云 - 开源中国。在这个demo源码中,不需要像之前一样自己获取gpio的地址,然后映射,也不需要在设备树添加什么东西,就直接通过GPIO的编号申请中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int gpio_irq_init(_CHAR_DEVICE *p_chrdev)
{
int irq_num = -1;
// 将GPIO引脚映射到中断号
irq_num = gpio_to_irq(GPIO_PIN);
PRT("GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);

// 请求中断
if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0)
{
PRTE("Failed to request IRQ %d\n", irq_num);
// 请求中断失败,释放GPIO引脚
gpio_free(GPIO_PIN);
return -ENODEV;
}
return 0;
}
  • 怎么计算GPIO编号

i.MX6ULL的GPIO引脚被组织成多个Bank,每个Bank包含32个GPIO引脚。这个我们可以看数据手册过着参考手册,我这里看的数据手册:

image-20250322222306438

GPIO引脚的编号通常以GPIOx_y的形式表示,其中x表示Bank编号,y表示该Bank中的引脚编号。例如:

  • GPIO1_0 表示Bank 1的第0个引脚。
  • GPIO2_15 表示Bank 2的第15个引脚。

那么,i.MX6ULL的GPIO编号可以通过以下公式计算:

1
GPIO编号 = (Bank编号 - 1) * 32 + 引脚编号

3.2 开发板验证

我们编译后拷贝到开发板,加载驱动:

1
insmod sdriver_demo.ko
image-20250322222543265

可以看到这里申请到的中断号是79。我这里其实有个疑问,就是之前裸机开发的时候,计算过GPIO1_IO18的中断号,应该是67+32=99,在NXP官方提供的库文件中也是这样的:

image-20250322223249063

但是linux申请这里是79,具体可能是有什么映射关系,这里没有深究了,后面有机会再补充吧。驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册上,输入如下命令:

1
cat /proc/interrupts
image-20250322222720633

然后我们按下按键,然后抬起,会看到中断服务函数被执行了:

image-20250322222817637

三、重要数据结构

大概会有以下几个相关的数据结构:

image-20250317154008608

最核心的结构体是 struct irq_desc,之前为了易于理解,我们说在 Linux 内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc 数组。注意:如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替 irq_desc 数组。 SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。

1. struct irq_desc

1.1 结构体说明

struct irq_desc定义如下:

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
struct irq_desc {
struct irq_common_data irq_common_data; /* 通用中断数据 */
struct irq_data irq_data; /* 中断数据 */
unsigned int __percpu *kstat_irqs; /* 中断统计信息 */
irq_flow_handler_t handle_irq; /* 中断处理函数 */
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler; /* 预处理中断处理函数 */
#endif
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int core_internal_state__do_not_mess_with_it;/* 内核内部状态标志位, 请勿修改 */
unsigned int depth; /* 嵌套中断禁用计数 */
unsigned int wake_depth; /* 嵌套唤醒使能计数 */
unsigned int tot_count;
unsigned int irq_count; /* 用于检测损坏的 IRQ 计数 */
unsigned long last_unhandled; /* 未处理计数的老化计时器 */
unsigned int irqs_unhandled; /* 未处理的中断计数 */
atomic_t threads_handled; /* 处理中断的线程计数 */
int threads_handled_last;
raw_spinlock_t lock; /* 自旋锁 */
struct cpumask *percpu_enabled;/* 指向每个 CPU 的使能掩码 */
const struct cpumask *percpu_affinity;/* 指向每个 CPU 亲和性掩码 */
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint; /* CPU 亲和性提示 */
struct irq_affinity_notify *affinity_notify;/* CPU 亲和性变化通知 */
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask; /* 等待处理的中断掩码 */
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active; /* 活动中的线程计数 */
wait_queue_head_t wait_for_threads; /* 等待线程的等待队列头 */
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;/* proc 文件系统目录项 */
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;/* 调试文件系统文件 */
const char *dev_name; /* 设备名称 */
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj; /* 内核对象 */
#endif
struct mutex request_mutex; /* 请求互斥锁 */
int parent_irq; /* 父中断号 */
struct module *owner; /* 模块拥有者 */
const char *name; /* 中断名称 */
} ____cacheline_internodealigned_in_smp;

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

image-20250317154800714

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

  • (1)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。

  • (2)模块的中断处理函数

比如对于GPIO模块向GIC发出的中断B,它的处理函数是irq_desc[B].handle_irq。

BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。

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

  • (3)外部设备提供的处理函数

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

对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。

一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。这个链表就是 action 链表。

对于我们举的这个例子来说, irq_desc 数组如下:

image-20250317172230937

1.2 总结

以下是 struct irq_desc 结构体的主要作用和功能:

(1) 中断处理函数管理: irq_desc 结构体中的 handle_irq 字段保存中断处理函数的指针。当硬件触发中断时, 内核会调用该函数来处理中断事件。

(2) 中断行为管理: irq_desc 结构体中的 action 字段是一个指向中断行为列表的指针。中断行为是一组回调函数, 用于注册、 注销和处理与中断相关的事件。

(3) 中断统计信息: irq_desc 结构体中的 kstat_irqs 字段是一个指向中断统计信息的指针。该信息用于记录中断事件的发生次数和处理情况, 可以帮助分析中断的性能和行为。

(4) 中断数据管理: irq_desc 结构体中的 irq_data 字段保存了与中断相关的数据, 如中断号、 中断类型等。 这些数据用于识别和管理中断。

(5) 通用中断数据管理: irq_desc 结构体中的 irq_common_data 字段保存了与中断处理相关的通用数据, 如中断控制器、 中断屏蔽等。 这些数据用于处理和控制中断的行为。

( 6) 中断状态管理: irq_desc 结构体中的其他字段用于管理中断的状态, 如嵌套中断禁用计数、 唤醒使能计数等。 这些状态信息帮助内核跟踪和管理中断的状态变化。

通过使用 irq_desc 结构体, 内核可以有效地管理和处理系统中的硬件中断。 它提供了一个统一的接口, 用于注册和处理中断处理函数、 管理中断行为, 并提供了必要的信息和数据结构来监视和控制中断的行为和状态。

2. struct irqaction

2.1 结构体说明

struct irqaction 是 Linux 内核中用于描述中断行为的数据结构之一。 它用于定义中断处理过程中的回调函数和相关属性。 irqaction 结构体的主要功能是管理与特定中断相关的行为和处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct irqaction {
irq_handler_t handler; // 中断处理函数
void *dev_id; // 设备 ID
void __percpu *percpu_dev_id;// 每个 CPU 的设备 ID
struct irqaction *next; // 下一个中断动作结构体
irq_handler_t thread_fn;// 线程处理函数
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; // proc 文件系统目录项指针
} ____cacheline_internodealigned_in_smp;

当调用 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,这就是中断上半部、下半部。

在 reqeust_irq 时可以传入 dev_id,为何需要 dev_id?

(1)中断处理函数执行时,可以使用 dev_id 。

(2)卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项。

所以在共享中断中必须提供 dev_id,非共享中断可以不提供。

2.2 总结

以下是 struct irqaction 结构体的主要作用和功能:

(1) 中断处理函数管理: irqaction 结构体中的 handler 字段保存中断处理函数的指针。 该函数在中断发生时被调用, 用于处理中断事件。

(2) 中断处理标志管理: irqaction 结构体中的 flags 字段用于指定中断处理的各种属性和标志。 这些标志控制中断处理的行为, 例如触发方式、 中断类型等。

(3) 设备标识符管理: irqaction 结构体中的 dev_id 字段用于保存与中断处理相关的设备标识符。 它可以是指向设备结构体或其他与设备相关的数据的指针, 用于将中断处理与特定设备关联起来。

(4) 中断行为链表管理: irqaction 结构体中的 next 字段是一个指向下一个 irqaction 结构体的指针, 用于构建中断行为的链表。 这样可以将多个中断处理函数链接在一起, 以便在中断发生时按顺序调用它们。

通过使用 irqaction 结构体, 内核可以灵活地定义和管理与特定中断相关的行为和处理函数。 它提供了一个统一的接口, 用于注册和注销中断处理函数, 并提供了必要的属性和数据结构来控制中断处理的行为和顺序。

3. struct irq_data

struct irq_data主要内容如下:

image-20250317202410518
1
2
3
4
5
6
7
8
9
10
11
12
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_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。

4. struct irq_domain

struct irq_domain主要内容如下:

image-20250317202625200

当我们后面学习如何在设备树中指定中断,设备树的中断如何被转换为 irq 时, irq_domain 将会起到极大的作为。这里先简单了解一下,在设备树中会看到这样的属性:

1
2
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 结构体,里面有各种操作函数,主要是:

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

5. struct irq_chip

struct irq_chip 主要内容如下 :

1
2
3
4
5
6
7
8
9
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
//......
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
* @parent_device:	pointer to parent device for irqchip
* @name: name for /proc/interrupts
* @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
* @irq_set_affinity: Set the CPU affinity on SMP machines. If the force
* argument is true, it tells the driver to
* unconditionally apply the affinity setting. Sanity
* checks against the supplied affinity mask are not
* required. This is used for CPU hotplug where the
* target CPU is not yet set in the cpu_online_mask.

我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。

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

四、中断在设备树中的写法

参考文档可以看这里:interrupts.txt - Documentation/devicetree/bindings/interrupt-controller/interrupts.txt - Linux source code v4.19.7

1. 设备树里的中断控制器

中断 硬件框图如下:

image-20250317154800714

在硬件上,“中断控制器”只有 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 的值一般有如下取值:

(1)#interrupt-cells=<1> :别的节点要使用这个中断控制器时,只需要一个 cell 来表明使用“哪一个中断”。

(2)#interrupt-cells=<2> :别的节点要使用这个中断控制器时,需要一个 cell 来表明使用“哪一个中断”;还需要另一个 cell 来描述中断,一般是表明触发类型(trigger type and level flags):

1
2
3
4
5
// 第 2 个 cell 的 bits[3:0]
1 = low-to-high edge triggered,上升沿触发
2 = high-to-low edge triggered,下降沿触发
4 = active high level-sensitive,高电平触发
8 = active low level-sensitive,低电平触发

示例如下:

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

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

2. 设备树里使用中断

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

2.1 相关属性

  • 要用哪一个中断控制器里的中断?
1
interrupt-parent=<&XXXX>
  • 要用哪一个中断?
1
interrupts=<>;

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

1
2
3
4
5
6
7
8
9
10
11
12
i2c@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”,比如:

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

2.2 总结

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

①、 #interrupt-cells,指定中断源的信息 cells 个数。

②、 interrupt-controller,表示当前节点为中断控制器。

③、 interrupts,指定中断号,触发方式等。

④、 interrupt-parent,指定父中断,也就是中断控制器。

3. 设备树里中断节点的示例

3.1 示例1

我们看一下 imx6ul.dtsi - arch/arm/boot/dts/imx6ul.dtsi

1
2
3
4
5
6
7
8
9
10
11
intc: interrupt-controller@a01000 {
compatible = "arm,gic-400", "arm,cortex-a7-gic";
interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
#interrupt-cells = <3>;
interrupt-controller;
interrupt-parent = <&intc>;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x2000>,
<0x00a04000 0x2000>,
<0x00a06000 0x2000>;
};

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

3.2 示例2

对于 gpio 来说, gpio 节点也可以作为中断控制器,比如 imx6ul.dtsi 文件中的 gpio5 节点内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
gpio5: gpio@20ac000 {
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>;
clocks = <&clks IMX6UL_CLK_GPIO5>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-ranges = <&iomuxc 0 7 10>, <&iomuxc 10 5 2>;
};

在这段代码的第 4 行, interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1,有

image-20250318085547111

可以看出, GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应 GPIO5_IO00~GPIO5_IO15 这低 16 个 IO, 75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。

第 8 行, interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。

第 9 行,将#interrupt-cells 修改为 2。

怎么引用这个GPIO节点的中断控制器?应该是在NXP维护的4.1.15版本内核中有 arch/arm/boot/dts/imx6ull-14x14-evk.dts 设备树文件,上面有这样一个节点:

1
2
3
4
5
6
7
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
position = <0>;
interrupt-parent = <&gpio5>;
interrupts = <0 8>;
};

fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片, fxls8471 有一个中断引脚链接到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为 GPIO5_IO00。

第 5 行, interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。

第 6 行, interrupts 设置中断信息, 0 表示 GPIO5_IO00, 8 表示低电平触发。

3.3 示例3

这里参考的是伟东山的嵌入式教程,所以以 100ASK_IMX6ULL 开发板为例,在 arch/arm/boot/dts 目录下可以看到2 个文件: imx6ull.dtsi、 100ask_imx6ull-14x14.dts 。我们把里面有关中断的部分内容抽取出来。

image-20250317210527344

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

image-20250317210628880

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

3.4 alpha开发板中的按键中断

3.4.1 硬件原理图

开发板上有一个按键,原理图如下:

image-20250318091821501

按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的, KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。搜一下参考手册就会发现,这个引脚是GPIO1_IO18:

image-20250318092001841

3.4.2 中断号

可以看这个笔记《LV04-07-中断与异常-05-IMX6ULL按键中断实例 | 苏木》这里再简单了解一下。我们前边知道了按键接在了GPIO1_IO18上边,我们可以查看《I.MX6UL参考手册》的3.2 Cortex A7 interrupts一节,找到这个GPIO管脚对应的中断号:

可以看到GPIO1的0 -15管脚使用的是66,16 - 31使用的是67,这里只是IRQ的编号,对应到 GIC 的 SPI中断号需要在此编号基础上加上 32,所以这里的按键中断号实际为99(67+32)。但是其实在linux中开发的时候,会有函数(例如 gpio_to_irq())自动帮我们计算,我们只需要知道是哪个引脚就可以了。

3.4.3 触发方式

触发方式就可以看这个irq.h - include/dt-bindings/interrupt-controller/irq.h

1
2
3
4
5
6
#define IRQ_TYPE_NONE 0 		// 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发

3.4.4 中断节点

按键 KEY0 使用中断模式,需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
sdev_key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "sdev_key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
status = "okay";
};

第 8 行,设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。

第 9 行,设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18号 IO。

其中这个gpio1是在 imx6ul.dtsi

1
2
3
4
5
6
7
8
9
10
11
12
13
gpio1: gpio@209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_GPIO1>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-ranges = <&iomuxc 0 23 10>, <&iomuxc 10 17 6>,
<&iomuxc 16 33 16>;
};

4. 在代码中获得中断

之前我们学习设备树的时候 , 知道设备树中的节点有些能被转换为内核里的platform_device,有些不能:

(1)根节点下含有 compatile 属性的子节点,会转换为 platform_device

(2)含有特定 compatile 属性的节点的子节点,会转换为 platform_device如果一个节点的 compatile 属性,它的值是这 4 者之一: “simplebus”,”simple-mfd”,”isa”,”arm,amba-bus”,那么它的子结点(需含 compatile 属性)也可以转换为 platform_device。

(3)总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device。

4.1 对于 platform_device

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

1
2
3
4
5
6
7
8
9
10
11
/**
* 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.2 对于 I2C 设备、 SPI 设备

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int i2c_device_probe(struct device *dev)
{
//......
if (!client->irq && !driver->disable_i2c_core_irq_mapping) {
int irq = -ENOENT;

if (client->flags & I2C_CLIENT_HOST_NOTIFY) {
//......
} else 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);
}
//......
client->irq = irq;
}
//......
}

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

1
2
3
4
5
6
7
8
9
10
static int spi_drv_probe(struct device *dev)
{
//......
if (dev->of_node) {
spi->irq = of_irq_get(dev->of_node, 0);//从设备树解析出中断号
//......
}
//......
}

4.3 调用 of_irq_get() 获得中断号

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

4.4 对于 GPIO

可以使用gpio_to_irq() 或者 gpiod_to_irq()获得中断号。例如gpio_keys.c - drivers/input/keyboard/gpio_keys.c中第559行:

image-20250318093808546

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

1
2
3
4
5
6
7
8
9
10
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:

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

再去使用 gpiod_to_irq 获得中断号:

1
irq = gpiod_to_irq(bdata->gpiod);

参考资料:

genirq: add threaded interrupt handler support - kernel/git/torvalds/linux.git - Linux kernel source tree

Linux中断管理 (1)Linux中断管理机制 - ArnoldLu - 博客园

Linux 卸载驱动时,提示:Trying to free already-free IRQ-CSDN博客

嵌入式Linux驱动笔记(二十七)——中断子系统框架分析_irq: type mismatch-CSDN博客