一、前言
GIC(Generic Interrupt Controller)是ARM公司提供的通用中断控制器architecture specification目前有四个版本,V1~V4(V2最多支持8个ARM core,V3/V4支持更多的ARM core,主要用于ARM64服务器系统结构)。目前在ARM官方网站只能下载Version 2的GIC architecture specification,因此,本文的主要描述是一致的V2规范的GIC硬件及其驱动。
具体GIC硬件的实现形态有两种,一种是在ARM vensor研发自己的SOC会向ARM公司购买GIC的IP,这些IP型号包括:PL390,GIC-400,GIC-500。其中GIC-500最多支持128个 cpu core,它要求ARM core必须是ARMV8指令集(例如Cortex-A57),符合GIC architecture specification version 3。另一种形式是ARM vensor直接购买ARM公司的Cortex A9或者A15的IP,Cortex A9或者A15中会包括了GIC当然,这些实现也是一致的GIC V2的规格。
本文主要用于硬件描述GIC-以400为目标,当然也会顺便提到一些Cortex A9或者A15上的GIC实现。
本文主要分析linux kernel中GIC中断控制器的驱动代码(位于drivers/irqchip/irq-gic.c和irq-gic-common.c)。 irq-gic-common.c中是GIC V2和V通用代码,而irq-gic.c是V2 specific的代码,irq-gic-v3.c是V3 specific代码,不在本文的描述范围内。本文主要分为三个部分:第二章描述GIC V2硬件;第三章描述了GIC V2四章描述了底层硬件的初始化过程call back函数。
注:具体的linux kernel的版本是linux-3.17-rc3。
二、GIC-V2的硬件描述
1、GIC-V2的输入和输出信号
(1)GIC-V2.输入输出信号示意图
理解一个building block(无论是软件还是硬件),我们都可以先把它当成黑盒子,只研究它input,output。GIC-V输入和输出信号的示意图如下(注:我们使用GIC-以400为例,同时省略了400clock,config等信号):
(2)输入信号
上图左侧是外设interrupt source输入信号。分为两种类型,分别是PPI(Private Peripheral Interrupt)和SPI(Shared Peripheral Interrupt)。事实上,从名称中可以看出两种类型中断信号的特征,PPI中断信号是CPU私有,每个CPU都有特定的PPI信号线。而SPI是所有CPU共享。通过寄存器GICD_TYPER可以配置SPI(最多480个)。GIC-400支持多少个SPI中断,它的输入信号线有多少SPI interrupt request signal。同样,通过寄存器GICD_TYPER也可以配置CPU interface(最多8个),GIC-400支持多少个CPU interface,其输入信号线就提供多少组PPI中断信号线。一组PPI中断信号线包括6个实际信号signal:
(a)nLEGACYIRQ信号线。对应interrupt ID 31,在bypass mode下(这里的bypass是指bypass GIC functionality,直接连接到某个processor上),nLEGACYIRQ可直接连接到对应CPU的nIRQCPU信号线上。在这样的设置下,该CPU不参与其他属于该的CPU的PPI以及SPI中断响应,但特别是为这个中断服务。
(b)nCNTPNSIRQ信号线Non-secure physical timer对应中断事件interrupt ID 30。
(c)nCNTPSIRQ信号线secure physical timer对应中断事件interrupt ID 29。
(d)nLEGACYFIQ信号线interrupt ID 28。概念同nLEGACYIRQ不再描述信号线。
(e)nCNTVIRQ信号线interrupt ID 27。Virtual Timer Event,这里不描述虚拟化。
(f)nCNTHPIRQ信号线interrupt ID 26。Hypervisor Timer Event,这里不描述虚拟化。
对于Cortex A15的GIC实现,其PPI除了以上六条,还有一条中断信号线叫做Virtual Maintenance Interrupt,对应interrupt ID 25。
对于Cortex A9的GIC实现,其PPI中断信号线包括5条:
(a)nLEGACYIRQ信号线和nLEGACYFIQ信号线interrupt ID 31和interrupt ID 28.这部分与上面一致。
(b)由于Cortext A每个处理器都有自己的处理器Private timer和watch dog timer,这两个HW block分别使用了ID 29和ID 30
(c)Cortext A9内嵌一个global timer系统中的一切processor共享,对应interrupt ID 27
关于private timer和global timer请参考时间子系统的相关文件。
请参考虚拟化系列文件,了解一系列与虚拟化相关的中断。
(3)输出信号
所谓输出信号,其实就是GIC和各个CPU直接的接口,这些接口包括:
(a)触发CPU中断信号。nIRQCPU和nFIQCPU熟悉信号线ARM CPU工程师应该熟悉这两条信号线,主要用于触发ARM cpu进入IRQ mode和FIQ mode。
(b)Wake up信号。nFIQOUT和nIRQOUT信号线,去ARM CPU的电源管理模块,用来唤醒CPU的
(c)AXI slave interface signals。AXI(Advanced eXtensible Interface)属于总线协议AMBA规范的一部分。通过这些信号线,ARM CPU可以和GIC硬件block通信(如寄存器访问)。
(4)分配中断号
GIC-V支持的中断类型如下:
(a)外设中断(Peripheral interrupt)。有实际物理interrupt request signal上面已经介绍了那些中断。
(b)中断软件触发(SGI,Software-generated interrupt)。软件可以写GICD_SGIR寄存器可用于触发中断事件processor通信。
(c)虚拟中断(Virtual interrupt)和Maintenance interrupt。这两种中断与本文无关,不再重复。
识别这些interrupt source,我们必须编码它们,具体ID分布如下:
(a)ID0~ID31用于分发到特定的特定部分process的interrupt。标识这些interrupt不能只靠ID,因为各个interrupt source都用同样的ID0~ID31识别这些interrupt需要interrupt ID + CPU interface number。ID0ID15用于SGI,ID16ID31用于PPI。PPI中断类型将被送到其私有类型process上,等等process无关。SGI是通过写GICD_SGIR由寄存器触发的中断。Distributor通过processor source ID、中断ID和target processor ID来唯一识别一个SGI。
(b)ID32~ID1019用于SPI。 这是GIC规范的最大size,实际上GIC-400最大支持480个SPI,Cortex-A15和A9上的GIC最多支持224个SPI。
2、GIC-V2的内部逻辑
(1)GIC的block diagram
GIC的block diagram如下图所示:
GIC它可以清楚地分为两部分block,一个block是Distributor(上图左边的block),一个是CPU interface。CPU interface有两种,一种是普通processor接口,另一个是虚拟机接口。Virtual CPU interface本文不会详细描述。
(2)Distributor 概述
Distributor检测功能是检测每一个interrupt source控制每种状态interrupt source的行为,分发各个interrupt source中断事件分发给指定的一个或多个CPU interface上。虽然Distributor多个可以管理interrupt source,但它总是优先级最高的那个interrupt请求送往CPU interface。Distributor控制中断包括:
(1)中断enable或者disable的控制。Distributor控制中断分为两个层次。一是控制全局中断(GIC_DIST_CTRL)。一旦disable如果全局中断,那么任何事情都会被打断interrupt source产生的interrupt event不会传递CPU interface。另一个层次是针对每个层次的interrupt sourc进行控制(GIC_DIST_ENABLE_CLEAR),disable某一个interrupt source会导致该interrupt event不会分发到CPU interface,但不影响其他interrupt source产生interrupt event的分发。
(2)控制将当前优先级最高的中断事件分发到一个或者一组CPU interface。当一个中断事件分发到多个CPU interface的时候,GIC的内部逻辑应该保证只assert 一个CPU。
(3)优先级控制。
(4)interrupt属性设定。例如是level-sensitive还是edge-triggered
(5)interrupt group的设定
Distributor可以管理若干个interrupt source,这些interrupt source用ID来标识,我们称之interrupt ID。
(3)CPU interface
CPU interface这个block主要用于和process进行接口。该block的主要功能包括:
(a)enable或者disable CPU interface向连接的CPU assert中断事件。对于ARM,CPU interface block和CPU之间的中断信号线是nIRQCPU和nFIQCPU。如果disable了中断,那么即便是Distributor分发了一个中断事件到CPU interface,但是也不会assert指定的nIRQ或者nFIQ通知processor。
(b)ackowledging中断。processor会向CPU interface block应答中断(应答当前优先级最高的那个中断),中断一旦被应答,Distributor就会把该中断的状态从pending状态修改成active或者pending and active(这是和该interrupt source的信号有关,例如如果是电平中断并且保持了该asserted电平,那么就是pending and active)。processor ack了中断之后,CPU interface就会deassert nIRQCPU和nFIQCPU信号线。
(c)中断处理完毕的通知。当interrupt handler处理完了一个中断的时候,会向写CPU interface的寄存器从而通知GIC CPU已经处理完该中断。做这个动作一方面是通知Distributor将中断状态修改为deactive,另外一方面,CPU interface会priority drop,从而允许其他的pending的interrupt向CPU提交。
(d)设定priority mask。通过priority mask,可以mask掉一些优先级比较低的中断,这些中断不会通知到CPU。
(e)设定preemption的策略
(f)在多个中断事件同时到来的时候,选择一个优先级最高的通知processor
(4)实例
我们用一个实际的例子来描述GIC和CPU接口上的交互过程,具体过程如下:
(注:图片太长,因此竖着放,看的时候有点费劲,就当活动一下脖子吧)
首先给出前提条件:
(a)N和M用来标识两个外设中断,N的优先级大于M
(b)两个中断都是SPI类型,level trigger,active-high
(c)两个中断被配置为去同一个CPU
(d)都被配置成group 0,通过FIQ触发中断
下面的表格按照时间轴来描述交互过程:
时间 交互动作的描述 T0时刻 Distributor检测到M这个interrupt source的有效触发电平 T2时刻 Distributor将M这个interrupt source的状态设定为pending T17时刻 大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告M外设的中断请求。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会修改成M interrupt source对应的ID T42时刻 Distributor检测到N这个优先级更高的interrupt source的触发事件 T43时刻 Distributor将N这个interrupt source的状态设定为pending。同时,由于N的优先级更高,因此Distributor会标记当前优先级最高的中断 T58时刻 大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告N外设的中断请求。当然,由于T17时刻已经assert CPU了,因此实际的电平信号仍然保持asserted。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会被更新成N interrupt source的ID T61时刻 软件通过读取ack寄存器的内容,获取了当前优先级最高的,并且状态是pending的interrupt ID(也就是N interrupt source对应的ID),通过读该寄存器,CPU也就ack了该interrupt source N。这时候,Distributor将N这个interrupt source的状态设定为pending and active(因为是电平触发,只要外部仍然有asserted的电平信号,那么一定就是pending的,而该中断是正在被CPU处理的中断,因此状态是pending and active) 注意:T61标识CPU开始服务该中断 T64时刻 3个clock之后,由于CPU已经ack了中断,因此GIC中CPU interface模块 deassert nFIQCPU信号线,解除发向该CPU的中断请求 T126时刻 由于中断服务程序操作了N外设的控制寄存器(ack外设的中断),因此N外设deassert了其interrupt request signal T128时刻 Distributor解除N外设的pending状态,因此N这个interrupt source的状态设定为active T131时刻 软件操作End of Interrupt寄存器(向GICC_EOIR寄存器写入N对应的interrupt ID),标识中断处理结束。Distributor将N这个interrupt source的状态修改为idle 注意:T61~T131是CPU服务N外设中断的的时间区域,这个期间,如果有高优先级的中断pending,会发生中断的抢占(硬件意义的),这时候CPU interface会向CPU assert 新的中断。 T146时刻 大约15个clock之后,Distributor向CPU interface报告当前pending且优先级最高的interrupt source,也就是M了。漫长的pending之后,M终于迎来了春天。CPU interface拉低nFIQCPU信号线,向CPU报告M外设的中断请求。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会修改成M interrupt source对应的ID T211时刻 CPU ack M中断(通过读GICC_IAR寄存器),开始处理低优先级的中断。
三、GIC-V2 irq chip driver的初始化过程
在linux-3.17-rc3\drivers\irqchip目录下保存在各种不同的中断控制器的驱动代码,这个版本的内核支持了GICV3。irq-gic-common.c是通用的GIC的驱动代码,可以被各个版本的GIC使用。irq-gic.c是用于V2版本的GIC controller,而irq-gic-v3.c是用于V3版本的GIC controller。
1、GIC的device node和GIC irq chip driver的匹配过程
(1)irq chip driver中的声明
在linux-3.17-rc3\drivers\irqchip目录下的irqchip.h文件中定义了IRQCHIP_DECLARE宏如下:
#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 }
这个宏其实就是初始化了一个struct of_device_id的静态常量,并放置在__irqchip_of_table section中。irq-gic.c文件中使用IRQCHIP_DECLARE来定义了若干个静态的struct of_device_id常量,如下:
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);
兼容GIC-V2的GIC实现有很多,不过其初始化函数都是一个。在linux kernel编译的时候,你可以配置多个irq chip进入内核,编译系统会把所有的IRQCHIP_DECLARE宏定义的数据放入到一个特殊的section中(section name是__irqchip_of_table),我们称这个特殊的section叫做irq chip table。这个table也就保存了kernel支持的所有的中断控制器的ID信息(最重要的是驱动代码初始化函数和DT compatible string)。我们来看看struct of_device_id的定义:
struct of_device_id
{
char name[32];------要匹配的device node的名字
char type[32];-------要匹配的device node的类型
char compatible[128];---匹配字符串(DT compatible string),用来匹配适合的device node
const void *data;--------对于GIC,这里是初始化函数指针
};
这个数据结构主要被用来进行Device node和driver模块进行匹配用的。从该数据结构的定义可以看出,在匹配过程中,device name、device type和DT compatible string都是考虑的因素。更细节的内容请参考__of_device_is_compatible函数。
(2)device node
不同的GIC-V2的实现总会有一些不同,这些信息可以通过Device tree的机制来传递。Device node中定义了各种属性,其中就包括了memory资源,IRQ描述等信息,这些信息需要在初始化的时候传递给具体的驱动,因此需要一个Device node和driver模块的匹配过程。在Device Tree模块中会包括系统中所有的device node,如果我们的系统使用了GIC-400,那么系统的device node数据库中会有一个node是GIC-400的,一个示例性的GIC-400的device node(我们以瑞芯微的RK3288处理器为例)定义如下:
gic: interrupt-controller@ffc01000 {
compatible = "arm,gic-400";
interrupt-controller;
#interrupt-cells = <3>;
#address-cells = <0>;
reg = <0xffc01000 0x1000="">,----Distributor address range
<0xffc02000 0x1000="">,-----CPU interface address range
<0xffc04000 0x2000="">,-----Virtual interface control block
<0xffc06000 0x2000="">;-----Virtual CPU interfaces
interrupts = ;
};
(3)device node和irq chip driver的匹配
在machine driver初始化的时候会调用irqchip_init函数进行irq chip driver的初始化。在driver/irqchip/irqchip.c文件中定义了irqchip_init函数,如下:
void __init irqchip_init(void)
{
of_irq_init(__irqchip_begin);
}
__irqchip_begin就是内核irq chip table的首地址,这个table也就保存了kernel支持的所有的中断控制器的ID信息(用于和device node的匹配)。of_irq_init函数执行之前,系统已经完成了device tree的初始化,因此系统中的所有的设备节点都已经形成了一个树状结构,每个节点代表一个设备的device node。of_irq_init是在所有的device node中寻找中断控制器节点,形成树状结构(系统可以有多个interrupt controller,之所以形成中断控制器的树状结构,是为了让系统中所有的中断控制器驱动按照一定的顺序进行初始化)。之后,从root interrupt controller节点开始,对于每一个interrupt controller的device node,扫描irq chip table,进行匹配,一旦匹配到,就调用该interrupt controller的初始化函数,并把该中断控制器的device node以及parent中断控制器的device node作为参数传递给irq chip driver。。具体的匹配过程的代码属于Device Tree模块的内容,更详细的信息可以参考Device Tree代码分析文档。
2、GIC driver初始化代码分析
(1)gic_of_init的代码如下:
int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *cpu_base;
void __iomem *dist_base;
u32 percpu_offset;
int irq;
dist_base = of_iomap(node, 0);----------------映射GIC Distributor的寄存器地址空间
cpu_base = of_iomap(node, 1);----------------映射GIC CPU interface的寄存器地址空间
if (of_property_read_u32(node, "cpu-offset", &percpu_offset))--------处理cpu-offset属性。
percpu_offset = 0;
gic_init_bases(gic_cnt, -1, dist_base, cpu_base, percpu_offset, node);))-----主处理过程,后面详述
if (!gic_cnt)
gic_init_physaddr(node); -----对于不支持big.LITTLE switcher(CONFIG_BL_SWITCHER)的系统,该函数为空。
if (parent) {
--------处理interrupt级联
irq = irq_of_parse_and_map(node, 0); ---解析second GIC的interrupts属性,并进行mapping,返回IRQ number
gic_cascade_irq(gic_cnt, irq);
}
gic_cnt++;
return 0;
}
我们首先看看这个函数的参数,node参数代表需要初始化的那个interrupt controller的device node,parent参数指向其parent。在映射GIC-400的memory map I/O space的时候,我们只是映射了Distributor和CPU interface的寄存器地址空间,和虚拟化处理相关的寄存器没有映射,因此这个版本的GIC driver应该是不支持虚拟化的(不知道后续版本是否支持,在一个嵌入式平台上支持虚拟化有实际意义吗?最先支持虚拟化的应该是ARM64+GICV3/4这样的平台)。
要了解cpu-offset属性,首先要了解什么是banked register。所谓banked register就是在一个地址上提供多个寄存器副本。比如说系统中有四个CPU,这些CPU访问某个寄存器的时候地址是一样的,但是对于banked register,实际上,不同的CPU访问的是不同的寄存器,虽然它们的地址是一样的。如果GIC没有banked register,那么需要提供根据CPU index给出一系列地址偏移,而地址偏移=cpu-offset * cpu-nr。
interrupt controller可以级联。对于root GIC,其传入的parent是NULL,因此不会执行级联部分的代码。对于second GIC,它是作为其parent(root GIC)的一个普通的irq source,因此,也需要注册该IRQ的handler。由此可见,非root的GIC的初始化分成了两个部分:一部分是作为一个interrupt controller,执行和root GIC一样的初始化代码。另外一方面,GIC又作为一个普通的interrupt generating device,需要象一个普通的设备驱动一样,注册其中断handler。理解irq_of_parse_and_map需要irq domain的知识,请参考linux kernel的中断子系统之(二):irq domain介绍。
(2)gic_init_bases的代码如下:
void __init gic_init_bases(unsigned int gic_nr, int irq_start,
void __iomem *dist_base, void __iomem *cpu_base,
u32 percpu_offset, struct device_node *node)
{
irq_hw_number_t hwirq_base;
struct gic_chip_data *gic;
int gic_irqs, irq_base, i;
gic = &gic_data[gic_nr];
gic->dist_base.common_base = dist_base; ----省略了non banked的情况
gic->cpu_base.common_base = cpu_base;
gic_set_base_accessor(gic, gic_get_common_base);
for (i = 0; i < NR_GIC_CPU_IF; i++) ---后面会具体描述gic_cpu_map的含义
gic_cpu_map[i] = 0xff;
if (gic_nr == 0 && (irq_start & 31) > 0) {
--------------------(a)
hwirq_base = 16;
if (irq_start != -1)
irq_start = (irq_start & ~31) + 16;
} else {
hwirq_base = 32;
}
gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f; ----(b)
gic_irqs = (gic_irqs + 1) * 32;
if (gic_irqs > 1020)
gic_irqs = 1020;
gic->gic_irqs = gic_irqs;
gic_irqs -= hwirq_base;----------------------------(c)
if (of_property_read_u32(node, "arm,routable-irqs",----------------(d)
&nr_routable_irqs)) {
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id()); -------(e)
if (IS_ERR_VALUE(irq_base)) {
WARN(1, "Cannot allocate irq_descs @ IRQ%d, assuming pre-allocated\n",
irq_start);
irq_base = irq_start;
}
gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, -------(f)
hwirq_base, &gic_irq_domain_ops, gic);
} else {
gic->domain = irq_domain_add_linear(node, nr_routable_irqs, --------(f)
&gic_irq_domain_ops,
gic);
}
if (gic_nr == 0) {
---只对root GIC操作,因为设定callback、注册Notifier只需要一次就OK了
#ifdef CONFIG_SMP
set_smp_cross_call(gic_raise_softirq);------------------(g)
register_cpu_notifier(&gic_cpu_notifier);------------------(h)
#endif
set_handle_irq(gic_handle_irq); ---这个函数名字也不好,实际上是设定arch相关的irq handler
}
gic_chip.flags |= gic_arch_extn.flags;
gic_dist_init(gic);---------具体的硬件初始代码,参考下节的描述
gic_cpu_init(gic);
gic_pm_init(gic);
}
(a)gic_nr标识GIC number,等于0就是root GIC。hwirq的意思就是GIC上的HW interrupt ID,并不是GIC上的每个interrupt ID都有map到linux IRQ framework中的一个IRQ number,对于SGI,是属于软件中断,用于CPU之间通信,没有必要进行HW interrupt ID到IRQ number的mapping。变量hwirq_base表示该GIC上要进行map的base ID,hwirq_base = 16也就意味着忽略掉16个SGI。对于系统中其他的GIC,其PPI也没有必要mapping,因此hwirq_base = 32。
在本场景中,irq_start = -1,表示不指定IRQ number。有些场景会指定IRQ number,这时候,需要对IRQ number进行一个对齐的操作。
(b)变量gic_irqs保存了该GIC支持的最大的中断数目。该信息是从GIC_DIST_CTR寄存器(这是V1版本的寄存器名字,V2中是GICD_TYPER,Interrupt Controller Type Register,)的低五位ITLinesNumber获取的。如果ITLinesNumber等于N,那么最大支持的中断数目是32(N+1)。此外,GIC规范规定最大的中断数目不能超过1020,1020-1023是有特别用户的interrupt ID。
(c)减去不需要map(不需要分配IRQ)的那些interrupt ID,OK,这时候gic_irqs的数值终于和它的名字一致了。gic_irqs从字面上看不就是该GIC需要分配的IRQ number的数目吗?
(d)of_property_read_u32函数把arm,routable-irqs的属性值读出到nr_routable_irqs变量中,如果正确返回0。在有些SOC的设计中,外设的中断请求信号线不是直接接到GIC,而是通过crossbar/multiplexer这个的HW block连接到GIC上。arm,routable-irqs这个属性用来定义那些不直接连接到GIC的中断请求数目。
(e)对于那些直接连接到GIC的情况,我们需要通过调用irq_alloc_descs分配中断描述符。如果irq_start大于0,那么说明是指定IRQ number的分配,对于我们这个场景,irq_start等于-1,因此不指定IRQ 号。如果不指定IRQ number的,就需要搜索,第二个参数16就是起始搜索的IRQ number。gic_irqs指明要分配的irq number的数目。如果没有正确的分配到中断描述符,程序会认为可能是之前已经准备好了。
(f)这段代码主要是向系统中注册一个irq domain的数据结构。为何需要struct irq_domain这样一个数据结构呢?从linux kernel的角度来看,任何外部的设备的中断都是一个异步事件,kernel都需要识别这个事件。在内核中,用IRQ number来标识某一个设备的某个interrupt request。有了IRQ number就可以定位到该中断的描述符(struct irq_desc)。但是,对于中断控制器而言,它不并知道IRQ number,它只是知道HW interrupt number(中断控制器会为其支持的interrupt source进行编码,这个编码被称为Hardware interrupt number )。不同的软件模块用不同的ID来识别interrupt source,这样就需要映射了。如何将Hardware interrupt number 映射到IRQ number呢?这需要一个translation object,内核定义为struct irq_domain。
每个interrupt controller都会形成一个irq domain,负责解析其下游的interrut source。如果interrupt controller有级联的情况,那么一个非root interrupt controller的中断控制器也是其parent irq domain的一个普通的interrupt source。struct irq_domain定义如下:
struct irq_domain {
……
const struct irq_domain_ops *ops;
void *host_data;
……
};
这个数据结构是属于linux kernel通用中断子系统的一部分,我们这里只是描述相关的数据成员。host_data成员是底层interrupt controller的私有数据,linux kernel通用中断子系统不应该修改它。对于GIC而言,host_data成员指向一个struct gic_chip_data的数据结构,定义如下:
struct gic_chip_data {
union gic_base dist_base;------------------GIC Distributor的基地址空间
union gic_base cpu_base;------------------GIC CPU interface的基地址空间
#ifdef CONFIG_CPU_PM--------------------GIC 电源管理相关的成员
u32 saved_spi_enable[DIV_ROUND_UP(1020, 32)];
u32 saved_spi_conf[DIV_ROUND_UP(1020, 16)];
u32 saved_spi_target[DIV_ROUND_UP(1020, 4)];
u32 __percpu *saved_ppi_enable;
u32 __percpu *saved_ppi_conf;
#endif
struct irq_domain *domain;-----------------该GIC对应的irq domain数据结构
unsigned int gic_irqs;-------------------GIC支持的IRQ的数目
#ifdef CONFIG_GIC_NON_BANKED
void __iomem *(*get_base)(union gic_base *);
#endif
};
对于GIC支持的IRQ的数目,这里还要赘述几句。实际上并非GIC支持多少个HW interrupt ID,其就支持多少个IRQ。对于SGI,其处理比较特别,并不归入IRQ number中。因此,对于GIC而言,其SGI(从0到15的那些HW interrupt ID)不需要irq domain进行映射处理,也就是说SGI没有对应的IRQ number。如果系统越来越复杂,一个GIC不能支持所有的interrupt source(目前GIC支持1020个中断源,这个数目已经非常的大了),那么系统还需要引入secondary GIC,这个GIC主要负责扩展外设相关的interrupt source,也就是说,secondary GIC的SGI和PPI都变得冗余了(这些功能,primary GIC已经提供了)。这些信息可以协助理解代码中的hwirq_base的设定。
在注册GIC的irq domain的时候还有一个重要的数据结构gic_irq_domain_ops,其类型是struct irq_domain_ops ,对于GIC,其irq domain的操作函数是gic_irq_domain_ops,定义如下:
static const struct irq_domain_ops gic_irq_domain_ops = {
.map = gic_irq_domain_map,
.unmap = gic_irq_domain_unmap,
.xlate = gic_irq_domain_xlate,
};
irq domain的概念是一个通用中断子系统的概念,在具体的irq chip driver这个层次,我们需要一些解析GIC binding,创建IRQ number和HW interrupt ID的mapping的callback函数,更具体的解析参考后文的描述。
漫长的准备过程结束后,具体的注册比较简单,调用irq_domain_add_legacy或者irq_domain_add_linear进行注册就OK了。关于这两个接口请参考linux kernel的中断子系统之(二):irq domain介绍。
(g) 一个函数名字是否起的好足可以看出工程师的功力。set_smp_cross_call这个函数看名字也知道它的含义,就是设定一个多个CPU直接通信的callback函数。当一个CPU core上的软件控制行为需要传递到其他的CPU上的时候(例如在某一个CPU上运行的进程调用了系统调用进行reboot),就会调用这个callback函数。对于GIC,这个callback定义为gic_raise_softirq。这个函数名字起的不好,直观上以为是和softirq相关,实际上其实是触发了IPI中断。
(h)在multi processor环境下,当processor状态发送变化的时候(例如online,offline),需要把这些事件通知到GIC。而GIC driver在收到来自CPU的事件后会对cpu interface进行相应的设定。
3、GIC硬件初始化
(1)Distributor初始化,代码如下:
static void __init gic_dist_init(struct gic_chip_data *gic)
{
unsigned int i;
u32 cpumask;
unsigned int gic_irqs = gic->gic_irqs;---------获取该GIC支持的IRQ的数目
void __iomem *base = gic_data_dist_base(gic); ----获取该GIC对应的Distributor基地址
writel_relaxed(0, base + GIC_DIST_CTRL); -----------(a)
cpumask = gic_get_cpumask(gic);---------------(b)
cpumask |= cpumask << 8;
cpumask |= cpumask << 16;------------------(c)
for (i = 32; i < gic_irqs; i += 4)
writel_relaxed(cpumask, base + GIC_DIST_TARGET + i * 4 / 4); --(d)
gic_dist_config(base, gic_irqs, NULL); ---------------(e)
writel_relaxed(1, base + GIC_DIST_CTRL);-------------(f)
}
(a)Distributor Control Register用来控制全局的中断forward情况。写入0表示Distributor不向CPU interface发送中断请求信号,也就disable了全部的中断请求(group 0和group 1),CPU interace再也收不到中断请求信号了。在初始化的最后,step(f)那里会进行enable的动作(这里只是enable了group 0的中断)。在初始化代码中,并没有设定interrupt source的group(寄存器是GIC_DIST_IGROUP),我相信缺省值就是设定为group 0的。
(b)我们先看看gic_get_cpumask的代码:
static u8 gic_get_cpumask(struct gic_chip_data *gic)
{
void __iomem *base = gic_data_dist_base(gic);
u32 mask, i;
for (i = mask = 0; i < 32; i += 4) {
mask = readl_relaxed(base + GIC_DIST_TARGET +