linux驱动-中断

    中断是处理器用于异步处理外围设备请求的一种机制,可以说中断处理是操作系统管理外围设备的基石,此外系统调度、核间交互等都离不开中断,它的重要性不言而喻,本文结整理 linux 中断相关知识点。注意:本文只讨论 arm 架构,对于其他架构的不做深入研究。

一、数据结构

1、irq_domain

    用于建立中断控制器的 hwirq 与 virq 之间的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct irq_domain {
struct list_head link; // 链接到 irq_domain_list 链表
const char *name;
const struct irq_domain_ops *ops; // 中断注册时调用该回调接口创建中断映射
void *host_data;
unsigned int flags;

/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent;
#endif

/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max; // 控制器支持的最大中断数
unsigned int revmap_direct_max_irq; // 直接映射的最大值; 使用 ~0 表示没有限制; 0 表示没有直接映射
unsigned int revmap_size; // 线性映射的大小; 0 仅用于基数映射
struct radix_tree_root revmap_tree; // 基数树的根
unsigned int linear_revmap[]; // 线性映射的 table
};

2、irq_chip

    中断控制器描述符,用来描述中断控制器的相关操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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); // 中断禁止

void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data); // 中断屏蔽
void (*irq_mask_ack)(struct irq_data *data);
......

void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);

unsigned long flags;
};

2、irq_chip

     virq 中断描述符,每一个 virq 都有一个关联的 irq_desc 用来保存该 virq 相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; // 真正的中断处理函数接口,也就是底半部分中断的回调接,这是一个链表,同一个中断允许注册多个 action
......
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

3、irq_data

    用来保存中断信息

1
2
3
4
5
6
7
8
9
10
struct irq_data {
u32 mask;
unsigned int irq; // 虚拟中断号 virq
unsigned long hwirq; // 中断控制器的 hwirq
...
struct irq_chip *chip; // 所属的中断控制器的操作函数
struct irq_domain *domain; // 所属的中断域
...
void *chip_data; // 私有数据
};

4、irqaction

    中断操作函数描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct irqaction {
irq_handler_t handler; // 最常见的中断处理函数,调用 irq_request 设置的回调函数。
void *dev_id;
void __percpu *percpu_dev_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;
} ____cacheline_internodealigned_in_smp;

二、linux 中断架构

    linux 内核的中断架构采用级联的方式,一般情况如下所示。

linux中断硬件框架

     采用这种级联的方式可以很大程度的节省硬件资源,而 arm cpu 就采用了 irq/fiq 两根物理信号线来连接。上图紫色的部分在 ARM 中称为 GIC (Generic Interrupt Controller),到目前已经更新到 v4 版本了。GIC v3/v4 用于 ARMv8 架构,即 64 位 ARM 芯片。而 GIC v2 用于 ARMv7 和其他更低的架构。

     linux 对中断的处理有两个原则,中断不能嵌套,即在 linux 中没有优先级中断的处理要越快越好,因此中断的处理就被分为上半部和下半部,对于中断的下半部的实现方式有三种,软中断、工作队列、中断线程化。

linux 中断处理

三、arm 异常处理

    中断属于异常的一种,arm 具有七种异常

异常 含义 arm工作模式 偏移地址
rest 复位异常 svc模式 0x00
swi 软中断异常 svc模式 0x04
undefine 未定义指令异常 und模式 0x08
prefetch abort 取指异常源 abort模式 0x0c
data abort 数据异常 abort模式 0x10
reserved reserved reserved 0x14
irq 中断异常 IRQ模式 0x18
fiq 快中断异常 FIQ模式 0x1c

    这七种异常构建的表格就叫做异常向量表,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//arch/arm/kernel/entry-armv.S
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq

// arch/arm/kernel/vmlinux.lds.S
__vectors_start = .;
.vectors 0xffff0000 : AT(__vectors_start) {
*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;

    由上述代码也可以知道异常向量表被放在了 .vectors 这个段,也就是 __vectors_start 到 __vectors_end 这段内存空间。arm 支持两个固定的位置存放这个表,分别为 0x00000000 和 0xFFFF0000。在操作系统中一般使用第二个地址。因此这个向量表在运行的时候需要被放入到 0xFFFF0000 这个地址,在 linux 中一般情况是不会直接使用物理地址。因此 0xFFFF0000 在内核中只是一个虚拟地址,整个向量表的构建流程如下。

1
2
3
4
5
6
7
8
9
10
// arch/arm/kernel/head.S
bl __create_page_tables // 创建页表
ldr r13, =__mmap_switched // 当使能 mmu 之后将跳转到 __mmap_switched
1: b __enable_mmu // 使能 mmu

// arch/arm/kernel/head-common.S
__mmap_switched:
--> start_kernel
--> paging_init
--> devicemaps_init 最终调用到这个函数

    首先创建页表,在使能 mmu 之后跳转到 __mmap_switched,在经过上述调用链,最终调用到 devicemaps_init 这个函数。

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
static void __init devicemaps_init(const struct machine_desc *mdesc)
{
struct map_desc map;
unsigned long addr;
void *vectors;

/*
* Allocate the vector page early.
*/
vectors = early_alloc(PAGE_SIZE * 2); // 分配 8k 内存

early_trap_init(vectors); // 将异常向量表复制到前面申请的 8k 内存

......

// 将虚拟地址 0xffff0000 映射到前面的 8k 内存空间
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
map.type = MT_HIGH_VECTORS;
#else
map.type = MT_LOW_VECTORS;
#endif
create_mapping(&map);

if (!vectors_high()) {
map.virtual = 0;
map.length = PAGE_SIZE * 2;
map.type = MT_LOW_VECTORS;
create_mapping(&map);
}

/* Now create a kernel read-only mapping */
map.pfn += 1;
map.virtual = 0xffff0000 + PAGE_SIZE;
map.length = PAGE_SIZE;
map.type = MT_LOW_VECTORS;
create_mapping(&map);

......
}

    具体的复制过程也不复杂就是将 __vectors_start 到 __vectors_end 的段内数据,即异常向量表,复制到 vectors 也就是前面分配的 8k 物理空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7M

......

// 将 __vectors_start 所在段数据复制到 vectors
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

kuser_init(vectors_base);
#endif
}

整个流程总结如下

  • 在内核中申请一片 8k 物理内存空间
  • 将向量表从 .vectors 这个段搬到这个 8k 物理内存空间
  • 将虚拟地址 0xffff0000 映射到前面的 8k 物理内存空间

    至此为止异常向量的构建也就完成了,当 cpu 触发异常,就会跳转到这个地址,并根据不同的异常执行不同偏移地址的代码,如果是中断产生的异常则会执行 0xFFFF0018 处的代码。即 vector_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
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
//arch/arm/kernel/entry-armv.S
macro vector_stub, name, mode, correction=0
.align 5
vector_\name: // 将 name 替换为 irq 就是 vector_irq, 因此跳转到这里执行
.if \correction
sub lr, lr, #\correction
.endif

