linux kernel的中断子系统之(七):GIC代码分析

作者:linuxer 发布于:2014-9-4 16:59 分类:中断子系统

一、前言

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和V3的通用代码,而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-V2的输入和输出信号的示意图如下(注:我们以GIC-400为例,同时省略了clock,config等信号):

gic-400 

(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中断信号线除了上面的6个,还有一个叫做Virtual Maintenance Interrupt,对应interrupt ID 25。

对于Cortex A9的GIC实现,其PPI中断信号线包括5根:

(a)nLEGACYIRQ信号线和nLEGACYFIQ信号线。对应interrupt ID 31和interrupt ID 28。这部分和上面一致。

(b)由于Cortext A9的每个处理器都有自己的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-V2支持的中断类型有下面几种:

(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。ID0~ID15用于SGI,ID16~ID31用于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

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 source进行控制(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接口上的交互过程,具体过程如下:

xxx

(注:图片太长,因此竖着放,看的时候有点费劲,就当活动一下脖子吧)

首先给出前提条件:

(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 + i);
        mask |= mask >> 16;
        mask |= mask >> 8;
        if (mask)
            break;
    }

    return mask;
}

这里操作的寄存器是Interrupt Processor Targets Registers,该寄存器组中,每个GIC上的interrupt ID都有8个bit来控制送达的target CPU。我们来看看下面的图片:

cpu mask

GIC_DIST_TARGETn(Interrupt Processor Targets Registers)位于Distributor HW block中,能控制送达的CPU interface,并不是具体的CPU,如果具体的实现中CPU interface和CPU是严格按照上图中那样一一对应,那么GIC_DIST_TARGET送达了CPU Interface n,也就是送达了CPU n。当然现实未必如你所愿,那么怎样来获取这个CPU的mask呢?我们知道SGI和PPI不需要使用GIC_DIST_TARGET控制target CPU。SGI送达目标CPU有自己特有的寄存器来控制(Software Generated Interrupt Register),对于PPI,其是CPU私有的,因此不需要控制target CPU。GIC_DIST_TARGET0~GIC_DIST_TARGET7是控制0~31这32个interrupt ID(SGI和PPI)的target CPU的,但是实际上SGI和PPI是不需要控制target CPU的,因此,这些寄存器是read only的,读取这些寄存器返回的就是cpu mask值。假设CPU0接在CPU interface 4上,那么运行在CPU 0上的程序在读GIC_DIST_TARGET0~GIC_DIST_TARGET7的时候,返回的就是0b00010000。

当然,由于GIC-400只支持8个CPU,因此CPU mask值只需要8bit,但是寄存器GIC_DIST_TARGETn返回32个bit的值,怎么对应?很简单,cpu mask重复四次就OK了。了解了这些知识,回头看代码就很简单了。

(c)step (b)中获取了8个bit的cpu mask值,通过简单的copy,扩充为32个bit,每8个bit都是cpu mask的值,这么做是为了下一步设定所有IRQ(对于GIC而言就是SPI类型的中断)的CPU mask。

(d)设定每个SPI类型的中断都是只送达该CPU。

(e)配置GIC distributor的其他寄存器,代码如下:

void __init gic_dist_config(void __iomem *base, int gic_irqs,  void (*sync_access)(void))
{
    unsigned int i;

    /* Set all global interrupts to be level triggered, active low.    */
    for (i = 32; i < gic_irqs; i += 16)
        writel_relaxed(0, base + GIC_DIST_CONFIG + i / 4);

    /* Set priority on all global interrupts.   */
    for (i = 32; i < gic_irqs; i += 4)
        writel_relaxed(0xa0a0a0a0, base + GIC_DIST_PRI + i);

    /* Disable all interrupts.  Leave the PPI and SGIs alone as they are enabled by redistributor registers.    */
    for (i = 32; i < gic_irqs; i += 32)
        writel_relaxed(0xffffffff, base + GIC_DIST_ENABLE_CLEAR + i / 8);

    if (sync_access)
        sync_access();
}

程序的注释已经非常清楚了,这里就不细述了。需要注意的是:这里设定的都是缺省值,实际上,在各种driver的初始化过程中,还是有可能改动这些设置的(例如触发方式)。

 

(2)CPU interface初始化,代码如下:

static void gic_cpu_init(struct gic_chip_data *gic)
{
    void __iomem *dist_base = gic_data_dist_base(gic);-------Distributor的基地址空间
    void __iomem *base = gic_data_cpu_base(gic);-------CPU interface的基地址空间
    unsigned int cpu_mask, cpu = smp_processor_id();------获取CPU的逻辑ID
    int i;


    cpu_mask = gic_get_cpumask(gic);-------------(a)
    gic_cpu_map[cpu] = cpu_mask;


    for (i = 0; i < NR_GIC_CPU_IF; i++)
        if (i != cpu)
            gic_cpu_map[i] &= ~cpu_mask; ------------(b)

    gic_cpu_config(dist_base, NULL); --------------(c)

    writel_relaxed(0xf0, base + GIC_CPU_PRIMASK);-------(d)
    writel_relaxed(1, base + GIC_CPU_CTRL);-----------(e)
}

(a)系统软件实际上是使用CPU 逻辑ID这个概念的,通过smp_processor_id可以获得本CPU的逻辑ID。gic_cpu_map这个全部lookup table就是用CPU 逻辑ID作为所以,去寻找其cpu mask,后续通过cpu mask值来控制中断是否送达该CPU。在gic_init_bases函数中,我们将该lookup table中的值都初始化为0xff,也就是说不进行mask,送达所有的CPU。这里,我们会进行重新修正。

(b)清除lookup table中其他entry中本cpu mask的那个bit。

(c)设定SGI和PPI的初始值。具体代码如下:

void gic_cpu_config(void __iomem *base, void (*sync_access)(void))
{
    int i;

    /* Deal with the banked PPI and SGI interrupts - disable all
     * PPI interrupts, ensure all SGI interrupts are enabled.     */
    writel_relaxed(0xffff0000, base + GIC_DIST_ENABLE_CLEAR);
    writel_relaxed(0x0000ffff, base + GIC_DIST_ENABLE_SET);

    /* Set priority on PPI and SGI interrupts    */
    for (i = 0; i < 32; i += 4)
        writel_relaxed(0xa0a0a0a0, base + GIC_DIST_PRI + i * 4 / 4);

    if (sync_access)
        sync_access();
}

程序的注释已经非常清楚了,这里就不细述了。

(d)通过Distributor中的寄存器可以控制送达CPU interface,中断来到了GIC的CPU interface是否可以真正送达CPU呢?也不一定,还有一道关卡,也就是CPU interface中的Interrupt Priority Mask Register。这个寄存器设定了一个中断优先级的值,只有中断优先级高过该值的中断请求才会被送到CPU上去。我们在前面初始化的时候,给每个interrupt ID设定的缺省优先级是0xa0,这里设定的priority filter的优先级值是0xf0。数值越小,优先级越过。因此,这样的设定就是让所有的interrupt source都可以送达CPU,在CPU interface这里不做控制了。

(e)设定CPU interface的control register。enable了group 0的中断,disable了group 1的中断,group 0的interrupt source触发IRQ中断(而不是FIQ中断)。

 

(3)GIC电源管理初始化,代码如下:

static void __init gic_pm_init(struct gic_chip_data *gic)
{
    gic->saved_ppi_enable = __alloc_percpu(DIV_ROUND_UP(32, 32) * 4, sizeof(u32));

    gic->saved_ppi_conf = __alloc_percpu(DIV_ROUND_UP(32, 16) * 4,  sizeof(u32));

    if (gic == &gic_data[0])
        cpu_pm_register_notifier(&gic_notifier_block);
}

这段代码前面主要是分配两个per cpu的内存。这些内存在系统进入sleep状态的时候保存PPI的寄存器状态信息,在resume的时候,写回寄存器。对于root GIC,需要注册一个和电源管理的事件通知callback函数。不得不吐槽一下gic_notifier_block和gic_notifier这两个符号的命名,看不出来和电源管理有任何关系。更优雅的名字应该包括pm这样的符号,以便让其他工程师看到名字就立刻知道是和电源管理相关的。

 

四、GIC callback函数分析

1、irq domain相关callback函数分析

irq domain相关callback函数包括:

(1)gic_irq_domain_map函数:创建IRQ number和GIC hw interrupt ID之间映射关系的时候,需要调用该回调函数。具体代码如下:

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw)
{
    if (hw < 32) {------------------SGI或者PPI
        irq_set_percpu_devid(irq);--------------------------(a)
        irq_set_chip_and_handler(irq, &gic_chip, handle_percpu_devid_irq);-------(b)
        set_irq_flags(irq, IRQF_VALID | IRQF_NOAUTOEN);--------------(c)
    } else {
        irq_set_chip_and_handler(irq, &gic_chip, handle_fasteoi_irq);----------(d)
        set_irq_flags(irq, IRQF_VALID | IRQF_PROBE);

        gic_routable_irq_domain_ops->map(d, irq, hw);----------------(e)
    }
    irq_set_chip_data(irq, d->host_data);-----设定irq chip的私有数据
    return 0;
}

(a)SGI或者PPI和SPI最大的不同是per cpu的,SPI是所有CPU共享的,因此需要分配per cpu的内存,设定一些per cpu的flag。

(b)设定该中断描述符的irq chip和high level的handler

(c)设定irq flag是有效的(因为已经设定好了chip和handler了),并且request后不是auto enable的。

(d)对于SPI,设定的high level irq event handler是handle_fasteoi_irq。对于SPI,是可以probe,并且request后是auto enable的。

(e)有些SOC会在各种外设中断和GIC之间增加cross bar(例如TI的OMAP芯片),这里是为那些ARM SOC准备的

 

(2)gic_irq_domain_unmap是gic_irq_domain_map的逆过程也就是解除IRQ number和GIC hw interrupt ID之间映射关系的时候,需要调用该回调函数。

(3)gic_irq_domain_xlate函数:除了标准的属性之外,各个具体的interrupt controller可以定义自己的device binding。这些device bindings都需在irq chip driver这个层面进行解析。要给定某个外设的device tree node 和interrupt specifier,该函数可以解码出该设备使用的hw interrupt ID和linux irq type value 。具体的代码如下:

static int gic_irq_domain_xlate(struct irq_domain *d,
                struct device_node *controller,
                const u32 *intspec, unsigned int intsize,--------输入参数
                unsigned long *out_hwirq, unsigned int *out_type)----输出参数
{
    unsigned long ret = 0; 
    *out_hwirq = intspec[1] + 16; ---------------------(a)

    *out_type = intspec[2] & IRQ_TYPE_SENSE_MASK; -----------(b)

    return ret;
}

(a)根据gic binding文档的描述,其interrupt specifier包括3个cell,分别是interrupt type(0 表示SPI,1表示PPI),interrupt number(对于PPI,范围是[0-15],对于SPI,范围是[0-987]),interrupt flag(触发方式)。GIC interrupt specifier中的interrupt number需要加上16(也就是加上SGI的那些ID号),才能转换成GIC的HW interrupt ID。

(b)取出bits[3:0]的信息,这些bits保存了触发方式的信息

 

2、电源管理的callback函数

TODO

 

3、irq chip回调函数分析

(1)gic_mask_irq函数

这个函数用来mask一个interrupt source。代码如下:

static void gic_mask_irq(struct irq_data *d)
{
    u32 mask = 1 << (gic_irq(d) % 32);

    raw_spin_lock(&irq_controller_lock);
    writel_relaxed(mask, gic_dist_base(d) + GIC_DIST_ENABLE_CLEAR + (gic_irq(d) / 32) * 4);
    if (gic_arch_extn.irq_mask)
        gic_arch_extn.irq_mask(d);
    raw_spin_unlock(&irq_controller_lock);
}

GIC有若干个叫做Interrupt Clear-Enable Registers(具体数目是和GIC支持的hw interrupt数目相关,我们前面说过的,GIC是一个高度可配置的interrupt controller)。这些Interrupt Clear-Enable Registers寄存器的每个bit可以控制一个interrupt source是否forward到CPU interface,写入1表示Distributor不再forward该interrupt,因此CPU也就感知不到该中断,也就是mask了该中断。特别需要注意的是:写入0无效,而不是unmask的操作。

由于不同的SOC厂商在集成GIC的时候可能会修改,也就是说,也有可能mask的代码要微调,这是通过gic_arch_extn这个全局变量实现的。在gic-irq.c中这个变量的全部成员都设定为NULL,各个厂商在初始中断控制器的时候可以设定其特定的操作函数。

(2)gic_unmask_irq函数

这个函数用来unmask一个interrupt source。代码如下:

static void gic_unmask_irq(struct irq_data *d)
{
    u32 mask = 1 << (gic_irq(d) % 32);

    raw_spin_lock(&irq_controller_lock);
    if (gic_arch_extn.irq_unmask)
        gic_arch_extn.irq_unmask(d);
    writel_relaxed(mask, gic_dist_base(d) + GIC_DIST_ENABLE_SET + (gic_irq(d) / 32) * 4);
    raw_spin_unlock(&irq_controller_lock);
}

GIC有若干个叫做Interrupt Set-Enable Registers的寄存器。这些寄存器的每个bit可以控制一个interrupt source。当写入1的时候,表示Distributor会forward该interrupt到CPU interface,也就是意味这unmask了该中断。特别需要注意的是:写入0无效,而不是mask的操作。

(3)gic_eoi_irq函数

当processor处理中断的时候就会调用这个函数用来结束中断处理。代码如下:

static void gic_eoi_irq(struct irq_data *d)
{
    if (gic_arch_extn.irq_eoi) {
        raw_spin_lock(&irq_controller_lock);
        gic_arch_extn.irq_eoi(d);
        raw_spin_unlock(&irq_controller_lock);
    }

    writel_relaxed(gic_irq(d), gic_cpu_base(d) + GIC_CPU_EOI);
}

对于GIC而言,其中断状态有四种:

中断状态 描述
Inactive 中断未触发状态,该中断即没有Pending也没有Active
Pending 由于外设硬件产生了中断事件(或者软件触发)该中断事件已经通过硬件信号通知到GIC,等待GIC分配的那个CPU进行处理
Active CPU已经应答(acknowledge)了该interrupt请求,并且正在处理中
Active and Pending 当一个中断源处于Active状态的时候,同一中断源又触发了中断,进入pending状态

processor ack了一个中断后,该中断会被设定为active。当处理完成后,仍然要通知GIC,中断已经处理完毕了。这时候,如果没有pending的中断,GIC就会将该interrupt设定为inactive状态。操作GIC中的End of Interrupt Register可以完成end of interrupt事件通知。

(4)gic_set_type函数

这个函数用来设定一个interrupt source的type,例如是level sensitive还是edge triggered。代码如下:

static int gic_set_type(struct irq_data *d, unsigned int type)
{
    void __iomem *base = gic_dist_base(d);
    unsigned int gicirq = gic_irq(d);
    u32 enablemask = 1 << (gicirq % 32);
    u32 enableoff = (gicirq / 32) * 4;
    u32 confmask = 0x2 << ((gicirq % 16) * 2);
    u32 confoff = (gicirq / 16) * 4;
    bool enabled = false;
    u32 val;

    /* Interrupt configuration for SGIs can't be changed */
    if (gicirq < 16)
        return -EINVAL;

    if (type != IRQ_TYPE_LEVEL_HIGH && type != IRQ_TYPE_EDGE_RISING)
        return -EINVAL;

    raw_spin_lock(&irq_controller_lock);

    if (gic_arch_extn.irq_set_type)
        gic_arch_extn.irq_set_type(d, type);

    val = readl_relaxed(base + GIC_DIST_CONFIG + confoff);
    if (type == IRQ_TYPE_LEVEL_HIGH)
        val &= ~confmask;
    else if (type == IRQ_TYPE_EDGE_RISING)
        val |= confmask;

    /*
     * As recommended by the spec, disable the interrupt before changing
     * the configuration
     */
    if (readl_relaxed(base + GIC_DIST_ENABLE_SET + enableoff) & enablemask) {
        writel_relaxed(enablemask, base + GIC_DIST_ENABLE_CLEAR + enableoff);
        enabled = true;
    }

    writel_relaxed(val, base + GIC_DIST_CONFIG + confoff);

    if (enabled)
        writel_relaxed(enablemask, base + GIC_DIST_ENABLE_SET + enableoff);

    raw_spin_unlock(&irq_controller_lock);

    return 0;
}

对于SGI类型的interrupt,是不能修改其type的,因为GIC中SGI固定就是edge-triggered。对于GIC,其type只支持高电平触发(IRQ_TYPE_LEVEL_HIGH)和上升沿触发(IRQ_TYPE_EDGE_RISING)的中断。另外需要注意的是,在更改其type的时候,先disable,然后修改type,然后再enable。

(5)gic_retrigger

这个接口用来resend一个IRQ到CPU。

static int gic_retrigger(struct irq_data *d)
{
    if (gic_arch_extn.irq_retrigger)
        return gic_arch_extn.irq_retrigger(d);

    /* the genirq layer expects 0 if we can't retrigger in hardware */
    return 0;
}

看起来这是功能不是通用GIC拥有的功能,各个厂家在集成GIC的时候,有可能进行功能扩展。

(6)gic_set_affinity

在多处理器的环境下,外部设备产生了一个中断就需要送到一个或者多个处理器去,这个设定是通过设定处理器的affinity进行的。具体代码如下:

static int gic_set_affinity(struct irq_data *d, const struct cpumask *mask_val,    bool force)
{
    void __iomem *reg = gic_dist_base(d) + GIC_DIST_TARGET + (gic_irq(d) & ~3);
    unsigned int cpu, shift = (gic_irq(d) % 4) * 8;
    u32 val, mask, bit;

    if (!force)
        cpu = cpumask_any_and(mask_val, cpu_online_mask);---随机选取一个online的cpu
    else
        cpu = cpumask_first(mask_val); --------选取mask中的第一个cpu,不管是否online

    raw_spin_lock(&irq_controller_lock);
    mask = 0xff << shift;
    bit = gic_cpu_map[cpu] << shift;-------将CPU的逻辑ID转换成要设定的cpu mask
    val = readl_relaxed(reg) & ~mask;
    writel_relaxed(val | bit, reg);
    raw_spin_unlock(&irq_controller_lock);

    return IRQ_SET_MASK_OK;
}

GIC Distributor中有一个寄存器叫做Interrupt Processor Targets Registers,这个寄存器用来设定制定的中断送到哪个process去。由于GIC最大支持8个process,因此每个hw interrupt ID需要8个bit来表示送达的process。每一个Interrupt Processor Targets Registers由32个bit组成,因此每个Interrupt Processor Targets Registers可以表示4个HW interrupt ID的affinity,因此上面的代码中的shift就是计算该HW interrupt ID在寄存器中的偏移。

(7)gic_set_wake

这个接口用来设定唤醒CPU的interrupt source。对于GIC,代码如下:

static int gic_set_wake(struct irq_data *d, unsigned int on)
{
    int ret = -ENXIO;

    if (gic_arch_extn.irq_set_wake)
        ret = gic_arch_extn.irq_set_wake(d, on);

    return ret;
}

设定唤醒的interrupt和具体的厂商相关,这里不再赘述。


4、BSP(bootstrap processor)之外,其他CPU的callback函数

对于multi processor系统,不可能初始化代码在所有的processor上都执行一遍,实际上,系统的硬件会选取一个processor作为引导处理器,我们称之BSP。这个processor会首先执行,其他的CPU都是处于reset状态,等到BSP初始化完成之后,release所有的non-BSP,这时候,系统中的各种外设硬件条件和软件条件(例如per CPU变量)都准备好了,各个non-BSP执行自己CPU specific的初始化就OK了。

上面描述的都是BSP的初始化过程,具体包括:

……
    gic_dist_init(gic);------初始化GIC的Distributor
    gic_cpu_init(gic);------初始化BSP的CPU interface
    gic_pm_init(gic);------初始化GIC的Power management
……

对于GIC的Distributor和Power management,这两部分是全局性的,BSP执行初始化一次就OK了。对于CPU interface,每个processor负责初始化自己的连接的那个CPU interface HW block。我们用下面这个图片来描述这个过程:

booting

  假设CPUx被选定为BSP,那么第三章描述的初始化过程在该CPU上欢畅的执行。这时候,被初始化的GIC硬件包括:root GIC的Distributor、root GIC CPU Interface x(连接BSP的那个CPU interface)以及其他的级联的非root GIC(上图中绿色block,当然,我偷懒,没有画non-root GIC)。

BSP初始化完成之后,各个其他的CPU运行起来,会发送CPU_STARTING消息给关注该消息的模块。毫无疑问,GIC driver模块当然要关注这样的消息,在初始化过程中会注册callback函数如下:

register_cpu_notifier(&gic_cpu_notifier);

GIC相关的回调函数定义如下:

static struct notifier_block gic_cpu_notifier = {
    .notifier_call = gic_secondary_init,
    .priority = 100,
};

static int gic_secondary_init(struct notifier_block *nfb, unsigned long action,  void *hcpu)
{
    if (action == CPU_STARTING || action == CPU_STARTING_FROZEN)
        gic_cpu_init(&gic_data[0]);---------初始化那些非BSP的CPU interface
    return NOTIFY_OK;
}

因此,当non-BSP booting up的时候,发送CPU_STARTING消息,调用GIC的callback函数,对上图中的紫色的CPU Interface HW block进行初始化,这样,就完成了全部GIC硬件的初始化过程。


Change log:
11月3号,修改包括:
1、使用GIC-V2这样更通用的描述,而不是仅仅GIC-400

 

原创文章,转发请注明出处。蜗窝科技,http://www.wowotech.net/linux_kenrel/gic_driver.html

标签: GIC 代码分析

评论:

tigger
2015-03-26 15:00
hi linuxer
3个clock之后,由于CPU已经ack了中断,因此GIC中CPU interface模块 deassert nFIQCPU信号线,解除发向该CPU的中断请求
我没有想明白,其实这个时候还是有中断M在pending的,为什么nFIQCPU信号会被deassert呢?
我看gic datasheet 没有找到对这里的描述。
linuxer
2015-03-27 08:54
@tigger:我的理解如下:
1、GIC和Processor之间除了nFIQCPU信号之外,还有AXI slave interface signals用于寄存器的访问。
2、虽然M和N同时asserted,在ack本次中断的时间点,高优先级的N胜出,processor进入对N的处理,M虽然pending,也只能等到N处理完(processor写EOI寄存器)再assert processor,这时GIC和processor的约定
3、为何一定这么做呢?因为GIC是支持中断优先级的,试想,如果在ack中断的时候,由于M仍然pending,因此nFIQCPU仍保持asserted状态,那么当一个比N还高优先级的P产生了中断,这时候如何通知Processor呢?这时候是无法同nFIQCPU信号传递的,因为它还是asserted状态
4、因此,一定ack了中断,nFIQCPU必须deasserted,无论还有多少个中断pending,哪些中断的优先级都是低于当前处理的那个。除非优先级高过当前处理中的那个中断,这时候GIC才会通过nFIQCPU通知procssor,告知该事件
tuya
2015-01-01 16:59
@linuxer,请教一个问题,dts中的中断号是软件中断号还是硬件中断号呢?
linuxer
2015-01-01 23:58
@tuya:dts中的中断号当然是硬件中断号了,对于一个使用中断的外设的dts中,一定要描述下面的内容:
1、使用哪一个interrupt controller
2、使用该interrupt controller的哪一个中断号。这个中断号是和interrupt controller和外设interrupt request signal的物理连接有关系,当然是该interrupt controller这个domain中的硬件interrupt ID

你说的软件中断号应该是指IRQ number,这个IRQ number是系统分配的,和硬件无关
tuya
2015-01-02 10:01
@linuxer:@linuxer,IRQ Domain介绍 文中写道:
这时候,对于这个版本的GIC driver而言,初始化之后,HW interrupt ID和IRQ number的映射关系已经建立,保存在线性lookup table中,size等于GIC支持的中断数目,具体如下:

index 0~15对应的IRQ无效

16号IRQ  <------------------>16号HW interrupt ID

17号IRQ  <------------------>17号HW interrupt ID

是不是说在gic driver中HW interrupt ID和IRQ number是相等的?

但是你文中又说到
     if (domain->ops->xlate(domain, irq_data->np, irq_data->args,----C
                    irq_data->args_count, &hwirq, &type))
            return 0;
    }

    /* Create mapping */
    virq = irq_create_mapping(domain, hwirq);--------D
    if (!virq)
        return virq;

C:解铃还需系铃人,interrupts属性最好由interrupt controller(也就是irq domain)解释。如果xlate函数能够完成属性解析,那么将输出参数hwirq和type,分别表示HW interrupt ID和interupt type(触发方式等)。

D:解析完了,最终还是要调用irq_create_mapping函数来创建HW interrupt ID和IRQ number的映射关系。

我的理解是在gic_irq_domain_xlate中会将dts中的中断号+32获得真正的HW interrupt ID,然后在用virq = irq_create_mapping(domain, hwirq)获得HW interrupt ID和IRQ number的映射。不知到这样理解对吗?

还有一个问题
request_irq用的应该是IRQ number吧?
linuxer
2015-01-05 12:07
@tuya:是不是说在gic driver中HW interrupt ID和IRQ number是相等的?
---------------------------------------------------------
是的

我的理解是在gic_irq_domain_xlate中会将dts中的中断号+32获得真正的HW interrupt ID,然后在用virq = irq_create_mapping(domain, hwirq)获得HW interrupt ID和IRQ number的映射。不知到这样理解对吗?
---------------------------------------------------------
IRQ和HW interrupt ID的映射数据库的建立有两种形态:
1、一次性在interrupt controller的driver中的初始化代码中完成。例如当前版本的GIC driver。
2、在各个driver的初始化代码中不断的创建,丰富,完善

因此irq_create_mapping函数有上面两种场景,对以场景1,其实并没有创建IRQ和hw interrupt之间的映射(映射已经建立好了),这是,直接返回对应的IRQ number就OK了。另外一个场景就是需要创建映射,这时候,需要调用alloc IRQ number分配一个空闲的IRQ,然调用irq_domain_associate来真正创建映射


还有一个问题
request_irq用的应该是IRQ number吧?
---------------------------------------------------------
当然,必须的
gzz
2015-07-21 16:41
@linuxer:真正的HW interrupt ID.在写入到dt里面还需要减去32吗?因为之前已经建立好hw与irq间的映射关系了。dt中的hw号在of_create_mapping时把这个值+32.则比如hw实际是30.到mapping时变为62.但一旦中断去读gic的中断id时,又直接是用hw 30去寻找irq num,那么久找不到irq=62的啦。所以在dt里面的hw需要减去32?
比如一个62的hw 串口中断。写入dt描述是《0 30 0x04>吗?
不知道理解的对不对啊
gzz
2015-07-21 17:00
@linuxer:如果是简单的情况下,这个request_irq传入的irq num本质的数值大小就等于物理中断号吧?
linuxer
2015-07-21 19:40
@gzz:对于gic而言,HW interrupt ID是[0-1019],其中:
1、SPI interrupts 范围是 [32-1019]
2、SGI interrupts 范围是 [0-15]
3、PPI interrupts 范围是 [16-31]


在dts文件中的HW interrupt ID定义如下:
1、SPI interrupts 范围是 [0-987]
2、PPI interrupts 范围是 [0-15]

只要掌握这两个编码规则,一切都很简单了
linuxer
2015-07-21 19:47
@gzz:本质上virtual interrupt ID(irq number)和HW interrupt ID之间可以建立任何的映射关系。不过对于本文中的GIC驱动代码,它采用了偷懒的做法,初始化的时候就创建了映射,这个映射关系就是virtual interrupt ID等于HW interrupt ID。
azureming
2014-11-05 10:37
非常感谢linuxer的解释
想再请教个问题
----------------------------
对于GIC而言(从硬件角度看),它的抢占就是更新GICC_IAR寄存器病assert CPU,具体CPU是否响应就不是我家的事情了。
对于linux kernel而言(从软件角度看),抢占是中断handler的抢占,因此,上面++++符号围起来的那段描述对linux kernel而言是正确的,因为一旦读出了GICC_IAR寄存器,中断handler将不会再被抢占,因为整个handler中都是关闭中断的。
----------------------------
看到你们的讨论,理解下大概是这样,低优先级中断发生的前16个Clock期间发生了高优先级中断,这个时候会CPU会服务高优先级中断先。
那如果是已经进入了低优先级中断的服务,正在处理,那这个时候高优先级中断发生了,会进行打断原来的中断服务程序么?我看到过一个说法是,当开始服务之后,会调用local_irq_enable函数打开中断,那是不是意味着这个时候高优先级中断是没有被屏蔽的来着?“一旦读出了GICC_IAR寄存器,中断handler将不会被抢占,因为整个handler中都是关闭中断的”看上去又有点矛盾,求解释下,谢谢(PS:貌似总懒得看代码,希望站在巨人的肩膀上,想速成)
azureming
2014-11-05 10:52
@azureming:(c)中断处理完毕的通知。当interrupt handler处理完了一个中断的时候,会向写CPU interface的寄存器从而通知GIC CPU已经处理完该中断。做这个动作一方面是通知Distributor将中断状态修改为deactive,另外一方面,可以允许其他的pending的interrupt向CPU interface提交。
--是指在中断处理过程中,其它pending的interrupt在distributor就会被禁止向CPU interface提交中断申请么,即使被pending的是高优先级的中断?
linuxer
2014-11-05 12:53
@azureming:多谢你的指正。我在这里的表述应该是不准确的,我已经修改成:做这个动作一方面是通知Distributor将中断状态修改为deactive,另外一方面,CPU interface会priority drop,从而允许其他的pending的interrupt向CPU提交。

distributor永远是把当前enable的优先级最高的那个pending中断送达CPU interface
linuxer
2014-11-05 12:32
@azureming:对于GIC硬件而言,preempt意味着一个高优先级中断抢占(注意:是硬件抢占,就是更新GICC_IAR寄存器并assert CPU)当前active的中断,这里包含两层意思:
1、必须有当前active的中断。如果软件已经调用了EOI的动作,GIC硬件会标记该中断为idle,那么就不存在抢占了
2、有当前active 的中断,那么GIC硬件就会记录current priority,如果新的pending中断优先级高于current priority,那么就会抢占当前中断

对于linux kernel而言,如果是已经进入了低优先级中断的服务(interrupt handler),正在处理,那这个时候高优先级中断发生了,虽然硬件会抢占并signal cpu,但是cpu是不会响应该中断的,直到软件调用local_irq_enable函数打开中断(在irq_exit函数中,进入softirq处理的时候)。当然,我上面的描述是针对比较新的内核,你说的情况可能发生在旧内核中,也就是还区分fast handler和slow handler的时代,对于slow handler,在进入具体的中断服务程序(irq action)的时候,会调用local_irq_enable函数打开中断。
“一旦读出了GICC_IAR寄存器,中断handler将不会被抢占,因为整个handler中都是关闭中断的”,我上面的这些描述是针对新的内核,我所有的分析是基于3.14内核的
linuxer
2014-11-05 12:36
@linuxer:“一旦读出了GICC_IAR寄存器,中断handler将不会被抢占,因为整个handler中都是关闭中断的”,更好懂的说法是:一旦读出了GICC_IAR寄存器,该中断的top half过程中将不会被抢占,因为整个top half过程中都是关闭中断的”
azureming
2014-11-05 14:46
@linuxer:谢谢,对于不求甚解的我,大概也知道了抢占大概咋回事了,呵呵
内容写的非常好,貌似找不到转发的按钮?(感觉很有必要建立自己的转发档案,便于搜寻)
牛文值得收藏啊
azureming
2014-11-04 16:51
非常感谢linuxer的分享。
想请教个问题,SPI分发到各个CPU有没有什么策略来着?比如是依次轮着把产生的中断给各个CPU,还是说看哪个CPU有空就给哪个CPU来着。
linuxer
2014-11-04 18:25
@azureming:对于GIC-V2而言,SPI的分发是根据Distributor中的Interrupt Processor Targets Registers来决定的。对于任何一个SPI,其都有在某个GICD_ITARGETSRn寄存器中有8个bit标识送达的processor,如果只有一个bit被set,那么就很简单了,如果该中断是当前优先级最高的中断,那么Distributor就会送到对应的CPU interface,具体CPU interface是否会signal对应的CPU还是有其他寄存器来控制(Interrupt Priority Mask Register),如果优先级OK的话,该中断最终会送达指定的CPU。

现在问题来了(不是挖掘机哪家强哦),如果该中断对应的Interrupt Processor Targets Registers中的那8个bit有多个bit被set的话,Distributor如何处理呢?你的猜测是“依次轮着把产生的中断给各个CPU,还是说看哪个CPU有空就给哪个CPU来着”,让硬件处理这么复杂的逻辑有些不合适,实际上,GIC的硬件是不会进行任何判断的,也不会集成任何的算法,它就是根据Interrupt Processor Targets Registers的bit设定情况,忠实的把中断送往指定的一个processor或者多个processors。

一个SPI的中断送往多个CPU对于软件处理是否OK呢?当然不OK了,具体原因大家可以思考一下。这时候,就要靠软件来控制了。大家可以去看看gic_set_affinity这个函数,实际上,这个函数确保一个中断的Interrupt Processor Targets Registers中的那8个bit只有一个bit被设定。
buyit
2015-04-30 22:38
@linuxer:一个SPI的终端送往多个CPU在绝大多数的时候是没有意义的,因为有可能会造成多个CPU去响应这个中断,虽然最后只有一个CPU会真正ack这个irq,而其它CPU会读到1023这个fake irq number,这样子意味着其它CPU浪费了资源做无用功,所以MIPS和ARM斗士在set_affinity函数里面做了点文法,无视上层传递下来的mask里面的多个bit,只会把该中断分发给mask里面的第一个有效位(这个位代表的cpu必须是online的)代表的CPU。
      但是这里面也有例外,假设有一个secure watchdog,它是全局的一个watchdog,并不是per-cpu类型的,这个中断一般会分发给每一个CPU,如果系统有8核,那么这个secure watchdog会分配给8个CPU,当然这个代码可以自己写,不能呼叫ARM的API。这样做的理由是:全局的secure watchdog保障的是全系统的稳定,单个CPU的watch dog事件由per-cpu的watchdog irq去处理就行了。
      还有例外的情况是类似于bus error这种中断,也是广播到所有CPU去处理。
linuxer
2015-05-03 22:29
@buyit:你说的这个secure watchdog的例子我到时很感兴趣,能否给些具体的信息?什么CPU,我打算去看看它的datasheet

PS:bus error是属于IRQ中断吗?感觉好像是其他某种类型的exception
buyit
2015-05-04 10:12
@linuxer:前公司的一种实现,没有公开资料。。。
secure watchdog和bus error都配置成了fiq,在secure mode处理。:)
tigger
2015-05-04 13:42
@linuxer:如果我没有记错的话,qualcomm 在他的b family架构系列里面,比如8x16/8x26 会有一个secure的watchdog
我不清楚他的secuer /non secure的watchdong是不是同一个watchdog,但是他的设计是比如正常时间watchdog timer out 发一个irq,这个irq给non -secure的系统处理,比如Linux kernel,如果这个时候Linux kernel hung住了,那么这个watch dog继续timer out,发一个fiq出来,这个fiq 就call到trust os里面,trust os处理了,如果turst os这个时候也卡住了,那没办法,软件没法响应,那就只能暴力让硬件重启了
buyit
2015-05-04 16:26
@tigger:qc系统里面有两种watchdog:non-secure和secure
non-secure watchdog超时,发送irq给kernel喂狗,如果kernel不能及时处理,发送fiq给secure os,这时候secure os会重启之后做dump,不会继续执行。
secure watchdog超时,发送fiq给secure os喂狗,如果secure os不能及时处理,HW直接重启之后做dump。
piter
2015-05-18 09:02
@buyit:hi linuxer:
  想請教一下,看到你說的這個gic_dist_init只會讓該中斷對應到一個cpu。然後我就去追當我們註冊irq時的這個function:request_threaded_irq,發現他最後也是call 這個gic_dist_init這個::
request_threaded_irq ->__setup_irq -> setup_affinity -> chip->irq_set_affinity (.irq_set_affinity = gic_set_affinity, drivers/irqchip/irq-gic.c)
  這樣不是代表,當我們用request_threaded_irq註冊interrupt 時候,該interrupt 只會給特定的cpu 嗎(變成大部分都只有cpu0)? 這樣感覺跟以往的認知不一樣。比如說以往會有個觀念是說若cpu0 正在處理interrupt0 這時候若有interrupt1 進來時,cpu1會去處理interrupt 1。感覺若照程式這樣看來,會變成同一時間只能有單一中斷被響應...。
   請問您有什麼看法,感謝
forion
2014-11-01 21:54
hi linuxer
这篇文章的信息量确实不是读一两次就能完全吸收的掉的。
现在问题来了,
1:local timer的那个中断,不一定是用30吧?会不会有些soc,使用自己soc里面的timer,申请SPI中断作为smp 各个core的timer 中断呢?
2:比如cpu睡下去了,一般cpu都会suspend到iram里面执行,那么比如外部一个gpio中断唤醒cpu,那么cpu的电,比如vddarm 是由gic 来唤醒cpu 让cpu上电的吗?当然我也不太清楚vddarm 是给gic供电的还是什么,因为如果vddarm给gic供电,那么肯定是先有vddarm上电,gic才能活动。
linuxer
2014-11-02 23:40
@forion:其实如果CPU suspend to ram,那么所有的context保存在RAM中,internal RAM那么小,能保存所有的现场吗?当然,对于CPU suspend to ram,系统要保证RAM CHIP是供电的,确保其数据可以保持,如果是SDRAM,多半是保持供电,SDRAM处于self refresh的状态。CPU模块是否要供电呢?说CPU语义不是特别清晰,我们还是说SOC好了,SOC是否需要供电呢?毫无疑问,Vcore(核心电压,我猜测是你说的vddarm)是不需要供给的,processor已经是suspend了,不需要取指令,不需要执行,不需要保存硬件上下文(已经保存在ram中了),有什么理由要供电呢?整个SOC唯一需要供电的就是一个小小的HW block,主要是保证系统被唤醒时候需要的block(例如一个或者两个GPIO,或者RTC)以及恢复现场需要的硬件支持(一个恢复现场需要的地址,因为下电后,再上电,processor的PC值等于0)。唤醒过程给Vcore加电当然不能是GIC的功能,GIC本身也应该是VCORE供电的。这个功能来自外部的PMU。

关于timer的问题,我明天再回答吧。
linuxer
2014-11-03 15:05
@forion:基本上如何使用PPI是由SOC系统决定的。对于GIC 400,ARM公司规定了其使用的方法(本文档中描述的内容)。不过,对于各个SOC Vendor,可以自行确定其PPI的使用方式,同样的,SOC厂商也可以自己设计local timer,global timer等。不过,对于使用Cortext A9的ARM 处理器,A9内部已经集成了local timer和watchdog timer的硬件(每个processor一个哦),对应的interrupt ID分别是29和30。
你说的“SOC里面的timer”,我想你说的是global timer,当然,一般而言,我们倾向把global timer的中断设定为PPI type,也就是说,虽然global timer的硬件只有一个,但是其产生的中断事件是per cpu的。当然,你说的方法也OK,申请SPI中断,不过,这个SPI中断必须广播给系统的所有的CPU,而这个广播的动作又需要SGI来完成。虽然可以实现功能,但是比较繁琐。
forion
2014-11-03 16:32
@linuxer:hi 我想你上面描述的是只有一个SPI的globale timer
我现在的系统是,global timer 申请4个,每个cpu上面有一个,中断号各不相同。而不是一个SPI,这样是不是就不需要广播的动作呢?
forion
2014-11-03 16:34
@forion:有4个spi的中断,作为4个cpu的local timer
linuxer
2014-11-03 17:15
@forion:那你需要四个独立的timer硬件哦。不过始终都是怪怪的,整个系统应该在一个统一的tick下运行,你的tick策略为何呢?
forion
2014-11-03 18:06
@linuxer:我就是没有完全理解这个timer的策略,好像是再有一个broadcast的timer 用作smp各个core的同步
zhedawo
2014-10-30 21:30
@linuer 你写的文章非常好,让我受益匪浅。
我有一个疑惑:
在2.3.b 里你提到: "如果在这个过程中又产生了新的中断,那么Distributor就会把该中断的状态从pending状态修改成pending and active。这时候,CPU interface仍然会保持nIRQ或者nFIQ信号的asserted状态,也就是向processor signal下一个中断"
但在实例中,T64 时刻,尽管N中断的状态为pending and active,但却Desasserted 了CPU。
请问,哪里是正确的?谢谢
linuxer
2014-10-31 10:10
@zhedawo:我花费了较长的时间恢复现场,呵呵~~~这份文档有写了有一段时间了。
我相信实例中描述的是正确的,一旦processor ack了中断,一定会CPU interface就会deassert nIRQCPU或者nFIQCPU信号线。

如果在这个过程中,该interrupt source又产生了新的中断,Distributor就会把该中断的状态从active状态(当前正在处理)修改成pending and active。当然,也不是说该中断状态变成pending就要向processor trigger中断,实际上这和Distributor如何分发中断相关
linuxer
2014-09-19 18:54
@linuxer:感謝linuxer的說明,看來我原先的理解應該是正確的
再多請教一個問題,在中斷時序圖T64到T126的中間,如果有更高優先權的外設P發生了中斷,nFIQCPU在這段時間內是不是又會assert low?

另外,是的,我來自台灣。感謝蝸窩提供這麼多優質文章,這裡每篇文章我都慢慢吸收,再配著內核代碼看,真的是種享受。
以前很急著想要短時間看很多東西,理解不紮實,結果最後也是都忘記。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
目前回复的次数有限制,因此我重新开一个评论。
我的理解:T64到T126之间,如果有更高优先级的中断进来,nFIQCPU会assert low的,当然,对于linux kernel,这时候由于关中断不能响应。

欢迎来自台湾的朋友,其实我现在就职于一家台湾公司,也算是有缘了。大家都是对linux kernel有兴趣才走到一起,多交流,多提出问题,一起进步吧!
sky
2014-09-13 18:58
@linuxer ,文章写的透彻。如果把中断的注册request_irq,触发 等讲一下,就会有一个中断的完整体系,期待!
linuxer
2014-09-13 22:32
@sky:中断注册属于linux中断子系统之之(五):driver API的内容,也是我正在进行的内容。由于最近家里搞装修,可能进度不是非常的理想,预计下个星期可以发表。

多谢对蜗窝的关注和鼓励!
maotou
2014-09-09 15:38
前辈您让我review实在不敢,只希望能全部吸收您写的.有点不明白的是,在您举的实例图中,在T0到T42时刻,CPU为什么没有ack M的中断呢,等到T42时刻N的到来,到CPU在T64去ack N的中断.这个时间间隔是小于M的响应时间的,所以为什么M没有被响应呢?  还有是不是在active and pending状态就不能被强占了呢?
linuxer
2014-09-09 17:03
@maotou:在T0到T42时刻,CPU为什么没有ack M的中断呢?
----------------------------
1、由于内核同步的原因,nFIQCPU[n]虽然是有效电平状态,但是由于该CPU处于关闭中断状态,因此不能响应该中断,也就是没有办法ack M的中断
2、即便没有关中断,从CPU响应中断到ack M(read GICC_IAR)还是需要有若干条指令的

关于interrupt preempt的问题
----------------------------
一个中断抢占的窗口size是从发生中断的那一刻到ack中断前16个clock这段时间。之所以抢占窗口的end point是ack中断前16个clock,是因为GIC需要时间(16个clock)来识别该中断(更新GICC_IAR寄存器)
maotou
2014-09-11 16:46
@linuxer:那我能理解成这个抢占的意思是在active前的pending状态发生的,其实当真正的中断进入active状态时,是不能被抢占的么?
linuxer
2014-09-11 18:50
@maotou:真是太感谢你的问题了,你的问题切中要害。我上面的回答应该是错误的(抱歉抱歉,我靠直觉回答了你的问题),我重新修正一下interrupt peemption这个概念:当CPU ack了低优先级的中断(进入active或者active and pending状态),并准备服务该中断,这时候,如果有高优先级中断进来,CPU响应了该中断,并ack这个高优先级中断,并开始服务它(而不是之前的那个active的中断),我们称低优先级中断被抢占了。
因此,实际上,pending状态是和抢占无关的(CPU都没有准备服务你又何谈抢占呢?),一个中断进入active状态,表示CPU正在服务该中断,这时候高优先级中断可以抢占低优先级中断,让CPU终止服务低优先级中断,转而服务高优先级中断。这时候,系统中有两个active状态的中断,这也被称为中断嵌套。
当然,我上面的话对于linux kernel而言是正确的,因为linux kernel进入IRQ异常向量的时候是关闭CPU中断的,新的内核整个handler都是关闭中断的,因此,一旦软件读取了GICC_IAR,就确定了本次要服务的中断号,这时候,即便产生了高优先级中断,发生了抢占,并signal到该CPU上,但是由于中断已经关闭,软件无法感知。
Xiang
2014-09-18 14:37
@linuxer:@linuxer: 上面第三段的"當然,我上面的話對於linux kernel而言是正確的",是否指的應該是"對於linux kernel而言不是正確的"?因為依照第一段搶佔的定義,應該是CPU開始服務優先權高的中斷才叫做搶占。但是新的kernel在整個handler都是關閉cpu中斷,所以應該沒有機會在CPU ack之後還發生搶占?
linuxer
2014-09-18 22:27
@Xiang:可能我这里描述的不清楚。"当然,我上面的话对于linux kernel而言是正确的",这里“上面的话”是指上一个回复中的话,具体如下:
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
一个中断抢占的窗口size是从发生中断的那一刻到ack中断前16个clock这段时间。之所以抢占窗口的end point是ack中断前16个clock,是因为GIC需要时间(16个clock)来识别该中断(更新GICC_IAR寄存器)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

对于GIC而言(从硬件角度看),它的抢占就是更新GICC_IAR寄存器病assert CPU,具体CPU是否响应就不是我家的事情了。
对于linux kernel而言(从软件角度看),抢占是中断handler的抢占,因此,上面++++符号围起来的那段描述对linux kernel而言是正确的,因为一旦读出了GICC_IAR寄存器,中断handler将不会再被抢占,因为整个handler中都是关闭中断的。

BTW,您使用繁体中文,冒昧的问一句:你是不是来自台湾或者香港?
Xiang
2014-09-19 16:40
@linuxer:感謝linuxer的說明,看來我原先的理解應該是正確的
再多請教一個問題,在中斷時序圖T64到T126的中間,如果有更高優先權的外設P發生了中斷,nFIQCPU在這段時間內是不是又會assert low?

另外,是的,我來自台灣。感謝蝸窩提供這麼多優質文章,這裡每篇文章我都慢慢吸收,再配著內核代碼看,真的是種享受。
以前很急著想要短時間看很多東西,理解不紮實,結果最後也是都忘記。
maotou
2014-09-11 16:50
@linuxer:从那个例子的图中看,感觉好像是在active and pending状态就不能被抢占了,还有前辈您能解释下active and pending这个状态的意义么,一直有些模糊?谢谢您
linuxer
2014-09-11 19:05
@maotou:从那个例子的图中看,感觉好像是在active and pending状态就不能被抢占了
-----------------------------------
例图中只是描述了一种情况,如果低优先级中断进入active and pending状态,高优先级仍然可以抢占并向CPU signal中断(当然,这时候软件是否响应该中断就另当别论了)。
对于linux kernel而言,即便高优先级中断发生了抢占(硬件角度),但是中断服务不能被抢占了(软件角度)。

关于active and pending状态
------------------------------
这个状态是和电平触发的中断相关。我们以高电平有效的中断为例子,只有该signal是高电平,对于GIC而言它就是pending的,当外设拉高电平,触发一次中断的时候,首先进入pending状态,CPU ACK该中断后会进入active,表明CPU正在服务该中断,但是,由于高电平是外设控制的,因此,即便CPU ack了中断,并不能改变该signal的高电平状态,因此该中断又是pending的,因此存在了active and pending状态。只有该外设中断的ISR中通过读写外设寄存器,清除了中断状态,这是,该中断信号才能被外设拉低,解除pending状态。

PS:不用叫我前辈,大家都是搞技术的,直接叫我的名字就OK了
maotou
2014-09-17 17:07
@linuxer:理解了很多,非常感谢您的回答,我会继续去思考 然后挖一挖
maotou
2014-09-11 17:02
@linuxer:我看关于GIC的说明文档中 中断的active and pending状态是可以转换为pending状态的,这种抢占会怎么发生呢?   (很抱歉有这么多问题额,脑子有些转不过来 嘿嘿)
linuxer
2014-09-11 19:11
@maotou:如果你愿意,可以把原文贴上来,可能更清楚。

我的理解:CPU通过发送EOI来解除中断的active状态,因此一个可能的流程是这样的:
1、中断触发,进入pending状态
2、CPU ack,进入active and pending状态
3、ISR ack,进入active状态
4、外设再次检测到中断事件,重新触发中断,进入active and pending状态
5、CPU EOI,进入pending状态

这时候,软件一旦打开中断,该中断会再次触发
maotou
2014-09-17 17:08
@linuxer:我说的图就是关于GIC的datasheet上的  几个状态之间的切换 之前不理解,现在您解释了之后理解啦
linuxer
2014-09-11 18:53
@linuxer:关于interrupt preempt的问题
----------------------------
一个中断抢占的窗口size是从发生中断的那一刻到ack中断前16个clock这段时间。之所以抢占窗口的end point是ack中断前16个clock,是因为GIC需要时间(16个clock)来识别该中断(更新GICC_IAR寄存器)

注:上面关于interrupt preempt的描述是从软件角度来看的抢占,并不是GIC定义的抢占
forion
2014-09-06 12:38
要不GIC interrupt specifier中的interrupt number加上16,不上SGI的那些ID号,intspec[1]才是真正的HW interrupt ID。
要不---是不是应该是要在
不上---是不是补上
其实不修复也应该能看懂,不过还是提一下。
linuxer
2014-09-08 00:13
@forion:已经修改啦,我都看不出来我当初是想怎么写的,重新组织了一下语句。多谢Forion!

其实还有些内容需要补充,就是多CPU的情况下,BSP和其他Processor在执行GIC初始化执行的代码是不一样的,中秋前实在没有精力写完了,匆忙发出去,节后再补充吧!

中秋节搞装修,我腿都跑断了,等上班的时候好好休息一下
forion
2014-09-09 12:44
@linuxer:多CPU的情况下,BSP和其他Processor在执行GIC初始化执行的代码是不一样的
不懂,等你补充。上面这句话我没法理解。
linuxer
2014-09-09 16:41
@forion:我增加了一个小节,描述BSP和其他non-BSP的GIC初始化序列,请参考。

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog