一.ARM中断处理流程

CPU每执行完一条指令,都会判断是否有异常发生,如果有异常发生,就跳转到相应的异常向量表的地址执行异常指令,该指令由软件设置,一般在系统内核启动的时候会设置异常向量表。

二.Linux内核对中断异常的处理

代码位置:arm/arm/kernel/entry-armv.S

Linux内核在系统启动的时候会设置异常向量表,该向量表代码如下:

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

对于中断,该指令为一条跳转指令,跳转到vector_irq地址去执行指令,在内核中vactor_irq由一个宏来展开,该宏的名称是vector_stub:

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
	.macro	vector_stub, name, mode, correction=0
.align 5
vector_\name:
.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
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)

irq入口对该宏的引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Interrupt dispatcher
*/
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

上面代码中的中断分两种模式,usr和svc。在vector_stub宏内部的代码先对cpsr进行判断得知应该执行哪种处理。

svn模式的中断处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__irq_svc:
svc_entry
irq_handler

#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif

svc_exit r5, irq = 1 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)

usr模式的中断处理代码如下:

1
2
3
4
5
6
7
8
9
__irq_usr:
usr_entry
kuser_cmpxchg_check
irq_handler
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq
UNWIND(.fnend )
ENDPROC(__irq_usr)

由上面两种模式的关键处理代码可知,它们都会保存中断现场,然后调用到irq_handler,然后恢复现场。

irq_handler也是一个宏定义,该宏的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* Interrupt handling.
*/
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
badr lr, 9997f
ldr pc, [r1]
#else
arch_irq_handler_default
#endif

由上面的代码可直,最终调用的中断处理函数为handle_arch_irq,该函数是一个C语言函数指针,总结内核对中断异常处理的汇编部分如下:

1
2
3
4
1.cpu产生中断异常,跳转到异常向量表中断异常的地址执行汇编中断处理代码
2.汇编中断处理代码保存中断现场
3.汇编中断处理代码调用C语言中断处理函数
4.汇编中断处理代码恢复中断现场

三.Linux中断控制器初始化

内核中对中断控制器的描述

在linux内核中每一个中断控制器有一个irq_domain结构体,内核维护了一个irq_domain链表。irq_domain结构体在内核中的定义如下:

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;
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;
unsigned int revmap_size;
struct radix_tree_root revmap_tree;
unsigned int linear_revmap[];
};

link:链表元素,用来将irq_domain链入全局链表。

name:该中断域的名字。

ops:irq_domain的操作方法集。该集合中有两个常用的函数指针,map指针用来在一个irq_domain上为中断号和硬件中断号建立映射关系,在map函数中一般会设置virq对应的irq_desc结构的irq_data成员以及handle_irq回调函数;xlate函数用来获取中断类型和描述。

of_node:关联到该irq_domain的设备树节点。

host_data:私有数据指针。

gc:irq_chip_generic链表。描述irq_chip。每个irq_chip_generic维护一个irq_chip_type表,用来表示一个irq_chip实例。

parent:该riq_domain的父节点。

hwirq_max:最大硬件中断号。

revmap_size:线性映射表的大小。

linear_revmap:线性反向映射表,是一个整数数组,用来记录硬件中断号和中断号的关系。该数组下标为硬件中断号,数组项为以该下标为硬件中断号对应的中断号。

内核中对中断的描述

linux内核中维护了一个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
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data qirq_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; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#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;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

其中,irq_data成员用来描述该中断关联的中断域,软硬件中断号以及irq_chip等硬件相关的信息。handle_irq成员为该中断的处理回调函数。action成员为用户注册的中断处理的链表,在action中handler函数指针即为用户申请中断提供的中断处理函数,action列表一般由irq_desc的handle_irq函数遍历以调用所有用户注册的中断处理函数。比如一个共享中断,就可能会有多个action存在。

中断控制器初始化流程

在内核start_kernel中,首先会调用setup_arch函数,该函数会解析设备树,将设备树中的所有节点转换为device_node结构体,其中包含中断控制器节点。

drivers/of/irq.c

在setup_arch之后会调用到init_IRQ()函数,在该函数中调用irqchip_init()函数初始化中断控制器。

1
2
3
4
5
6
7
extern struct of_device_id __irqchip_of_table[];

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

上面的函数通过执行of_irq_init函数对__irqchip_of_table[]中的每一项和所有的device_node进行匹配,匹配到的device_node根据有没有”interrupt-controller”属性再进行判断,得到中断控制器节点。

1
2
3
4
for_each_matching_node_and_match(np, matches, &match) {
if (!of_find_property(np, "interrupt-controller", NULL) ||
!of_device_is_available(np))
continue;

接着为中断控制器节点申请一个of_intc_desc结构,并初始化该结构,该结构表示的中断控制器的初始化函数irq_init_cb被设置为匹配到的__irqchip_of_table数组中的of_device_id结构的data参数。接着将该结构添加至局部链表intc_desc_list中。

1
2
3
4
5
6
7
8
9
10
11
12
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
if (WARN_ON(!desc)) {
of_node_put(np);
goto err;
}

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后从父中断为NULL(根中断)的中断控制器开始遍历,并调用每一个中断控制器描述符中的初始化回调函数irq_init_cb,每初始化一个中断控制器并将其从局部链表intc_desc_list删除,并添加到局部链表intc_parent_list中。

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
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
int ret;

if (desc->interrupt_parent != parent)
continue;

list_del(&desc->list);

of_node_set_flag(desc->dev, OF_POPULATED);

pr_debug("of_irq_init: init %s (%p), parent %p\n",
desc->dev->full_name,
desc->dev, desc->interrupt_parent);
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);
if (ret) {
of_node_clear_flag(desc->dev, OF_POPULATED);
kfree(desc);
continue;
}

/*
* This one is now set up; add it to the parent list so
* its children can get processed in a subsequent pass.
*/
list_add_tail(&desc->list, &intc_parent_list);
}

取出intc_parent_list中的中断控制器,以该中断控制器作为父亲中断控制器,并重复上面的操作,依次执行所有中断控制器的初始化函数完成所有中断控制器的初始化工作,将初始化完的中断控制器从局部链表intc_parent_list中删除并删除所有申请的of_intc_desc结构。

1
2
3
4
5
6
7
8
9
10
/* Get the next pending parent that might have children */
desc = list_first_entry_or_null(&intc_parent_list,
typeof(*desc), list);
if (!desc) {
pr_err("of_irq_init: children remain, but no parents\n");
break;
}
list_del(&desc->list);
parent = desc->dev;
kfree(desc);

中断控制器的驱动识别

前面说的中断控制器初始化接口中遍历所有中断控制器的device_node,并从__irqchip_of_table数组中为其匹配到一项,并将其data参数作为初始化接口执行。在这里匹配过程是根据of_device_id中的compatible属性和设备树节点device_node中的属性列表中的compatible属性进行匹配的。而该数组中的每一项元素都由驱动代码添加。

在中断控制器驱动中通过IRQCHIP_DECLARE宏向该数组中添加驱动支持的设备描述,该宏的实现如下:

1
2
3
4
5
6
7
8
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#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 }

该宏定义了一个与name相关名称的of_device_id结构并用传入的compat和data初始化该结构,它的段属性为__irqchip_of_table,内核编译的时候将这种段属性的地址放到和该段属性相同名字的地址处。

根中断控制器初始化

以omap驱动为例,omap根中断控制器驱动代码文件为drivers/irqchip/irq-omap-initc.c

在该代码中首先通过IRQCHIP_DECLARE添加多个驱动匹配的of_device_id,其中断控制器的初始化函数都为intc_of_init

1
2
3
4
5
IRQCHIP_DECLARE(omap2_intc, "ti,omap2-intc", intc_of_init);
IRQCHIP_DECLARE(omap3_intc, "ti,omap3-intc", intc_of_init);
IRQCHIP_DECLARE(dm814x_intc, "ti,dm814-intc", intc_of_init);
IRQCHIP_DECLARE(dm816x_intc, "ti,dm816-intc", intc_of_init);
IRQCHIP_DECLARE(am33xx_intc, "ti,am33xx-intc", intc_of_init);

在前面所说中断控制器初始化流程中最终会调用到intc_of_init完成根中断控制器的初始化。

在intc_of_init函数中,首先执行omap_init_irq函数为根中断控制器申请并初始化irq_domain结构。然后通过set_handle_irq函数将omap_intc_handle_irq函数设置为中断处理的C入口函数,即最开始汇编代码调用的入口函数指针handle_arch_irq。

1
2
3
4
5
ret = omap_init_irq(-1, of_node_get(node));
if (ret < 0)
return ret;

set_handle_irq(omap_intc_handle_irq);

irq_domain申请和初始化过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int __init omap_init_irq_of(struct device_node *node)
{
int ret;

omap_irq_base = of_iomap(node, 0);
if (WARN_ON(!omap_irq_base))
return -ENOMEM;

domain = irq_domain_add_linear(node, omap_nr_irqs,
&irq_generic_chip_ops, NULL);

omap_irq_soft_reset();

ret = omap_alloc_gc_of(domain, omap_irq_base);
if (ret < 0)
irq_domain_remove(domain);

return ret;
}

在omap_init_irq函数中,首先为irq映射io,接着调用irq_domain_add_liner函数申请其domain和向全局irq_domain链表中添加其domain,并且初始化了domain的ops成员以及根据irq个数为其linear_revmap成员申请空间。该中断控制器的ops成员如下:

1
2
3
4
5
struct irq_domain_ops irq_generic_chip_ops = {
.map = irq_map_generic_chip,
.unmap = irq_unmap_generic_chip,
.xlate = irq_domain_xlate_onetwocell,
};

然后通过写寄存器软复位中断控制器。

最后通过omap_alloc_gc_of函数申请并初始化了irq_chip_generic成员。并初始化了irq_chip_generic的irq_chip_type成员以及irq_chip_type成员的irq_chip成员结构,irq_chip结构中有一些列中断chip相关的函数指针,其中有清中断,中断应答,中断屏蔽,中断使能等函数指针。对irq_chip_type类型成员的初始化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
ct = gc->chip_types;

ct->type = IRQ_TYPE_LEVEL_MASK;

ct->chip.irq_ack = omap_mask_ack_irq;
ct->chip.irq_mask = irq_gc_mask_disable_reg;
ct->chip.irq_unmask = irq_gc_unmask_enable_reg;

ct->chip.flags |= IRQCHIP_SKIP_SET_WAKE;

ct->regs.enable = INTC_MIR_CLEAR0 + 32 * i;
ct->regs.disable = INTC_MIR_SET0 + 32 * i;

其中断处理函数注册为handle_level_irq,下面的代码将handle_level_irq函数设置到该中断域gc成员的ct成员的handler,在关联irq_desc的时候会调用到irq_domain的ops中的map函数,将其设置到irq_desc的handle_irq函数指针。

1
2
3
ret = irq_alloc_domain_generic_chips(d, 32, 1, "INTC",
handle_level_irq, IRQ_NOREQUEST | IRQ_NOPROBE,
IRQ_LEVEL, 0);

其他中断控制器初始化

以gpio-omap为例,在gpio-omap的probe函数中,首先申请和初始化irq_chip结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
irqc = devm_kzalloc(dev, sizeof(*irqc), GFP_KERNEL);
if (!irqc)
return -ENOMEM;

irqc->irq_startup = omap_gpio_irq_startup,
irqc->irq_shutdown = omap_gpio_irq_shutdown,
irqc->irq_ack = omap_gpio_ack_irq,
irqc->irq_mask = omap_gpio_mask_irq,
irqc->irq_unmask = omap_gpio_unmask_irq,
irqc->irq_set_type = omap_gpio_irq_type,
irqc->irq_set_wake = omap_gpio_wake_enable,
irqc->irq_bus_lock = omap_gpio_irq_bus_lock,
irqc->irq_bus_sync_unlock = gpio_irq_bus_sync_unlock,
irqc->name = dev_name(&pdev->dev);
irqc->flags = IRQCHIP_MASK_ON_SUSPEND;

gpio的资源由gpio_chip结构维护。在probe函数中执行omap_gpio_chip_init函数,在该函数中首先通过执行irq_alloc_descs为该gpio_chip的所有gpio中断申请irq_desc。

1
2
3
4
5
irq_base = irq_alloc_descs(-1, 0, bank->width, 0);
if (irq_base < 0) {
dev_err(bank->chip.parent, "Couldn't allocate IRQ numbers\n");
return -ENODEV;
}

然后通过调用gpiochip_irqchip_add函数为该gpio_chip申请和设置irq_domain结构,将之前申请和初始化的irq_chip也关联到其irq_domain。

1
2
3
ret = gpiochip_irqchip_add(&bank->chip, irqc,
irq_base, handle_bad_irq,
IRQ_TYPE_NONE);

gpiochip_irqchip_add是一个宏,最终调用的函数为_gpiochip_irqchip_add,在该函数中首先为该gpio_chip分配了irq_domain结构:

1
2
3
gpiochip->irqdomain = irq_domain_add_simple(of_node,
gpiochip->ngpio, first_irq,
&gpiochip_domain_ops, gpiochip);

gpiochip的domain->ops->map被设置为gpiochip_domain_ops:

1
2
3
4
5
6
static const struct irq_domain_ops gpiochip_domain_ops = {
.map = gpiochip_irq_map,
.unmap = gpiochip_irq_unmap,
/* Virtually all GPIO irqchips are twocell:ed */
.xlate = irq_domain_xlate_twocell,
};

接着调用循环调用irq_create_mapping函数,为该gpio_chip里的每一个gpio中断向irq_desc数组中添加项。

1
2
3
4
5
6
7
8
9
10
11
12
13
for (offset = 0; offset < gpiochip->ngpio; offset++) {
if (!gpiochip_irqchip_irq_valid(gpiochip, offset))
continue;
irq_base = irq_create_mapping(gpiochip->irqdomain, offset);
if (!irq_base_set) {
/*
* Store the base into the gpiochip to be used when
* unmapping the irqs.
*/
gpiochip->irq_base = irq_base;
irq_base_set = true;
}
}

irq_create_mapping函数中向irq_desc中添加项的代码如下,该代码路径为kernel/irq/irqdomain.c

首先在该gpio_chip的domain中检查该gpio的虚拟中断号是否注册,没有的话向irq_desc中添加该项并得到该项的下标,即虚拟终端号,然后执行irq_domain_associate函数将该虚拟号和硬件中断号的绑定关系存放在该gpio_chip的domain中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Check if mapping already exists */
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("-> existing mapping on virq %d\n", virq);
return virq;
}

/* Allocate a virtual interrupt number */
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL);
if (virq <= 0) {
pr_debug("-> virq allocation failed\n");
return 0;
}

if (irq_domain_associate(domain, virq, hwirq)) {
irq_free_desc(virq);
return 0;
}

在irq_domain_associate函数中建立关系的时候,首先通过执行domain->ops->map设置该中断的irq_desc中的irq_data的chip_data为该gpio_chip,设置irq_desc的irq_handler为该gpio_chip的irq_handler函数。

1
2
3
4
5
6
7
8
9
10
11
ret = domain->ops->map(domain, virq, hwirq);
实现代码为gpiochip_irq_map:
struct gpio_chip *chip = d->host_data;

irq_set_chip_data(irq, chip);
/*
* This lock class tells lockdep that GPIO irqs are in a different
* category than their parents, so it won't report false recursion.
*/
irq_set_lockdep_class(irq, chip->lock_key);
irq_set_chip_and_handler(irq, chip->irqchip, chip->irq_handler);

然后直接将该virq添加到domain中的linear_revmap数组中以hwirq为下标的项中,在该domain中记录硬件中断号和虚拟中断号的关系。

1
domain->linear_revmap[hwirq] = virq;

完成domain申请和irq_desc添加后,为其父中断irq注册中断,设置父中断的irq回调函数为omap_gpio_irq_handler。

1
2
3
ret = devm_request_irq(bank->chip.parent, bank->irq,
omap_gpio_irq_handler,
0, dev_name(bank->chip.parent), bank);

由于gpio中断控制器是连接到根中断控制器的某个中断上的,所以这里注册的omap_gpio_irq_handler函数就是为根中断控制器的关联中断申请了中断处理函数。

中断系统框架

在linux内核中,每一个中断都用一个irq_desc结构描述。irq_desc中用一个handle_irq方法进行中断处理,用action链表表示注册在该链表上的处理方法,用irq_data表示硬件相关的数据操作,比如irq_chip,硬件中断号,中断控制器的domain等。

handle_irq一般有两种实现方法,当该irq_desc为一个中断控制器的父中断时,该方法一般实现为分发功能,而且该方法一般由子中断控制器实现,主要功能是根据子中断控制器的状态状态寄存器获取子中断控制器上产生中断并处理之;当irq_desc为一个普通中断时,该方法一般实现过程是调用action列表每一个列表元素的handler函数(该函数为用户真正在申请该中断时注册的处理方法),并清除中断标记。

action列表主要记录了中断申请者的回调函数。当发生中断时,在irq_desc的handle_irq方法执行的时候,会调用每一个action中的handler函数。大部分情况下action列表中只有一个元素,有些情况下会有多个元素,比如一个共享的中断。

irq_data中主要为中断控制器的相关数据。每一个中断控制器都有一个domain结构,该结构用来管理该中断控制器的中断资源以及记录硬件中断号和软件中断号之间的关系。有一个或多个irq_chip结构,向中断核心提供了irq的操作函数列表。

中断处理流程:当cpu被中断时,首先通过根中断控制器的中断状态寄存器得到中断cpu的硬件中断号,然后从根中断控制器的domain结构的linear_revmap数组中得到在irq_desc中记录的中断号,再通过中断号找到irq_desc,并执行irq_desc->handle_irq函数。如果是普通中断,则在irq_desc->handle_irq中调用irq_desc中action成员的handler函数。如果是子中断控制器产生的中断,则重复上面的过程处理子中断控制器上的中断。

1
2
3
4
5
6
7
8
9
st=>start: 开始
op=>operation: 获取irq_dest
cond=>condition: 普通中断?
sub1=>subroutine: 分发到子中断控制器
io=>inputoutput: 执行action列表回调并清中断
e=>end: 结束
st->op(right)->cond
cond(yes)->io(bottom)->e
cond(no)->sub1(right)->op

中断号和硬件中断号之间的关系:在Linux内核中,用irq_desc类型的结构数组描述中断,该数组的每一个元素都表示一个中断,该数组元素的索引即是该中断的中断号,因此,中断号是唯一的,一个中断号对应一个中断。而硬件中断号是一个中断在某个中断控制器上的描述。因为有多个中断控制器,所以硬件中断号是会重复的。每个中断控制器都有一个irq_domain结构与之对应,在irq_domain中记录了硬件中断号和中断号之间的对应关系。在irq_domain中通过整数数组或基数树记录中断号和硬件中断号之间的关系,最简单的方法是用一个整数数组来记录。

1
domain->linear_revmap[hwirq] = virq;

可以看出,在linear_revmap数组中,用硬件中断号表示数组的索引值,数组元素为中断号。

四.Linux中断处理过程

C语言入口函数

cpu被中断时,先跳转到异常向量表所指定的中断入口执行指令,最终会调用到C语言函数指针接口handle_arch_irq,这个接口由不同芯片的代码设置,一般是在根中断控制器初始化的时候调用set_handle_irq接口设置的,以am335x中断驱动为例,其设置该函数代码如下:

1
set_handle_irq(omap_intc_handle_irq);

获取当前中断cpu的中断号

在omap_intc_handle_irq中首先读取中断控制器的 INTC_SIR_IRQ寄存器

1
irqnr = intc_readl(INTC_SIR);

接着通过该寄存器0-6位得到当前中断CPU的中断号(由335x芯片手册知该寄存器的0-6位表示当前中断cpu的硬件中断号)

1
irqnr &= ACTIVEIRQ_MASK;

处理中断

获取到中断控制器的当前中断号后调用handle_domain_irq函数处理该中断。

1
handle_domain_irq(domain, irqnr, regs);

handle_domain_irq函数调用__handle_domain_irq函数,在该函数中首先通过irq_find_mapping得到对应的软中断号,软硬中断号的对应关系由domain中的linear_revmap数组记录。

1
2
if (lookup)
irq = irq_find_mapping(domain, hwirq);

然后调用generic_handle_irq()函数处理该中断

1
generic_handle_irq(irq);

在generic_handle_irq函数中首先通过中断号获取到该中断对应的irq_desc,再通过generic_handle_irq_desc函数处理之

1
2
3
4
5
6
7
8
9
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);

if (!desc)
return -EINVAL;
generic_handle_irq_desc(desc);
return 0;
}

在generic_handle_irq_desc中执行该irq_desc结构里的handle_irq函数指针对应的函数处理之。

1
2
3
4
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}

根中断控制器handle_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);

cond_unmask_irq(desc);

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

由上面的代码可直根中断控制器关联的irq_desc->handle_irq函数首先会执行mask_ack_irq函数标记和响应中断源,然后调用handle_irq_event函数处理中断,该函数中会遍历action链表并执行其handler函数指针,该函数指针即为用户申请中断设置的中断处理函数。处理完中断后调用cond_unmask_irq函数清中断。由该过程可知道,用户调用申请中断的API申请中断的时候并不需要负责清中断操作。

其他中断控制器的处理函数以omap_gpio的bank中断处理函数为例说明,该函数是在gpio中断初始化的时候,注册的其父中断的中断处理函数(omap-gpio为第二级的中断控制器,其父中断为根中断控制器上的某个中断)。

在omap_gpio_irq_handler函数中,首先读取gpio中断控制器的中断状态寄存器(由335x手册可知,该寄存器的0-31位标识32个gpio中断,某一位的值为1表示该中断被触发),并清除读到的值中未使能的中断对应的位。

1
2
3
4
isr_reg = bank->base + bank->regs->irqstatus;
...
enabled = omap_get_gpio_irqbank_mask(bank);
isr_saved = isr = readl_relaxed(isr_reg) & enabled;

为了不在处理中断过程中错过新中断,在处理中断之前,先清除要处理的边沿触发类型的中断标记。

1
2
3
omap_disable_gpio_irqbank(bank, isr_saved & ~level_mask);
omap_clear_gpio_irqbank(bank, isr_saved & ~level_mask);
omap_enable_gpio_irqbank(bank, isr_saved & ~level_mask);

接着依次从中断状态寄存器中取出值为1的位,该位在中断状态寄存器中的位置就是该中断的硬件中断号,将该位置用bit变量来记录,记录后清除该位,然后调用通用中断处理函数generic_handle_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
while (isr) {
bit = __ffs(isr); //取出isr中第一个值被设置的位的位置值
isr &= ~(BIT(bit)); //清除该位置的bit

raw_spin_lock_irqsave(&bank->lock, lock_flags);
/*
* Some chips can't respond to both rising and falling
* at the same time. If this irq was requested with
* both flags, we need to flip the ICR data for the IRQ
* to respond to the IRQ for the opposite direction.
* This will be indicated in the bank toggle_mask.
*/
if (bank->toggle_mask & (BIT(bit)))
omap_toggle_gpio_edge_triggering(bank, bit);

raw_spin_unlock_irqrestore(&bank->lock, lock_flags);

raw_spin_lock_irqsave(&bank->wa_lock, wa_lock_flags);

generic_handle_irq(irq_find_mapping(bank->chip.irqdomain,
bit));

raw_spin_unlock_irqrestore(&bank->wa_lock,
wa_lock_flags);
}

在上面例子的场景里,gpio中断控制器没有子中断控制器,所以在这里generic_handle_irq函数最终会调用到用户注册的中断处理函数。

五.在linux中使用中断

在设备树中描述中断

以am33xx为例说明,中断描述分为三部分,根中断控制器,子中断控制器,以及普通中断的描述。

根中断控制器必须有interrupt-controller属性,根中断控制器还有reg属性和#interrupt-cells属性。interrupt-controller属性表示该设备节点是一个中断控制器,#interrupt-cells表示其子中断用多少个u32类型的值表示描述中断,reg属性描述根中断控制器的内存映射。在33xx系列芯片中,reg属性用两个u32的值表示,第一个表示中断控制器的基地址,第二个表示大小,基地址和大小可通过33xx用户手册的Memory Map章节获得,335x的中断控制器的地址描述如下:

1
2
3
4
Device Name		Start_address(hex)	End_addr(hex)	size	Description
________________________________________________________________________
Interrupt con 0x4820_0000 0x4820_0FFF 4KB intc registers
troller(INTCPS)

33xx系列的根中断控制器设备树节点描述如下:

1
2
3
4
5
6
intc:interrupt-controller@48200000 { /*label:name*/ /*设备树节点名字一般用name@addr表示*/
compatible = "ti,am33xx-intc"; /*用来和驱动匹配*/
interrupt-controller; /*表示自己是一个中断控制器*/
#interrupt-cells = <1>; /*子中断用1个u32类型的值来描述中断*/
reg = <0x48200000 0x1000>; /*寄存器地址和长度*/
};

子中断控制器作为一个中断控制器,也需要interrupt-controller和#interrupt-cells两个属性,还需要一个interrupts属性用来描述自己是中断控制器上的哪个中断,前面看到中断控制器的#interrupt-cells属性值为1,所以在子中断控制器的interrupts属性中,只用一个值来描述,这个值表示根中断控制器上的中断号,该值在33xx系列芯片中,可通过用户手册的interrupts章节的ARM Cortex-A8 Interrupt小节查到,335x的gpio0中断控制器的中断描述如下:

1
2
3
int number   Acronym/name   Source   Signal Name
---------------------------------------------------
96 GPIOINT0A GPIO 0 POINTRPEND1

33xx系列GPIO中断控制器设备树节点描述如下:

1
2
3
4
5
6
7
gpio0:gpio@44e07000 {
...
interrupt-controller; /*表示自己是一个中断控制器*/
#interrupt-cells = <2>; /*子中断用2个u32值来描述中断*/
interrupts = <96>; /*自己关联在根中断控制器上的中断号为96*/
...
}

其他中断节点要描述中断则需要有interrupt-parent和interrupts两个属性,interrupt-parent表示自己关联在哪个中断控制器上,interrupts用来描述该中断。以335x上nand节点为例说明,nand的父中断为gpmc中断控制器,所以其interrupt-parent应该为gpmc,gpmc中断控制器的interrupt-cells的值为2,所以在nand节点中每个中断都用2个u32类型的值表示,这两个值的意义一般需要通过查看内核源码中的文档或者驱动源码中查看,在这里第一个值表示关联在gpmc中断控制器上的中断号,第二个值表示中断类型。

1
2
3
4
5
6
nand@0,0 {
...
interrupt-parent = <&gpmc>;
interrupts = <0 IRQ_TYPE_NONE>,<1, IRQ_TYPE_NONE>;
...
}

在驱动程序中使用中断

在驱动程序中对中断的使用主要有两部分,一是从设备树解析中断节点或属性,创建中断描述符并获得中断号。二是使用得到的中断。

设备树相关的中断操作接口文件路径为:driver/of/irq.c

linux内核提供了一些中断API,定义在include/linux/interrupt.h中,一些常用接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
extern void free_irq(unsigned int, void *);

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)
extern void devm_free_irq(struct device *dev, unsigned int irq, void *dev_id);

extern void disable_irq(unsigned int irq);
extern void enable_irq(unsigned int irq);