@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr
mrs lr, spsr // 读取 spsr
str lr, [sp, #8] @ save spsr

@
@ Prepare for SVC32 mode. IRQs remain disabled.
@
mrs r0, cpsr
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0

@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f
THUMB( adr r0, 1f )
THUMB( ldr lr, [r0, lr, lsl #2] )
mov r0, sp
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_\name)

.......

vector_stub irq, IRQ_MODE, 4

.long __irq_usr @ 0 (USR_26 / USR_32)
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f

    当发生中断会根据中断时所处的模式调用不同的代码,如果是用户态则调用 __irq_usr 内核态调用 __irq_svc ,整个流程如下图所示。

linux 中断异常处理处理流程

    可以看出无论什么情况下都会调用 handle_arch_irq 这个函数,这个函数由连接到该 cpu 的中断控制器提供,对于 arm 架构他由 gic 提供。

四、gic 通用中断控制器

1、基本架构

    ARM 的 cpu 使用 gic 来进行管理,gicv2 最多能支持 8 core ,架构如下

gic v2 架构

     可以看出 gicv2 的组件主要由三部分分组成:

  • Distributor 采集所有中断源,对全局中断的使能控制、启用或禁止每个中断、设置每个中断的优先级、设置每个中断的目标处理器列表、设置中断的触发模式为电平(level-sensitive)或跳边沿(edge-triggered)触发、将每个中断设置为 Group 0 或 Group 1、将 SGI 转发到一个或多个目标处理器、每个中断的可见性以及通过软件设置或清除外设中断挂起状态的机制。
  • CPU interfaces 使能和发送一个具体的中断信号到特定对应的 CPU 上、确认具体中断已经被 CPU 接受,处理,以及处理完成、设置 cpu 能接受的中断的优先级别,以及对应的基于级别的中断抢断等处理。
  • virtual interface control 将 GICD 发送的虚拟中断信息,通过VIRQ,VFIQ 管脚,传输给 core

    从上图也可以看出,它将中断分为三类:

  • SGI(Software Generated Interrupt) , 软件产生的中断,中断号范围 0 - 15,也就是最前的 16 个中断,它被用来进行核间通信。
  • PPI(Private Peripheral Interrupt) , 私有物理中断,中断号范围 16 - 31,被 cpu 独占的中断,产生的中断直接发给对应的 cpu。
  • SPI(Shared peripheral Interrupt) , 这是常见的外部设备中断,硬件中断号范围 32 - 1019,也称为共享中断,比如按键触发一个中断,手机触摸屏触发的中断,共享的意思是说可以多个 cpu 或者说 core 处理,不限定特定的 cpu。

2、gic 中断触发流程

    中断信号先到 Distributor, 然后根据设定目标 CPU,送到对应的 CPU Interface 上,在这里仲裁是否优先级足够高,是否可以抢断或者打断当前的终端处理等,如果可以,那么 CPU Interface 就发送一个物理的信号到 CPU 的 IRQ/FIQ 接线上,CPU 感知到中断信号触发 cpu 的异常,CPU 进入异常处理模式处理对应的中断。区别在于 SGI 的触发是处理器向 Distributor 中的 GICD_SGIR 寄存器写值来发出 SGI。而 SPI 和 PPI 则是由连接到 GIC 上具体的物理信号信号线发出的电平(跳边沿) 触发。

3、git 中断控制器初始化

    由前面可以知道 cpu 触发异常之后最终会调用 handle_arch_irq 这个函数,这个函数由 gic 提供,内核 gic 节点的 dts 代码如下。

1
2
3
4
5
6
7
8
interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <0x3>;
interrupt-controller;
reg = <0xa01000 0x1000 0xa02000 0x100>;
linux,phandle = <0x1e>;
phandle = <0x1e>;
};

    在内核中搜索 arm,cortex-a7-gic 得到如下结果。

1
2
3
4
5
6
7
8
9
10
11
12
// drivers/irqchip/irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);

IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init); // 就是这个

IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init);
IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);

    这些宏怎么和 dts 进行匹配呢,匹配方式如下所示。

1) IRQCHIP_DECLARE 宏分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// include/linux/irqchip.h
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)

// include/linux/of.h
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)

#if defined(CONFIG_OF) && !defined(MODULE)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
#else
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__attribute__((unused)) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
#endif

    IRQCHIP_DECLARE 的宏展开将得到一个 of_device_id 结构,将前面的宏 IRQCHIP_DECLARE(cortex_a7_gic, “arm,cortex-a7-gic”, gic_of_init); 展开得到下面的结构体。

1
2
3
4
5
6
static const struct of_device_id __of_table_cortex_a7_gic      
__used __section(__irqchip_of_table) // 结构被放到了 __irqchip_of_table 这个段
= {
.compatible = "arm,cortex-a7-gic",
.data = gic_of_init
}

    由结构体 __of_table_cortex_a7_gic 也可以知道这些结构被放到了 __irqchip_of_table 这个段,在内核中搜索这个段标志,最后在 irqchip_init 中找到。

1
2
3
4
5
6
7
8
9
10
11
// drivers/irqchip/irqchip.c
static const struct of_device_id
irqchip_of_match_end __used __section(__irqchip_of_table_end);

extern struct of_device_id __irqchip_of_table[];

void __init irqchip_init(void)
{
of_irq_init(__irqchip_of_table);
acpi_probe_device_table(irqchip);
}

    我们反过来跟踪一下代码,发现代码的调用流程如下。

1
2
3
4
start_kernel (init)
--> init_IRQ
--> irqchip_init (drivers/irqchip/irqchip.c)
--> of_irq_init (drivers/of/irq.c)

     来看看 of_irq_init 究竟干了什么。

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
// drivers/of/irq.c 代码删减部分无关内容
void __init of_irq_init(const struct of_device_id *matches)
{
const struct of_device_id *match;
struct device_node *np, *parent = NULL;
struct of_intc_desc *desc, *temp_desc;
struct list_head intc_desc_list, intc_parent_list;

INIT_LIST_HEAD(&intc_desc_list);
INIT_LIST_HEAD(&intc_parent_list);

// 循环遍历 dts 中 interrupt-controller 节点
// 在 matches(也就是 __irqchip_of_table 这个段中) 找到和 interrupt-controller 节点中名字匹配的 of_device_id 结构
for_each_matching_node_and_match(np, matches, &match) {
if (!of_find_property(np, "interrupt-controller", NULL) ||
!of_device_is_available(np))
continue;

if (WARN(!match->data, "of_irq_init: no init function for %s\n",
match->compatible))
continue;

// 创建一个 desc
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
if (WARN_ON(!desc)) {
of_node_put(np);
goto err;
}

// 初始化 desc->irq_init_cb = match->data ,也就是 desc->irq_init_cb = gic_of_init
desc->irq_init_cb = match->data;
desc->dev = of_node_get(np);
desc->interrupt_parent = of_irq_find_parent(np); // 设置其父中断控制器
if (desc->interrupt_parent == np)
desc->interrupt_parent = NULL;
list_add_tail(&desc->list, &intc_desc_list); // 将该中断描述符连接到 intc_desc_list 链表
}

while (!list_empty(&intc_desc_list)) { // 找到顶层中断控制器,这里就是 gic 中断控制器,调用 desc->irq_init_cb

list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
int ret;

if (desc->interrupt_parent != parent)
continue;
ret = desc->irq_init_cb(desc->dev, desc->interrupt_parent);
}
}

    该函数主要做了下面的事情

  • 在 __irqchip_of_table 这个段中找到和设备树中 interrupt-controller 节点中名字匹配的 of_device_id 结构。
  • 并用匹配成功的 of_device_id 初始化动态创建的 of_intc_desc 并将挂接到 intc_desc_list
  • 调用回调函数 desc->irq_init_cb 也就是前面注册的 gic_of_init

2) 初始化代码分析

    由前面的分析可以得到,最终调用 gic_of_init 来对 gic 中断控制器来初始化。

2.1) gic_of_init

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
// 映射 gic 的寄存器地址
// 做一些硬件相关初始化
// 调用 __gic_init_bases 进一步初始化
gic_of_init(struct device_node *node, struct device_node *parent)
{
struct gic_chip_data *gic;
int irq, ret;

if (WARN_ON(!node))
return -ENODEV;

if (WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))
return -EINVAL;

gic = &gic_data[gic_cnt];

// 映射 gic 的寄存器地址
ret = gic_of_setup(gic, node);
if (ret)
return ret;

/*
* Disable split EOI/Deactivate if either HYP is not available
* or the CPU interface is too small.
*/
if (gic_cnt == 0 && !gic_check_eoimode(node, &gic->raw_cpu_base))
static_key_slow_dec(&supports_deactivate);

ret = __gic_init_bases(gic, -1, &node->fwnode);
if (ret) {
gic_teardown(gic);
return ret;
}

if (!gic_cnt) {
gic_init_physaddr(node);
gic_of_setup_kvm_info(node);
}

if (parent) {
irq = irq_of_parse_and_map(node, 0);
gic_cascade_irq(gic_cnt, irq);
}

if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(&node->fwnode, gic_data[gic_cnt].domain);

gic_cnt++;
return 0;
}

2.2) __gic_init_bases

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
// 设置回调函数 handle_arch_irq = gic_handle_irq
// 调用 gic_init_bases 进一步初始化
static int __init __gic_init_bases(struct gic_chip_data *gic,
int irq_start,
struct fwnode_handle *handle)
{
char *name;
int i, ret;

if (WARN_ON(!gic || gic->domain))
return -EINVAL;

if (gic == &gic_data[0]) {
/*
* Initialize the CPU interface map to all CPUs.
* It will be refined as each CPU probes its ID.
* This is only necessary for the primary GIC.
*/
for (i = 0; i < NR_GIC_CPU_IF; i++)
gic_cpu_map[i] = 0xff;
#ifdef CONFIG_SMP
set_smp_cross_call(gic_raise_softirq);
#endif
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
"AP_IRQ_GIC_STARTING",
gic_starting_cpu, NULL);

// 设置 handle_arch_irq = gic_handle_irq;
set_handle_irq(gic_handle_irq);

if (static_key_true(&supports_deactivate))
pr_info("GIC: Using split EOI/Deactivate mode\n");
}

// gic 中断控制器相关寄存器初始化
if (static_key_true(&supports_deactivate) && gic == &gic_data[0]) {
name = kasprintf(GFP_KERNEL, "GICv2");
gic_init_chip(gic, NULL, name, true);
} else {
name = kasprintf(GFP_KERNEL, "GIC-%d", (int)(gic-&gic_data[0]));
gic_init_chip(gic, NULL, name, false);
}
// 进一步初始化
ret = gic_init_bases(gic, irq_start, handle);
if (ret)
kfree(name);

return ret;
}

2.3) gic_init_bases

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
static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {
.translate = gic_irq_domain_translate,
.alloc = gic_irq_domain_alloc,
.free = irq_domain_free_irqs_top,
};

// 如果是采用 dts 则使用线性映射
// 为中断控制器创建一个中断域,使用线性映射方式进行初始化
// 设置中断域映射的回调函数 gic_irq_domain_hierarchy_ops
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,
struct fwnode_handle *handle)
{
irq_hw_number_t hwirq_base;
int gic_irqs, irq_base, ret;


if (handle) { /* DT/ACPI */
// 如果是采用 dts 则使用线性映射
// 为中断控制器创建一个中断域,使用线性映射方式进行初始化
gic->domain = irq_domain_create_linear(handle, gic_irqs,
&gic_irq_domain_hierarchy_ops,
gic);
} else { // 非 dts 方式,一次性就将所有的中断进行映射
/*
* For primary GICs, skip over SGIs.
* For secondary GICs, skip over PPIs, too.
*/
if (gic == &gic_data[0] && (irq_start & 31) > 0) {
hwirq_base = 16;
if (irq_start != -1)
irq_start = (irq_start & ~31) + 16;
} else {
hwirq_base = 32;
}

gic_irqs -= hwirq_base; /* calculate # of irqs to allocate */

//对该中断控制器上的所有 hwirq 创建一个 irq_descs
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,
numa_node_id());
if (irq_base < 0) {
WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
irq_start);
irq_base = irq_start;
}
//创建中断域并且调用 gic_irq_domain_ops 这个回调函数建立 hwirq 与 virq 之间的联系。
gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base,
hwirq_base, &gic_irq_domain_ops, gic);
}

if (WARN_ON(!gic->domain)) {
ret = -ENODEV;
goto error;
}

gic_dist_init(gic);
ret = gic_cpu_init(gic);
if (ret)
goto error;

ret = gic_pm_init(gic);
if (ret)
goto error;

return 0;

error:
if (IS_ENABLED(CONFIG_GIC_NON_BANKED) && gic->percpu_offset) {
free_percpu(gic->dist_base.percpu_base);
free_percpu(gic->cpu_base.percpu_base);
}

return ret;
}

2.4) 总结

    gic 的初始化做了下面的工作

  • 映射 gic 的寄存器地址,对 gic 的寄存器做一些初始化设置
  • 设置异常处理回调函数 handle_arch_irq = gic_handle_irq
  • 为中断控制器创建一个中断域,使用线性映射方式进行初始化
  • 设置中断域映射方式的回调函数 gic_irq_domain_hierarchy_ops

五、硬件中断的注册

    我们发现采用 dts 的方式,gic 中断控制器的初始化中仅仅只是创建了一个线性中断域,并且设置了 gic_irq_domain_hierarchy_ops 这个回调接口,整个过程都没有创建 hwirq 与 virq 的关联。从上面的代码分析我们也可以知道对于老式的不使用 dts 的时候,内核会在初始化 gic 的时候就为每一个 hwirq 创建一个 irq_descs 并且建立 hwirq 与 virq 的关联。

    那采用 dts 方式时中断控制器中的硬件中断在什么时候被注册的呢?答案是在设备模型创建设备的时候。对于使用到该中断控制器的中断,则会调用其所属的中断域中提供 irq_domain_ops 回调函数来创建。这样设计是很合理的,对于用到的中断我们才会去注册,能够提内存的使用效率。举个栗子说明一下,在 dts 中做如下配置

1
2
3
4
5
6
7
8
9
gpc: gpc@020dc000 {
compatible = "fsl,imx6ul-gpc", "fsl,imx6q-gpc";
reg = <0x020dc000 0x4000>;
interrupt-controller;
#interrupt-cells = <3>;
interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>; // 使用 89 号 SPI 中断
interrupt-parent = <&intc>; //父节点为 gic 中断控制器
fsl,mf-mix-wakeup-irq = <0xfc00000 0x7d00 0x0 0x1400640>;
};

    上述节点会在解析的时候创建出一个 platform 设备,具体流程如下

1
2
3
4
5
6
7
8
9
10
11
of_platform_default_populate_init
--> of_platform_default_populate
--> of_platform_populate
--> of_platform_bus_create
--> of_platform_device_create_pdata
--> of_platform_device_create_pdata
--> of_device_alloc
--> of_irq_count // 获取设备中断数量
--> of_irq_to_resource_table
--> of_irq_to_resource // 对每一个中断调用 of_irq_to_resource
--> irq_of_parse_and_map

    最终调用到了 irq_of_parse_and_map 这个函数。关于 dts 解析为 platform 设备的流程可以看我设备模型的文章。

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

// 解析设备节点,获取我们在 dts 中配置的中断信息
// struct of_phandle_args {
// struct device_node *np;
// int args_count; // 每一个中断 cell 的大小
// uint32_t args[MAX_PHANDLE_ARGS]; // args[0] = GIC_SPI, args[1] = 89, args[2] = IRQ_TYPE_LEVEL_HIGH
// };

if (of_irq_parse_one(dev, index, &oirq))
return 0;
// 为这个中断创建映射
return irq_create_of_mapping(&oirq);
}

    这个函数首先获取 dts 中的中断信息,再调用 irq_create_of_mapping 创建中断映射。

1
2
3
4
5
6
7
8
unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data)
{
struct irq_fwspec fwspec;
// 这个函数就是将 irq_data 中的信息转存到 fwspec
of_phandle_args_to_fwspec(irq_data, &fwspec);

return irq_create_fwspec_mapping(&fwspec);
}

    该函数将中断信息转存到 fwspec 之后调用 irq_create_fwspec_mapping 函数。由于涉及的函数实在太多,我这里就不把每个函数都写出来了,直接给出调用链。感兴趣可以对着这个调用链跟一下源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
irq_create_fwspec_mapping 
--> irq_find_matching_fwspec // 获取该中断的中断域
--> irq_domain->ops->translate // 如果中断域设置了则调用这个函数更新 hwirq, gic 设置的这个
--> irq_domain->ops->xlate // 如果中断域设置了则调用这个
if(domain->flags & IRQ_DOMAIN_FLAG_HIERARCHY)
--> irq_domain_alloc_irqs // 如果没设置 domain->ops->alloc 直接返回
--> irq_domain_alloc_descs // 动态申请一个 irq_desc 返回对应的 virq
--> irq_domain_alloc_irq_data // 初始化 irq_data
--> domain->ops->alloc // 调用中断域的 alloc 回调函数
--> gic_irq_domain_alloc // gic v2 设置的回调函数是这个
--> gic_irq_domain_translate // 更新 hwirq
// 建立 virq 与 hwirq 的关联
// 初始化 irq_desc ,对 hwirq < 32, irq_desc->handle_irq = handle_percpu_devid_irq,
// 对于 hwirq >= 32 则 irq_desc->handle_irq = handle_fasteoi_irq
--> gic_irq_domain_map
else
--> irq_create_mapping
--> irq_domain_alloc_descs // 动态申请一个 irq_desc 返回对应的 virq
--> irq_domain_associate // 建立 virq 与 hwirq 的关联
if(domain->ops->map)
--> domain->ops->map // 就是 gic_irq_domain_map 也就是前面分支的最后一个函数
else
--> 使用 linear_revmap 或 radix_tree 建立映射关系。

结论:

  • 在开机的时候设备模型就自动的帮我们将 dts 中配置的中断 hwirq 创建了一个 irq_desc 以及相关联的 virq。
  • 对于 gic 中断控制器对 irq_desc->handle_irq 设置两个默认值,记住这两个默认回调函数,后面会用到,如下所示
1
2
hwirq <  32: irq_desc->handle_irq = handle_percpu_devid_irq
hwirq >= 32: irq_desc->handle_irq = handle_fasteoi_irq

六、gic 中断处理流程

    有了前面的知识,就可以知道 gic 的中断处理流程,就比如当前面注册的 gic 上的 89 号中断触发时的流程:

  • gic 中断控制器发出 irq 信号给 cpu
  • cpu 接受到 irq 信号,进入异常处理模式,最终调用 handle_arch_irq
  • handle_arch_irq 也就是 gic 设置的 gic_handle_irq 其调用链如下
1
2
3
4
5
6
7
gic_handle_irq
--> handle_domain_irq
--> __handle_domain_irq
--> irq_find_mapping
--> generic_handle_irq
--> generic_handle_irq_desc
--> desc->handle_irq(desc);

最终会调用到对应 virq 的 desc->handle_irq(desc),这里有有两种情况。

1)默认中断处理流程

    由前面的分析可以知道 desc->handle_irq 默认情况下会调用函数 handle_fasteoi_irq ,他的调用链如下

1
2
3
4
handle_irq_event
--> handle_irq_event_percpu
--> __handle_irq_event_percpu
--> action->handler
  • 在默认情况下就会调用到该 virq 对应的 irq_desc 上的 action->handler , 这是什么这不就是我们 request_irq 注册的中断处理函数么,也就是我们常常说的中断顶半部分。

2) 调用下一个中断控制器的回调函数

    由前面可以知道中断 linux 是可以级联的,而实现级联的关键点也在这个地方。对于下一级中断控制器可以覆盖掉 gic 默认设置的 desc->handle_irq(desc) ,而注册自己的中断控制器的 handler 函数,当发生中断时就会调用到该处理函数。以 imx6ul 为例。dts 部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>; // 中断控制器寄存器的物理地址
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, // 使用 gic 的 66 号 spi 中断
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller; // 声明这是一个 gpio 控制器
#gpio-cells = <2>; // 声明这个 gpio 控制器上的 gpio 由两个 cell 描述
interrupt-controller; // 声明这是一个中断控制器
#interrupt-cells = <2>; // 对于这个中断控制器上的中断,由两个 cell 描述
};

gpio2: gpio@020a0000 {
...
};

gpio3: gpio@020a4000 {
...
}

在内核中搜索对应的 compatible 最后找到 “fsl,imx35-gpio” 相关的 probe 函数如下

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
static int mxc_gpio_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct mxc_gpio_port *port;
struct resource *iores;
int irq_base = 0;
int err;

mxc_gpio_get_hw(pdev);

port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;

// 地址映射
iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
port->base = devm_ioremap_resource(&pdev->dev, iores);
if (IS_ERR(port->base))
return PTR_ERR(port->base);

port->irq_high = platform_get_irq(pdev, 1);
port->irq = platform_get_irq(pdev, 0); // 获取使用的中断号也就是 dis 中配置的 66 号中断对应的 virq
if (port->irq < 0)
return port->irq;

...... // 省略一些中断控制器硬件相关的操作

if (mxc_gpio_hwtype == IMX21_GPIO)
// 重新设置 port->irq 的 desc->handle_irq 函数,
// 这里就是设置本中断控制器的回调函数
// 当 66 中断产生时调用 mx2_gpio_irq_handler
irq_set_chained_handler(port->irq, mx2_gpio_irq_handler);
} else {
// 这个函数和 irq_set_chained_handler 是一样的,
// 只是多设置了私有数据 desc->irq_common_data.handler_data = data;
irq_set_chained_handler_and_data(port->irq,
mx3_gpio_irq_handler, port);
if (port->irq_high > 0)
/* setup handler for GPIO 16 to 31 */
// 对于 gpio 上的 16 to 31 号中断,使用 gic 的 67 号 spi 中断
irq_set_chained_handler_and_data(port->irq_high,
mx3_gpio_irq_handler,
port);
}

err = bgpio_init(&port->gc, &pdev->dev, 4,
port->base + GPIO_PSR,
port->base + GPIO_DR, NULL,
port->base + GPIO_GDIR, NULL,
BGPIOF_READ_OUTPUT_REG_SET);
if (err)
goto out_bgio;

if (of_property_read_bool(np, "gpio_ranges"))
port->gpio_ranges = true;
else
port->gpio_ranges = false;

//设置该中断控制器的操作函数
port->gc.request = mxc_gpio_request;
port->gc.free = mxc_gpio_free;
port->gc.parent = &pdev->dev;
port->gc.to_irq = mxc_gpio_to_irq;
port->gc.base = (pdev->id < 0) ? of_alias_get_id(np, "gpio") * 32 :
pdev->id * 32;
//注册该中断控制器
err = devm_gpiochip_add_data(&pdev->dev, &port->gc, port);
if (err)
goto out_bgio;

// 为该中断控制器上的 32 个 gpio 动态创建 irq_desc 并返回其 virq
irq_base = irq_alloc_descs(-1, 0, 32, numa_node_id());
if (irq_base < 0) {
err = irq_base;
goto out_bgio;
}

// 建立该中断控制的中断域
port->domain = irq_domain_add_legacy(np, 32, irq_base, 0,
&irq_domain_simple_ops, NULL);
if (!port->domain) {
err = -ENODEV;
goto out_irqdesc_free;
}

// 进一步设置中断控制器的操作函数
// 为该 gpio 中断控制器每一个 virq 设置desc->handle_irq
/* gpio-mxc can be a generic irq chip */
err = mxc_gpio_init_gc(port, irq_base, &pdev->dev);
if (err < 0)
goto out_irqdomain_remove;

list_add_tail(&port->node, &mxc_gpio_ports);

platform_set_drvdata(pdev, port);
pm_runtime_put(&pdev->dev);

return 0;

out_pm_dis:
pm_runtime_disable(&pdev->dev);
clk_disable_unprepare(port->clk);
out_irqdomain_remove:
irq_domain_remove(port->domain);
out_irqdesc_free:
irq_free_descs(irq_base, 32);
out_bgio:
dev_info(&pdev->dev, "%s failed with errno %d\n", __func__, err);
return err;
}

    对于 gpio 中断控制器,在初始化时做了下面操做

  • 重新注册使用到的 gic 的中断号对应的 desc->handle_irq
  • 设置该中断控制器的操作函数并将其注册进内核
  • 这里采用了老式的方式创建映射,调用 irq_domain_add_legacy 直接对 gpio 控制器上的每一个 gpio 创建 irq_desc 以及中断域映射。这里和前面的的 gic 采用的时两种方式,前面的 gic 是在设备模型创建的时候动态创建映射。
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
static int mxc_gpio_init_gc(struct mxc_gpio_port *port, int irq_base,
struct device *dev)
{
struct irq_chip_generic *gc;
struct irq_chip_type *ct;

// 动态创建 irq_chip_generic 并设置 irq_chip_generic->chip_types->handler 的回调函数 handle_level_irq
gc = irq_alloc_generic_chip("gpio-mxc", 1, irq_base,
port->base, handle_level_irq);
if (!gc)
return -ENOMEM;
gc->private = port;

// 进一步设置中断控制器的操作函数
ct = gc->chip_types;
ct->chip.parent_device = dev;
ct->chip.irq_ack = irq_gc_ack_set_bit;
ct->chip.irq_mask = irq_gc_mask_clr_bit;
ct->chip.irq_unmask = irq_gc_mask_set_bit;
ct->chip.irq_set_type = gpio_set_irq_type;
ct->chip.irq_set_wake = gpio_set_wake_irq;
ct->chip.irq_request_resources = mxc_gpio_irq_reqres;
ct->chip.irq_release_resources = mxc_gpio_irq_relres,
ct->chip.flags = IRQCHIP_MASK_ON_SUSPEND;
ct->regs.ack = GPIO_ISR;
ct->regs.mask = GPIO_IMR;

// 为该 gpio 中断控制器每一个 virq 设置 desc->handle_irq
irq_setup_generic_chip(gc, IRQ_MSK(32), IRQ_GC_INIT_NESTED_LOCK,
IRQ_NOREQUEST, 0);

return 0;
}

void irq_setup_generic_chip(struct irq_chip_generic *gc, u32 msk,
enum irq_gc_flags flags, unsigned int clr,
unsigned int set)
{
......
for (i = gc->irq_base; msk; msk >>= 1, i++) {
if (!(msk & 0x01))
continue;
......
irq_set_chip_and_handler(i, chip, ct->handler); // 设置回调函数为 ct->handler
irq_set_chip_data(i, gc);
irq_modify_status(i, clr, set);
}
gc->irq_cnt = i - gc->irq_base;
}
EXPORT_SYMBOL_GPL(irq_setup_generic_chip);

    这里需要了解的就是 irq_setup_generic_chip 为该 gpio 中断控制器每一个 virq 设置 d esc->handle_irq 为对应的 irq_chip_generic->chip_types->handler 也就是上面的 handle_level_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
void handle_level_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc);

if (!irq_may_run(desc))
goto out_unlock;

desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

/*
* If its disabled or no action available
* keep it masked and get out of here
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out_unlock;
}

kstat_incr_irqs_this_cpu(desc);
handle_irq_event(desc); //调用 handle_irq_event

cond_unmask_irq(desc);

out_unlock:
raw_spin_unlock(&desc->lock);
}

    这个函数最终调用到 handle_irq_event 来处理中断,这个函数在前面已经出现过了,也就是最终调用到对应的 action->handler 来处理中断函数,对于gpio中断的调用流程总结如下

1
2
3
4
5
6
7
8
9
10
11
12
13
mx3_gpio_irq_handler 
// 从 gpio 中断控制器的寄存器中获取到对应的硬件中断号
// 并通过中断域获取到对应的 irq_desc
--> mxc_gpio_irq_handler
--> generic_handle_irq
--> generic_handle_irq_desc
--> desc->handle_irq // 调用到 gpio 中断控制器 hwir 对应的 virq 的 handle_irq
--> handle_level_irq
--> handle_irq_event
--> handle_irq_event
--> handle_irq_event_percpu
--> __handle_irq_event_percpu
--> action->handler

七、mix6ull gpio 中断调用流程

    最后对于 mix6ull 的 gpio 中断调用做一个整体的梳理

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
`````````````````````````````````````````` arm 异常处理流程
cpu 触发异常跳转到 0xFFFF0018
--> __irq_uer / _irq_scv // 根据不同的状态进行跳转
--> irq_hander
--> handler_arch_irq // 该函数由 gic 中断控制器初始化时设置为 gic_handle_irq
------------------------------------------ gic 中断控制器处理流程
gic_handle_irq
--> handle_domain_irq
--> __handle_domain_irq
--> irq_find_mapping
--> generic_handle_irq
--> generic_handle_irq_desc
--> desc->handle_irq(desc); // 该函数由接在 gic 上的 gpio 中断控制器初始化为 mx3_gpio_irq_handler
------------------------------------------- gpio 中断控制器处理流程
mx3_gpio_irq_handler
// 从 gpio 中断控制器的寄存器中获取到对应的硬件中断号
// 并通过中断域获取到对应的 irq_desc
--> mxc_gpio_irq_handler
--> generic_handle_irq
--> generic_handle_irq_desc
--> desc->handle_irq // 调用到 gpio 中断控制器 hwir 对应的 virq 的 handle_irq
--> handle_level_irq
--> handle_irq_event
--> handle_irq_event
--> handle_irq_event_percpu
--> __handle_irq_event_percpu
--> action->handler // 最终调用到对应的 gpio 中断控制器 hwir 对应的 virq 的 irq_desc 上的 action->handler ,也就是我们常说的顶半部分中断。

八、linux 下使用中断

    对于使用来说就相当简单了,只需要调用对应的接口就行了,使用流程如下。

  • 在 dts 中配置中断信息
  • 获取配置的中断的 virq
  • 像 virq 注册中断回调函数。
  • 编写中断处理函数

1、在 dts 中配置中断信息

    对于 dts 配置中断需要了解中断相关的 dts 描述,以下整理出 dts 描述

interrupt-parent: 用来描述该设备使用的父中断控制
#interrupt-cells: 用来描述该中断控制器的信息(interrupts)需要几个 cell 描述
interrupts: 用来描述该设备使用具体中断,也就是前面的 cell 的具体描述,具体的解释由中断控制器给出。举个栗子

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
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>; // 该中断控制器上的中断由三个 cell 描述
interrupt-controller;
reg = <0x00a01000 0x1000>, // GIC 中断控制器的物理地址
<0x00a02000 0x100>;
};

soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;

gpio1: gpio@0209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
// 第一个参数表示使用的中断类型为 SPI 中断,第二个表示中断号为 66 第三个参数表示 触发方式为高电平
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, // 使用 gic 的 66 号 spi 中断
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>; // 使用 gic 的 66 号 spi 中断
gpio-controller;
#gpio-cells = <2>;
interrupt-controller; // 表明这是一个中断控制器
#interrupt-cells = <2>;
};
}

my_dev {
compatible = "fsl,fxls8471";
interrupt-parent = <&gpio1>; //表示使用的中断控制器为 gpio1
// 第 1 个表示中断号,第 2 个参数表示触发方式
interrupts = <0 8>; // 使用 gpio 的 0 号中断,触发方式为低电平触发
};

    在内核中触发方式可以在 dts 中可以只接写对应的数字

  • 1 表示上升沿触发
  • 2 表示下降沿触发
  • 4 表示高电平触发
  • 8 表示低电平触发

    他们可以进行组合,例如双边沿触发的话直接设置 3(1+2) 就可以了。下面是 mtk 平台下的 gpio 中断控制器描述,mtk 有些平台就采用了 4 cell 个来描述中断如下所示的一个引脚配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
button: button { 
compatible = "mediatek,mt6785-key";
};

&button {

button_k2:button_k2 {
compatible = "mediatek,k2";
interrupt-parent = <&pio>; // 使用 pio 这个中断控制器
// 153 中断号
// IRQ_TYPE_LEVEL_LOW 中断触发电平
// 1 中断触发方式: 1 是边沿触发 4 是电平触发
// 0 for future 现在未使用,默认为 0
interrupts = <153 IRQ_TYPE_LEVEL_LOW 1 0>; // 中断引脚为 153 触发方式 IRQ_TYPE_LEVEL_LOW
irq-gpio = <&pio 153 0>; //用到的 gpio 为 153,这个是用来操作这个引脚用的
status = "okay";
};
};

2、获取配置的中断的 virq

2.1) platform 设备

1
2
3
// dev 设备指针
// num 获取该设备上 interrupts 描述的第 num 个中断号的 virq
int platform_get_irq(struct platform_device *dev, unsigned int num)

2.2) i2c/spi 设备

  • i2c_client->irq :用来保存该 i2c 设备的中断号对应的 virq
  • spi_device->irq :用来保存该 spi 设备的中断号对应的 virq

    他们分别在 i2c_device_probe 和 spi_drv_probe 两个函数中解析

2.3) gpio

    gpio 和中 hwirq 存在一种映射关系,一般情况下都是线性映射。因此内核为gpio 提供了一组转换函数,可以直接将 gpio 描述符转换为对应的 virq。

1
2
3
4
5
// 老式接口,gpio 表示 gpio 号
static inline int gpio_to_irq(unsigned int gpio)

// 新接口推荐使用这个接口,将 gpio 描述符转换为 virq
int gpiod_to_irq(const struct gpio_desc *desc)

2.4) 通用接口

1
2
3
// dev   设备节点,注意和前面的 dev 区分
// index 获取该设备上 interrupts 描述的第 index 个中断号的 virq
static inline int of_irq_get(struct device_node *dev, int index)

3、注册中断回调函数

    

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
// irq      要申请的中断的 virq
// handler 中断回调函数,也就是底半部分中断
// flags 中断标志位,可以设置触发方式
// name 中断名称
// dev 私有数据,将作为参数传递给 handler
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)

// 功能和前面相同,只是多了自动释放资源的功能
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)

// irq 要申请的中断的 virq
// handler 中断回调函数
// thread_fn 线程处理函数,当 handler 处理完,调用该函数处理底半部分。当 handler 传入为空时直接调用该函数
// flags 中断标志位,可以设置触发方式
// name 中断名称
// dev 私有数据,将作为参数传递给 handler
int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);

// 功能和前面相同,只是多了自动释放资源的功能
int __must_check
devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);

4、完整代码

4.1) 驱动程序

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
#include <asm/device.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>

#define BUTTON_DEBUG 0

#define BUTTON_INFO(fmt, args...) \
do {\
if (BUTTON_DEBUG) {\
pr_info(fmt, ##args); \
} \
} while (0)

#define button_NAME "button_name"

struct work_struct button_eint_work;
struct workqueue_struct *button_eint_workqueue;

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中断事件标志, 中断服务程序将它置1,button_drv_read 将它清0 */
static volatile int ev_press = 0;

struct button_cdev {

dev_t dev;
struct cdev cdev; /* 字符设备结构体 */

struct class *button_class;
struct device *button_class_dev;
};

static struct button_cdev* button_cdevp = NULL;

struct button_desc { //按键描述结构体
int irq;
unsigned long button_eint_type; //按键中断触发方式
unsigned long irq_pin;
unsigned int gpio_value;
unsigned int key_val;
char* gpio_name;
char* irq_name;
};

/* 用来指定按键所用的外部中断引脚及中断触发方式, 名字 */
static struct button_desc button_irq_desc = {
.irq = 0,
.button_eint_type = IRQ_TYPE_EDGE_BOTH, //双边沿触发
.irq_pin = 0,
.gpio_value = 0,
.key_val = 0,
.gpio_name = "irq-gpio",
.irq_name = "button",
};

/* 中断服务程序当有按键被按下时调用 */
static irqreturn_t button_eint_func(int irq,void *data)
{

BUTTON_INFO("button_eint_func\n");
/* 为了节省中断的调用时间,因此在中断中只做唤醒对列的工作 */
if (!work_pending(&button_eint_work))
queue_work(button_eint_workqueue, &button_eint_work); //唤醒队列

return IRQ_HANDLED;
}

static int button_setup_eint(void)
{
int ret;
struct device_node *node = NULL;
BUTTON_INFO("button_setup_eint\n");

node = of_find_compatible_node(NULL, NULL, "mediatek,k2"); //通过compatible属性查找指定节点
if(!node)
{
pr_err("mt6785-button node get fail!!!\n");
return -1;
}

button_irq_desc.irq = irq_of_parse_and_map(node, 0); //根据node节点,返回第0个中断的中断号

ret = request_irq(button_irq_desc.irq, button_eint_func,button_irq_desc.button_eint_type, button_irq_desc.irq_name, (void *)&(button_irq_desc));
if (ret > 0)
{
pr_err("[button]EINT IRQ LINE NOT AVAILABLE\n");
}

pr_err("[button] set EINT finished, %s:irq=%d\n", button_irq_desc.irq_name, button_irq_desc.irq);


//enable_irq(button_irq_desc.irq);

return 0;
}

/* 真正的中断处理函数 */
static void button_eint_work_callback(struct work_struct* work)
{
/* 获取当前按键的状态 */
button_irq_desc.gpio_value = __gpio_get_value(button_irq_desc.irq_pin);

BUTTON_INFO("button_value = %d\n",button_irq_desc.gpio_value);

/* 根据当前的按键状态得到对应的键值
* 按下: 返回0x00
* 松开: 返回0x81
*/
if (button_irq_desc.gpio_value)
{
/* 松开 */
button_irq_desc.key_val = 0x80 | button_irq_desc.gpio_value;
}
else
{
/* 按下 */
button_irq_desc.key_val = button_irq_desc.gpio_value;
}

ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */

}

//创建一个button工作队列,用于处理中断
static void button_setup_workqueue(void)
{
button_eint_workqueue = create_singlethread_workqueue("button_eint");
INIT_WORK(&button_eint_work, button_eint_work_callback);
}

//获取gpio相关信息
static int button_get_gpio_info(void)
{
struct device_node *node = NULL;

node = of_find_compatible_node(NULL, NULL, "mediatek,k2"); //通过compatible属性查找指定节点
if(!node)
{
pr_err("mt6785-button node get fail!!!\n");
return -1;
}

//从dts获取要操作的gpio
button_irq_desc.irq_pin = of_get_named_gpio(node, button_irq_desc.gpio_name, 0);
if(!button_irq_desc.irq_pin)
{
printk("get irq pin fail\n");
}

printk("button = %d\n", button_irq_desc.irq_pin);

return 0;
}

/* 读取按键状态 */
ssize_t button_drv_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
if (size != 1)
return -EINVAL;

/* 如果没有按键动作, 休眠 */
wait_event_interruptible(button_waitq, ev_press);

/* 如果有按键动作, 返回键值 */
copy_to_user(buf, &button_irq_desc.key_val, 1);
ev_press = 0;

return 1;
}

static int button_drv_open(struct inode *inode, struct file *filp)
{
struct button_cdev* button_cdevp;

button_cdevp = container_of(inode->i_cdev, struct button_cdev, cdev); //获取 struct button_cdev 结构

filp->private_data = button_cdevp; //初始化 private_date 结构

return 0;
}

int button_drv_close(struct inode *inode, struct file *file)
{

return 0;
}

static struct file_operations sencod_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = button_drv_open,
.read = button_drv_read,
.release = button_drv_close,
};

void button_setup_cdev(struct button_cdev* devp)
{
int err;

/*
* 1. 初始化cdev结构
* 2. 建立sencod_drv_fops与cdev的联系
*/
cdev_init(&devp->cdev,&sencod_drv_fops);
devp->cdev.owner = THIS_MODULE;

/* 向内核注册一个字符设备 */
err = cdev_add(&devp->cdev,devp->dev,1);
if(err)
{
printk(KERN_NOTICE "Error %d adding button", err);
}

}

static int button_probe(struct platform_device *dev)
{
int ret = 0;
BUTTON_INFO("button_probe\n");

/* 获取一个button_cdevp结构 */
button_cdevp = kzalloc(sizeof(struct button_cdev), GFP_KERNEL);
if (!button_cdevp)
{
return -ENOMEM;
}

//申请设备号
ret = alloc_chrdev_region(&button_cdevp->dev,0,1,"button");
if(ret < 0)
{
return ret;
}

//初始化以及注册字符设备
button_setup_cdev(button_cdevp);

//创建一个 button_drv 的类
button_cdevp->button_class = class_create(THIS_MODULE, "button_drv");

//在 /dev 目录下创建一个 buttons 设备文件节点,这个节点根据设备号建立
button_cdevp->button_class_dev = device_create(button_cdevp->button_class, NULL, button_cdevp->dev, NULL, "buttons"); /* /dev/buttons */
if(IS_ERR(button_cdevp->button_class_dev))
{
printk("Err: failed in creating class./n");
return -1;
}

//初始化中断服务函数
ret = button_setup_eint();
if(ret)
{
return ret;
}


ret = button_get_gpio_info();
if(ret)
{
return ret;
}

button_setup_workqueue();

BUTTON_INFO("button_probe end\n");
return 0;
}

//match列表,platform 驱动在注册的时候会去匹配这里面的名字,如果这里面的名字存在就调用probe函数。
static const struct of_device_id button_switch_of_match[] = {
{.compatible = "mediatek,mt6785-key"},
{},
};

//构造一个platform_driver结构用来表示按键驱动
static struct platform_driver button_driver = {
.probe = button_probe,
.driver = {
.name = "button_driver",
.owner = THIS_MODULE,
.of_match_table = button_switch_of_match,
},
};

static int button_mod_init(void)
{
int ret = 0;

BUTTON_INFO("[button]button_mod_init begin!\n");
/*
* 将button_driver这个驱动注册到bus总线
* 在bus总线上根据of_match_table匹配是否存在mediatek,mt6785-key这样的设备
* mediatek,mt6785-key 这个名字就是在mt6785.dts中配置的
* 内核会根据这个节点生成一个设备,platform 驱动在注册的时候会去匹配
* 这只是其中一种匹配方式,匹配成功就调用probe函数
*/
ret = platform_driver_register(&button_driver);
if (ret)
{
pr_err("[button]platform driver register error:(%d)\n", ret);
return ret;
}
else
{
pr_err("[button]platform driver register done!\n");
}

BUTTON_INFO("[button]button_mod_init done!\n");
return 0;

}

static void button_mod_exit(void)
{
BUTTON_INFO("[button]button_mod_exit\n");
cdev_del(&button_cdevp->cdev);
class_destroy(button_cdevp->button_class);
unregister_chrdev_region(button_cdevp->dev,1);
free_irq(button_irq_desc.irq, (void *)&(button_irq_desc));
platform_driver_unregister(&button_driver);
BUTTON_INFO("[button]button_mod_exit Done!\n");
}


module_init(button_mod_init);
module_exit(button_mod_exit);

MODULE_DESCRIPTION(" button driver");
MODULE_AUTHOR("baron-z");
MODULE_LICENSE("GPL");

4.2) dts文件

文件名 k85v1_64.dts

1
2
3
4
5
6
7
8
9
10
&button {

button_k2:button_k2 {
compatible = "mediatek,k2"; //当前节点名
interrupt-parent = <&pio>; //中断属于pio
interrupts = <153 IRQ_TYPE_LEVEL_LOW 1 0>; //中断引脚为153 触发方式 IRQ_TYPE_LEVEL_LOW 这个会在代码中重新设置设
irq-gpio = <&pio 153 0>; //用到的gpio为153,这个是用来操作这个引脚用的
status = "okay";
};
};

文件名 mt6785.dts

1
2
3
button: button {
compatible = "mediatek,mt6785-key"; //button根节点,表示生成这样一个设备
};

4.3) 验证程序

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
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

const char* device_name = "/dev/buttons";

int main(int argc, char **argv)
{
int fd;
unsigned char key_val;

fd = open(device_name, O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}

while (1)
{
read(fd, &key_val, 1); //调用读取函数,当没有按键按下时,内核里面会调用wait_event_interruptible 函数使该进程进入休眠。
printf("key_val = 0x%x\n", key_val);
}

return 0;
}

4.4) 测试过程

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
k85v1_64:/cache # insmod key_one.ko

k85v1_64:/cache # cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
......
24: 0 0 0 0 0 0 0 0 mt-eint 1 Edge TOUCH_PANEL-eint
30: 11 11 17 11 5 2 10 3 mt-eint 7 Edge type_c_port0-IRQ
31: 0 0 0 0 0 0 0 0 mt-eint 8 Edge 11240000.msdc cd
39: 29 60 57 39 29 14 12 16 mt-eint 16 Edge 5-0034
176: 0 0 0 0 0 0 0 0 mt-eint 153 Edge button
216: 2 29 14 10 5 6 6 5 mt-eint 193 Level mt6358-irq
......

k85v1_64:/cache #
k85v1_64:/cache #
k85v1_64:/cache # ./libshowlogotest &
[1] 3020
k85v1_64:/cache # key_val = 0x0
key_val = 0x81
key_val = 0x0
key_val = 0x81
key_val = 0x0
key_val = 0x81
key_val = 0x0
key_val = 0x81

    参考文献: