电脑基础 · 2023年3月27日

中断控制器

在Linux内核中,各个设备驱动可以简单地调用request_irq()、enable_irq()、disable_irq()、
local_irq_disable()、local_irq_enable()等通用API来完成中断申请、使能、禁止等功能。

local_irq_disable()、local_irq_enable()的实现与具体中断控制器无关,对于ARM v6以上的体系结
构而言,是直接调用CPSID/CPSIE指令进行,而对于ARM v6以前的体系结构,则是通过MRS、MSR指令
来读取和设置ARM的CPSR寄存器。由此可见,local_irq_disable()、local_irq_enable()针对的并不是
外部的中断控制器,而是直接让CPU本身不响应中断请求。相关的实现位于arch/arm/include/asm/irqflags.h

1#if __LINUX_ARM_ARCH__ >= 6
2
3static inline unsigned long arch_local_irq_save(void)
4{
5 unsigned long flags;
6
7 asm volatile(
8 " mrs %0, cpsr @ arch_local_irq_save\n"
9 " cpsid i"
10 : "=r" (flags) : : "memory", "cc");
11 return flags;
12}
13
14static inline void arch_local_irq_enable(void)
15{
16 asm volatile(
17 " cpsie i @ arch_local_irq_enable"
18 :
19 :
20 : "memory", "cc");
21}
22
23static inline void arch_local_irq_disable(void)
24{
25 asm volatile(
26 " cpsid i @ arch_local_irq_disable"
27 :
28 :
29 : "memory", "cc");
30}
31#else
32
33/*
34 * Save the current interrupt enable state & disable IRQs
35 */
36static inline unsigned long arch_local_irq_save(void)
37{
38 unsigned long flags, temp;
39
40 asm volatile(
41 " mrs %0, cpsr @ arch_local_irq_save\n"
42 " orr %1, %0, #128\n"
43 " msr cpsr_c, %1"
44 : "=r" (flags), "=r" (temp)
45 :
46 : "memory", "cc");
47 return flags;
48}
49
50/*
51 * Enable IRQs
52 */
53static inline void arch_local_irq_enable(void)
54{
55 unsigned long temp;
56 asm volatile(
57 " mrs %0, cpsr @ arch_local_irq_enable\n"
58 " bic %0, %0, #128\n"
59 " msr cpsr_c, %0"
60 : "=r" (temp)
61 :
62 : "memory", "cc");
63}
64
65/*
66 * Disable IRQs
67 */
68static inline void arch_local_irq_disable(void)
69{
70 unsigned long temp;
71 asm volatile(
72 " mrs %0, cpsr @ arch_local_irq_disable\n"
73 " orr %0, %0, #128\n"
74 " msr cpsr_c, %0"
75 : "=r" (temp)
76 :
77 : "memory", "cc");
78}
79 #endif

与local_irq_disable()和local_irq_enable()不同,disable_irq()、enable_irq()针对的则是中断
控制器,因此它们适用的对象是某个中断。disable_irq()的字面意思是暂时屏蔽掉某中断(其实在内核
的实现层面上做了延后屏蔽),直到enable_irq()后再执行ISR。实际上,屏蔽中断可以发生在外设、中
断控制器、CPU三个位置,如图1所示。对于外设端,是从源头上就不产生中断信号给中断控制器,由
于它高度依赖于外设于本身,所以Linux不提供标准的API而是由外设的驱动直接读写自身的寄存器。
中断控制器
在内核中,通过irq_chip结构体来描述中断控制器。该结构体内部封装了中断mask、unmask、ack等成
员函数,其定义于include/linux/irq.h中,如代码清单4所示。

1struct irq_chip {
2 const char *name;
3 unsigned int (*irq_startup)(struct irq_data *data);
4 void (*irq_shutdown)(struct irq_data *data);
5 void (*irq_enable)(struct irq_data *data);
6 void (*irq_disable)(struct irq_data *data);
7
8 void (*irq_ack)(struct irq_data *data);
9 void (*irq_mask)(struct irq_data *data);
10 void (*irq_mask_ack)(struct irq_data *data);
11 void (*irq_unmask)(struct irq_data *data);
12 void (*irq_eoi)(struct irq_data *data);
13
14 int (*irq_set_affinity)(struct irq_data *data, const struct
cpumask *dest, bool force);
15 int (*irq_retrigger)(struct irq_data *data);
16 int (*irq_set_type)(struct irq_data *data, unsigned int
flow_type);
17 int (*irq_set_wake)(struct irq_data *data, unsigned int on);
18};

各个芯片公司会将芯片内部的中断控制器实现为irq_chip驱动的形式。受限于中断控制器硬件的能
力,这些成员函数并不一定需要全部实现,有时候只需要实现其中的部分函数即可。譬如
drivers/pinctrl/sirf/pinctrl-sirf.c驱动中的下面代码部分:

static struct irq_chip sirfsoc_irq_chip = {
.name = "sirf-gpio-irq",
.irq_ack = sirfsoc_gpio_irq_ack,
.irq_mask = sirfsoc_gpio_irq_mask,
.irq_unmask = sirfsoc_gpio_irq_unmask,
.irq_set_type = sirfsoc_gpio_irq_type,
};

我们只实现了其中的ack、mask、unmask和set_type成员函数,ack函数用于清中断,mask、unmask用
于中断屏蔽和取消中断屏蔽、set_type则用于配置中断的触发方式,如高电平、低电平、上升沿、下降沿
等。至于到enable_irq()的时候,虽然没有实现irq_enable()成员函数,但是内核会间接调用
irq_unmask()成员函数,这点从kernel/irq/chip.c中可以看出:

void irq_enable(struct irq_desc *desc)
{
irq_state_clr_disabled(desc);
if (desc->irq_data.chip->irq_enable)
desc->irq_data.chip->irq_enable(&desc->irq_data);
else
desc->irq_data.chip->irq_unmask(&desc->irq_data);
irq_state_clr_masked(desc);
}

在芯片内部,中断控制器可能不止1个,多个中断控制器之间还很可能是级联的。举个例子,假设芯
片内部有一个中断控制器,支持32个中断源,其中有4个来源于GPIO控制器外围的4组GPIO,每组GPIO
上又有32个中断(许多芯片的GPIO控制器也同时是一个中断控制器),其关系如图4所示。

中断控制器
图4 中断控制器典型分布图
那么,一般来讲,在实际操作中,gpio0_0~gpio0_31这些引脚本身在第1级会使用中断号28,而这些
引脚本身的中断号在实现与GPIO控制器对应的irq_chip驱动时,我们又会把它映射到Linux系统的32~63号
中断。同理,gpio1_0~gpio1_31这些引脚本身在第1级会使用中断号29,而这些引脚本身的中断号在实现
与GPIO控制器对应的irq_chip驱动时,我们又会把它映射到Linux系统的64~95号中断,以此类推。对于中
断号的使用者而言,无须看到这种2级映射关系。如果某设备想申请与gpio1_0这个引脚对应的中断,它只
需要申请64号中断即可。这个关系图看起来如图5所示。
中断控制器
图5 中断级联与映射

要特别注意的是,上述图4和5中所涉及的中断号的数值,无论是base还是具体某个GPIO对应的
中断号是多少,都不一定是如图20.4和图20.5所描述的简单线性映射。Linux使用IRQ Domain来描述一个
中断控制器所管理的中断源。换句话说,每个中断控制器都有自己的Domain。我们可以将IRQ Domain看
作是IRQ控制器的软件抽象。在添加IRQ Domain的时候,内核中存在的映射方法有:
irq_domain_add_legacy()、irq_domain_add_linear()、irq_domain_add_tree()等。

irq_domain_add_legacy()实际上是一种过时的方法,它一般是由IRQ控制器驱动直接指定中断源硬
件意义上的偏移(一般称为hwirq)和Linux逻辑上的中断号的映射关系。类似图20.5的指定映射可以被这
种方法弄出来。irq_domain_add_linear()则在中断源和irq_desc之间建立线性映射,内核针对这个IRQ
Domain维护了一个hwirq和Linux逻辑IRQ之间关系的一个表,这个时候我们其实也完全不关心逻辑中断号
了;irq_domain_add_tree()则更加灵活,逻辑中断号和hwirq之间的映射关系是用一棵radix树来描述的,
我们需要通过查找的方法来寻找hwirq和Linux逻辑IRQ之间的关系,一般适合某中断控制器支持非常多中
断源的情况。

实际上,在当前的内核中,中断号更多的是一个逻辑概念,具体数值是多少不是很关键。人们更多的
是关心在设备树中设置正确的interrupt_parrent和相对该interrupt_parent的偏移。
以drivers/pinctrl/sirf/pinctrl-sirf.c的irq_chip部分为例,在sirfsoc_gpio_probe()函数中,每组GPIO的中
断都通过gpiochip_set_chained_irqchip()级联到上一级中断控制器的中断。

代码清单5 二级GPIO中断级联到一级中断控制器

1static int sirfsoc_gpio_probe(struct device_node *np)
2{
3...
4for (i = 0; i < SIRFSOC_GPIO_NO_OF_BANKS; i++) {
5 bank = &sgpio->sgpio_bank[i];
6 spin_lock_init(&bank->lock);
7 bank->parent_irq = platform_get_irq(pdev, i);
8 if (bank->parent_irq < 0) {
9 err = bank->parent_irq;
10 goto out_banks;
11 }
12
13 gpiochip_set_chained_irqchip(&sgpio->chip.gc,
14 &sirfsoc_irq_chip,
15 bank->parent_irq,
16 sirfsoc_gpio_handle_irq);
17}
18
19...
20}

下面用一个实例来呈现这个过程,假设GPIO0_0~31对应上级中断号28,而外设A使用了GPIO0_5(即
第0组GPIO的第5个),并假定外设A的中断号为37,即32+5,中断服务程序为deva_isr()。那么,当
GPIO0_5中断发生的时候,内核的调用顺序是:sirfsoc_gpio_handle_irq()->generic_handle_irq()-

deva_isr()。如果硬件的中断系统有更深的层次,这种软件上的中断服务程序级联实际上可以有更深的
级别。

在上述实例中,GPIO0_0~31的interrupt_parrent实际是上级中断控制器,而外设A的interrupt_parrent就
是GPIO0,这些都会在设备树中进行呈现。

特别值得一提的是,目前多数主流ARM芯片内部的一级中断控制器都使用了ARM公司的GIC,我们
几乎不需要实现任何代码,只需要在设备树中添加相关的节点。

如在arch/arm/boot/dts/exynos5250.dtsi中即含有:

gic:interrupt-controller@10481000 {
compatible = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x10481000 0x1000>, <0x10482000 0x2000>;
};

打开drivers/irqchip/irq-gic.c,发现GIC驱动的入口声明如下:

IRQCHIP_DECLARE(gic_400, "arm,gic-400", 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);

这说明drivers/irqchip/irq-gic.c这个驱动可以兼容arm,gic-400、arm,cortex-a15-gic、arm,cortex-a7-gic
等,但是初始化函数都是统一的gic_of_init。