Chinaunix首页 | 论坛 | 博客
  • 博客访问: 336257
  • 博文数量: 106
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 861
  • 用 户 组: 普通用户
  • 注册时间: 2013-09-10 08:32
文章分类

全部博文(106)

文章存档

2016年(11)

2015年(93)

2013年(2)

分类: LINUX

2015-08-19 13:35:43

/>

Linux kernel的中断子系统之(二):IRQ Domain介绍

作者: 一、概述

在linux kernel中,我们使用下面两个ID来标识一个来自外设的中断:

1、IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。

2、HW interrupt ID。对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。

这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,这就是本文主要的内容。

 

二、历史

关于HW interrupt ID映射到IRQ number上 这事,在过去系统只有一个interrupt controller的时候还是很简单的,中断控制器上实际的HW interrupt line的编号可以直接变成IRQ number。例如我们大家都熟悉的SOC内嵌的interrupt controller,这种controller多半有中断状态寄存器,这个寄存器可能有64个bit(也可能更多),每个bit就是一个IRQ number,可以直接进行映射。这时候,GPIO的中断在中断控制器的状态寄存器中只有一个bit,因此所有的GPIO中断只有一个IRQ number,在该通用GPIO中断的irq handler中进行deduplex,将各个具体的GPIO中断映射到其相应的IRQ number上。如果你是一个足够老的工程师,应该是经历过这个阶段的。

随着linux kernel的发展,将interrupt controller抽象成irqchip这个概念越来越流行,甚至GPIO controller也可以被看出一个interrupt controller chip,这样,系统中至少有两个中断控制器了,一个传统意义的中断控制器,一个是GPIO controller type的中断控制器。随着系统复杂度加大,外设中断数据增加,实际上系统可以需要多个中断控制器进行级联,面对这样的趋势,linux kernel工程师如何应对?答案就是irq domain这个概念。

我们听说过很多的domain,power domain,clock domain等等,所谓domain,就是领域,范围的意思,也就是说,任何的定义出了这个范围就没有意义了。系统中所有的interrupt controller会形成树状结构,对于每个interrupt controller都可以连接若干个外设的中断请求(我们称之interrupt source),interrupt controller会对连接其上的interrupt source(根据其在Interrupt controller中物理特性)进行编号(也就是HW interrupt ID了)。但这个编号仅仅限制在本interrupt controller范围内。

 

三、接口

1、向系统注册irq domain

具体如何进行映射是interrupt controller自己的事情,不过,有软件架构思想的工程师更愿意对形形色色的interrupt controller进行抽象,对如何进行HW interrupt ID到IRQ number映射关系上进行进一步的抽象。因此,通用中断处理模块中有一个irq domain的子模块,该模块将这种映射关系分成了三类:

(1)线性映射。其实就是一个lookup table,HW interrupt ID作为index,通过查表可以获取对应的IRQ number。对于Linear map而言,interrupt controller对其HW interrupt ID进行编码的时候要满足一定的条件:hw ID不能过大,而且ID排列最好是紧密的。对于线性映射,其接口API如下:

static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node, 
                     unsigned int size,---------该interrupt domain支持多少IRQ 
                     const struct irq_domain_ops *ops,---callback函数 
                     void *host_data)-----driver私有数据 

    return __irq_domain_add(of_node, size, size, 0, ops, host_data); 
}

(2)Radix Tree map。建立一个Radix Tree来维护HW interrupt ID到IRQ number映射关系。HW interrupt ID作为lookup key,在Radix Tree检索到IRQ number。如果的确不能满足线性映射的条件,可以考虑Radix Tree map。实际上,内核中使用Radix Tree map的只有powerPC和MIPS的硬件平台。对于Radix Tree map,其接口API如下:

static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node, 
                     const struct irq_domain_ops *ops, 
                     void *host_data) 

    return __irq_domain_add(of_node, 0, ~0, 0, ops, host_data); 
}

(3)no map。有些中断控制器很强,可以通过寄存器配置HW interrupt ID而不是由物理连接决定的。例如PowerPC 系统使用的MPIC (Multi-Processor Interrupt Controller)。在这种情况下,不需要进行映射,我们直接把IRQ number写入HW interrupt ID配置寄存器就OK了,这时候,生成的HW interrupt ID就是IRQ number,也就不需要进行mapping了。对于这种类型的映射,其接口API如下:

static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node, 
                     unsigned int max_irq, 
                     const struct irq_domain_ops *ops, 
                     void *host_data) 

    return __irq_domain_add(of_node, 0, max_irq, max_irq, ops, host_data); 
}

这类接口的逻辑很简单,根据自己的映射类型,初始化struct irq_domain中的各个成员,调用__irq_domain_add将该irq domain挂入irq_domain_list的全局列表。

2、为irq domain创建映射

上节的内容主要是向系统注册一个irq domain,具体HW interrupt ID和IRQ number的映射关系都是空的,因此,具体各个irq domain如何管理映射所需要的database还是需要建立的。例如:对于线性映射的irq domain,我们需要建立线性映射的lookup table,对于Radix Tree map,我们要把那个反应IRQ number和HW interrupt ID的Radix tree建立起来。创建映射有四个接口函数:

(1)调用irq_create_mapping函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number(这个IRQ number是动态分配的)。该函数的原型定义如下:

extern unsigned int irq_create_mapping(struct irq_domain *host, 
                       irq_hw_number_t hwirq);

驱动调用该函数的时候必须提供HW interrupt ID,也就是意味着driver知道自己使用的HW interrupt ID,而一般情况下,HW interrupt ID其实对具体的driver应该是不可见的,不过有些场景比较特殊,例如GPIO类型的中断,它的HW interrupt ID和GPIO有着特定的关系,driver知道自己使用那个GPIO,也就是知道使用哪一个HW interrupt ID了。

(2)irq_create_strict_mappings。这个接口函数用来为一组HW interrupt ID建立映射。具体函数的原型定义如下:

extern int irq_create_strict_mappings(struct irq_domain *domain, 
                      unsigned int irq_base, 
                      irq_hw_number_t hwirq_base, int count);

(3)irq_create_of_mapping。看到函数名字中的of(open firmware),我想你也可以猜到了几分,这个接口当然是利用device tree进行映射关系的建立。具体函数的原型定义如下:

extern unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data);

通常,一个普通设备的device tree node已经描述了足够的中断信息,在这种情况下,该设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:

unsigned int irq_of_parse_and_map(struct device_node *dev, int index) 

    struct of_phandle_args oirq;

    if (of_irq_parse_one(dev, index, &oirq))----分析device node中的interrupt相关属性 
        return 0;

    return irq_create_of_mapping(&oirq);-----创建映射,并返回对应的IRQ number 
}

对于一个使用Device tree的普通驱动程序(我们推荐这样做),基本上初始化需要调用irq_of_parse_and_map获取IRQ number,然后调用request_threaded_irq申请中断handler。

(4)irq_create_direct_mapping。这是给no map那种类型的interrupt controller使用的,这里不再赘述。

 

四、数据结构描述

1、irq domain的callback接口

struct irq_domain_ops抽象了一个irq domain的callback函数,定义如下:

struct irq_domain_ops { 
    int (*match)(struct irq_domain *d, struct device_node *node); 
    int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); 
    void (*unmap)(struct irq_domain *d, unsigned int virq); 
    int (*xlate)(struct irq_domain *d, struct device_node *node, 
             const u32 *intspec, unsigned int intsize, 
             unsigned long *out_hwirq, unsigned int *out_type); 
};

我们先看xlate函数,语义是翻译(translate)的意思,那么到底翻译什么呢?在DTS文件中,各个使用中断的device node会通过一些属性(例如interrupts和interrupt-parent属性)来提供中断信息给kernel以便kernel可以正确的进行driver的初始化动作。这里,interrupts属性所表示的interrupt specifier只能由具体的interrupt controller(也就是irq domain)来解析。而xlate函数就是将指定的设备(node参数)上若干个(intsize参数)中断属性(intspec参数)翻译成HW interrupt ID(out_hwirq参数)和trigger类型(out_type)。

match是判断一个指定的interrupt controller(node参数)是否和一个irq domain匹配(d参数),如果匹配的话,返回1。实际上,内核中很少定义这个callback函数,实际上struct irq_domain中有一个of_node指向了对应的interrupt controller的device node,因此,如果不提供该函数,那么default的匹配函数其实就是判断irq domain的of_node成员是否等于传入的node参数。

map和unmap是操作相反的函数,我们描述其中之一就OK了。调用map函数的时机是在创建(或者更新)HW interrupt ID(hw参数)和IRQ number(virq参数)关系的时候。其实,从发生一个中断到调用该中断的handler仅仅调用一个request_threaded_irq是不够的,还需要针对该irq number设定:

(1)设定该IRQ number对应的中断描述符(struct irq_desc)的irq chip

(2)设定该IRQ number对应的中断描述符的highlevel irq-events handler

(3)设定该IRQ number对应的中断描述符的 irq chip data

这些设定不适合由具体的硬件驱动来设定,因此在Interrupt controller,也就是irq domain的callback函数中设定。

2、irq domain

在内核中,irq domain的概念由struct irq_domain表示:

struct irq_domain { 
    struct list_head link; 
    const char *name; 
    const struct irq_domain_ops *ops; ----callback函数 
    void *host_data;

    /* Optional data */ 
    struct device_node *of_node; ----该interrupt domain对应的interrupt controller的device node 
    struct irq_domain_chip_generic *gc; ---generic irq chip的概念,本文暂不描述

    /* reverse map data. The linear map gets appended to the irq_domain */ 
    irq_hw_number_t hwirq_max; ----该domain中最大的那个HW interrupt ID 
    unsigned int revmap_direct_max_irq; ---- 
    unsigned int revmap_size; ---线性映射的size,for Radix Tree map和no map,该值等于0 
    struct radix_tree_root revmap_tree; ----Radix Tree map使用到的radix tree root node 
    unsigned int linear_revmap[]; -----线性映射使用的lookup table 
};

linux内核中,所有的irq domain被挂入一个全局链表,链表头定义如下:

static LIST_HEAD(irq_domain_list);

struct irq_domain中的link成员就是挂入这个队列的节点。通过irq_domain_list这个指针,可以获取整个系统中HW interrupt ID和IRQ number的mapping DB。host_data定义了底层interrupt controller使用的私有数据,和具体的interrupt controller相关(对于GIC,该指针指向一个struct gic_chip_data数据结构)。

对于线性映射:

(1)linear_revmap保存了一个线性的lookup table,index是HW interrupt ID,table中保存了IRQ number值

(2)revmap_size等于线性的lookup table的size。

(3)hwirq_max保存了最大的HW interrupt ID

(4)revmap_direct_max_irq没有用,设定为0。revmap_tree没有用。

对于Radix Tree map:

(1)linear_revmap没有用,revmap_size等于0。

(2)hwirq_max没有用,设定为一个最大值。

(3)revmap_direct_max_irq没有用,设定为0。

(4)revmap_tree指向Radix tree的root node。

 

五、中断相关的Device Tree知识回顾

想要进行映射,首先要了解interrupt controller的拓扑结构。系统中的interrupt controller的拓扑结构以及其interrupt request line的分配情况(分配给哪一个具体的外设)都在Device Tree Source文件中通过下面的属性给出了描述。这些内容在Device Tree的三份文档中给出了一些描述,这里简单总结一下:

对于那些产生中断的外设,我们需要定义interrupt-parent和interrupts属性:

(1)interrupt-parent。表明该外设的interrupt request line物理的连接到了哪一个中断控制器上

(2)interrupts。这个属性描述了具体该外设产生的interrupt的细节信息(也就是传说中的interrupt specifier)。例如:HW interrupt ID(由该外设的device node中的interrupt-parent指向的interrupt controller解析)、interrupt触发类型等。

对于Interrupt controller,我们需要定义interrupt-controller和#interrupt-cells的属性:

(1)interrupt-controller。表明该device node就是一个中断控制器

(2)#interrupt-cells。该中断控制器用多少个cell(一个cell就是一个32-bit的单元)描述一个外设的interrupt request line。?具体每个cell表示什么样的含义由interrupt controller自己定义。

(3)interrupts和interrupt-parent。对于那些不是root 的interrupt controller,其本身也是作为一个产生中断的外设连接到其他的interrupt controller上,因此也需要定义interrupts和interrupt-parent的属性。

 

六、Mapping DB的建立

1、概述

系统中HW interrupt ID和IRQ number的mapping DB是在整个系统初始化的过程中建立起来的,过程如下:

(1)DTS文件描述了系统中的interrupt controller以及外设IRQ的拓扑结构,在linux kernel启动的时候,由bootloader传递给kernel(实际传递的是DTB)。

(2)在Device Tree初始化的时候,形成了系统内所有的device node的树状结构,当然其中包括所有和中断拓扑相关的数据结构(所有的interrupt controller的node和使用中断的外设node)

(3)在machine driver初始化的时候会调用of_irq_init函数,在该函数中会扫描所有interrupt controller的节点,并调用适合的interrupt controller driver进行初始化。毫无疑问,初始化需要注意顺序,首先初始化root,然后first level,second level,最好是leaf node。在初始化的过程中,一般会调用上节中的接口函数向系统增加irq domain。有些interrupt controller会在其driver初始化的过程中创建映射

(4)在各个driver初始化的过程中,创建映射

 

2、 interrupt controller初始化的过程中,注册irq domain

我们以GIC的代码为例。具体代码在gic_of_init->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;

…… 
对于root GIC 
        hwirq_base = 16; 
        gic_irqs = 系统支持的所有的中断数目-16。之所以减去16主要是因为root GIC的0~15号HW interrupt 是for IPI的,因此要去掉。也正因为如此hwirq_base从16开始


    irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());申请gic_irqs个IRQ资源,从16号开始搜索IRQ number。由于是root GIC,申请的IRQ基本上会从16号开始


    gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, 
                    hwirq_base, &gic_irq_domain_ops, gic);---向系统注册irq domain并创建映射

…… 
}

很遗憾,在GIC的代码中没有调用标准的注册irq domain的接口函数。要了解其背后的原因,我们需要回到过去。在旧的linux kernel中,ARM体系结构的代码不甚理想。在arch/arm目录充斥了很多board specific的代码,其中定义了很多具体设备相关的静态表格,这些表格规定了各个device使用的资源,当然,其中包括IRQ资源。在这种情况下,各个外设的IRQ是固定的(如果作为驱动程序员的你足够老的话,应该记得很长篇幅的针对IRQ number的宏定义),也就是说,HW interrupt ID和IRQ number的关系是固定的。一旦关系固定,我们就可以在interupt controller的代码中创建这些映射关系。具体代码如下:

struct irq_domain *irq_domain_add_legacy(struct device_node *of_node, 
                     unsigned int size, 
                     unsigned int first_irq, 
                     irq_hw_number_t first_hwirq, 
                     const struct irq_domain_ops *ops, 
                     void *host_data) 

    struct irq_domain *domain;

    domain = __irq_domain_add(of_node, first_hwirq + size,----注册irq domain 
                  first_hwirq + size, 0, ops, host_data); 
    if (!domain) 
        return NULL;

    irq_domain_associate_many(domain, first_irq, first_hwirq, size); ---创建映射

    return 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

……

如果想充分发挥Device Tree的威力,3.14版本中的GIC 代码需要修改。

 

3、在各个硬件外设的驱动初始化过程中,创建HW interrupt ID和IRQ number的映射关系

我们上面的描述过程中,已经提及:设备的驱动在初始化的时候可以调用irq_of_parse_and_map这个接口函数进行该device node中和中断相关的内容(interrupts和interrupt-parent属性)进行分析,并建立映射关系,具体代码如下:

unsigned int irq_of_parse_and_map(struct device_node *dev, int index) 

    struct of_phandle_args oirq;

    if (of_irq_parse_one(dev, index, &oirq))----分析device node中的interrupt相关属性 
        return 0;

    return irq_create_of_mapping(&oirq);-----创建映射 
}

我们再来看看irq_create_of_mapping函数如何创建映射:

unsigned int irq_create_of_mapping(struct of_phandle_args *irq_data) 

    struct irq_domain *domain; 
    irq_hw_number_t hwirq; 
    unsigned int type = IRQ_TYPE_NONE; 
    unsigned int virq;

    domain = irq_data->np ? irq_find_host(irq_data->np) : irq_default_domain;--A 
    if (!domain) { 
        return 0; 
    }


    if (domain->ops->xlate == NULL)--------------B 
        hwirq = irq_data->args[0]; 
    else { 
        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;

    /* Set type if specified and different than the current one */ 
    if (type != IRQ_TYPE_NONE && 
        type != irq_get_trigger_type(virq)) 
        irq_set_irq_type(virq, type);---------E 
    return virq; 
}

A:这里的代码主要是找到irq domain。这是根据传递进来的参数irq_data的np成员来寻找的,具体定义如下:

struct of_phandle_args { 
    struct device_node *np;---指向了外设对应的interrupt controller的device node 
    int args_count;-------该外设定义的interrupt相关属性的个数 
    uint32_t args[MAX_PHANDLE_ARGS];----具体的interrupt相当属性的定义 
};

B:如果没有定义xlate函数,那么取interrupts属性的第一个cell作为HW interrupt ID。

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

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

E:如果有需要,调用irq_set_irq_type函数设定trigger type

irq_create_mapping函数建立HW interrupt ID和IRQ number的映射关系。该接口函数以irq domain和HW interrupt ID为参数,返回IRQ number。具体的代码如下:

unsigned int irq_create_mapping(struct irq_domain *domain, 
                irq_hw_number_t hwirq) 

    unsigned int hint; 
    int virq;

如果映射已经存在,那么不需要映射,直接返回 
    virq = irq_find_mapping(domain, hwirq); 
    if (virq) { 
        return virq; 
    }


    hint = hwirq % nr_irqs;-------分配一个IRQ 描述符以及对应的irq number 
    if (hint == 0) 
        hint++; 
    virq = irq_alloc_desc_from(hint, of_node_to_nid(domain->of_node)); 
    if (virq <= 0) 
        virq = irq_alloc_desc_from(1, of_node_to_nid(domain->of_node)); 
    if (virq <= 0) { 
        pr_debug("-> virq allocation failed\n"); 
        return 0; 
    }

    if (irq_domain_associate(domain, virq, hwirq)) {---建立mapping 
        irq_free_desc(virq); 
        return 0; 
    }

    return virq; 
}

对于分配中断描述符这段代码,后续的文章会详细描述。这里简单略过,反正,指向完这段代码,我们就可以或者一个IRQ number以及其对应的中断描述符了。程序注释中没有使用IRQ number而是使用了virtual interrupt number这个术语。virtual interrupt number还是重点理解“virtual”这个词,所谓virtual,其实就是说和具体的硬件连接没有关系了,仅仅是一个number而已。具体建立映射的函数是irq_domain_associate函数,代码如下:

int irq_domain_associate(struct irq_domain *domain, unsigned int virq, 
             irq_hw_number_t hwirq) 

    struct irq_data *irq_data = irq_get_irq_data(virq); 
    int ret;

    mutex_lock(&irq_domain_mutex); 
    irq_data->hwirq = hwirq; 
    irq_data->domain = domain; 
    if (domain->ops->map) { 
        ret = domain->ops->map(domain, virq, hwirq);---调用irq domain的map callback函数 
    }

    if (hwirq < domain->revmap_size) { 
        domain->linear_revmap[hwirq] = virq;----填写线性映射lookup table的数据 
    } else { 
        mutex_lock(&revmap_trees_mutex); 
        radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);--向radix tree插入一个node 
        mutex_unlock(&revmap_trees_mutex); 
    } 
    mutex_unlock(&irq_domain_mutex);

    irq_clear_status_flags(virq, IRQ_NOREQUEST); ---该IRQ已经可以申请了,因此clear相关flag

    return 0; 
}

 

七、将HW interrupt ID转成IRQ number

创建了庞大的HW interrupt ID到IRQ number的mapping DB,最终还是要使用。具体的使用场景是在CPU相关的处理函数中,程序会读取硬件interrupt ID,并转成IRQ number,调用对应的irq event handler。在本章中,我们以一个级联的GIC系统为例,描述转换过程

1、GIC driver初始化

上面已经描述了root GIC的的初始化,我们再来看看second GIC的初始化。具体代码在gic_of_init->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;

…… 
对于second GIC 
        hwirq_base = 32;  
        gic_irqs = 系统支持的所有的中断数目-32。之所以减去32主要是因为对于second GIC,其0~15号HW interrupt 是for IPI的,因此要去掉。而16~31号HW interrupt 是for PPI的,也要去掉。也正因为如此hwirq_base从32开始


    irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());申请gic_irqs个IRQ资源,从16号开始搜索IRQ number。由于是second GIC,申请的IRQ基本上会从root GIC申请的最后一个IRQ号+1开始


    gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base, 
                    hwirq_base, &gic_irq_domain_ops, gic);---向系统注册irq domain并创建映射

…… 
}

second GIC初始化之后,该irq domain的HW interrupt ID和IRQ number的映射关系已经建立,保存在线性lookup table中,size等于GIC支持的中断数目,具体如下:

index 0~32对应的IRQ无效

root GIC申请的最后一个IRQ号+1  <------------------>32号HW interrupt ID

root GIC申请的最后一个IRQ号+2  <------------------>33号HW interrupt ID

……

OK,我们回到gic的初始化函数,对于second GIC,还有其他部分的初始化内容:

int __init gic_of_init(struct device_node *node, struct device_node *parent) 
{

……

    if (parent) { 
        irq = irq_of_parse_and_map(node, 0);--解析second GIC的interrupts属性,并进行mapping,返回IRQ number 
        gic_cascade_irq(gic_cnt, irq);---设置handler 
    } 
…… 
}

上面的初始化函数去掉和级联无关的代码。对于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函数相信大家已经熟悉了,这里不再描述。gic_cascade_irq函数如下:

void __init gic_cascade_irq(unsigned int gic_nr, unsigned int irq) 

    if (irq_set_handler_data(irq, &gic_data[gic_nr]) != 0)---设置handler data 
        BUG(); 
    irq_set_chained_handler(irq, gic_handle_cascade_irq);---设置handler 
}

2、具体如何在中断处理过程中,将HW interrupt ID转成IRQ number

在系统的启动过程中,经过了各个interrupt controller以及各个外设驱动的努力,整个interrupt系统的database(将HW interrupt ID转成IRQ number的数据库,这里的数据库不是指SQL lite或者oracle这样通用数据库软件)已经建立。一旦发生硬件中断,经过CPU architecture相关的中断代码之后,会调用irq handler,该函数的一般过程如下:

(1)首先找到root interrupt controller对应的irq domain。

(2)根据HW 寄存器信息和irq domain信息获取HW interrupt ID

(3)调用irq_find_mapping找到HW interrupt ID对应的irq number

(4)调用handle_IRQ(对于ARM平台)来处理该irq number

对于级联的情况,过程类似上面的描述,但是需要注意的是在步骤4中不是直接调用该IRQ的hander来处理该irq number因为,这个irq需要各个interrupt controller level上的解析。举一个简单的二阶级联情况:假设系统中有两个interrupt controller,A和B,A是root interrupt controller,B连接到A的13号HW interrupt ID上。在B interrupt controller初始化的时候,除了初始化它作为interrupt controller的那部分内容,还有初始化它作为root interrupt controller A上的一个普通外设这部分的内容。最重要的是调用irq_set_chained_handler设定handler。这样,在上面的步骤4的时候,就会调用13号HW interrupt ID对应的handler(也就是B的handler),在该handler中,会重复上面的(1)~(4)。

 

原创文章,转发请注明出处。蜗窝科技。 标签:

评论:

gzz 
2015-07-22 10:41
假设我有一个二级的中断是SPI32要注册,挂在一级的20上。那也就是我这个二级 32号中断注册的时候直接以irq=20 request就行吗?request_irq会去帮我找帮handle放到到对应的irq_desc吗。 
如果有多级了,这个irq_desc内核又是如何维护的呢?
2015-07-23 00:31
@gzz:假设我有一个二级的中断是SPI32要注册,挂在一级的20上。那也就是我这个二级 32号中断注册的时候直接以irq=20 request就行吗? 
--------------------------------------- 
当然不行。 
唉,看来我的文档还是写的不行啊,我通篇都是在将irq number和HW interrupt ID的东西,我觉得自己讲的很清楚了,不过读者似乎不是这样的,郁闷。 

1、对于老的方式而言,IRQ都是固定的,在driver初始化的时候可以通过传统的方法xxx_get_resource的方法获得irq number,这种方式都已经固定了所有的irq desc,因此irq desc也是一个静态表格而已。一旦获取了irq number,就可以调用request_irq来申请中断了 
2、对于使用device tree的情况,一般可以通过of_irq_get的方式(Note:一般不会直接调用该函数,你的driver模型会封装的,例如如果你的设备是挂接到platform bus上的时候,可以使用platform_get_irq来获取irq number),一旦获取了irq number,就可以调用request_irq来申请中断了 

描述硬件中断编号需要两个因素:irq domain和HW interrupt ID。根据你描述的例子,实际上硬件中断编号有两个,分别是: 
1、一级irq domain的HW interrupt ID 52,也就是SPI20 
2、二级irq domain的HW interrupt ID 64,也就是SPI32 
二级irq domain的SPI32在注册的时候当然不能直接注册,因为这里描述都是硬件中断编号,不是irq number,必须申请irq number的资源并建立映射(当然,也可以在中断控制器初始化的时候一次性创建所有的映射,不过那也不是推荐的方式)。 

如果有多级了,这个irq_desc内核又是如何维护的呢? 
------------------------------------------------ 
irq_desc或者说irq number是和复杂的硬件拓扑没有关系的,关于中断描述符有一份专门的文档描述,可以参考
gzz 
2015-07-23 09:08
@linuxer:thanks。你可能没有理解我的意思。就是说像ARM,他一般gic_init_base的时候已经做好一个controller的domain的irq和hw的映射。就比如现在SPI32要注册了,arm简单点一般irq num就是硬件中断号64了。那我的疑问是一级假如也有一个SPI32号硬件中断(对应于irq 64)被注册了呢?那内核是怎么request的时候是如何去区分相同的一级和二级的irq.还是说这样就直接报错,不运行呢?还是说这种级联情况下,应该不会存在这个问题?所以我就想是不是irq_desc会给每个domain来分组啥的。 
谢谢,菜鸟不是很懂。
gzz 
2015-07-21 10:19
请问,那如果还是使用老是的板级描述符 
resource = { 
.start = IRQ_EINT(irq_num)。 

这里的还是用实际的物理中断号码?还是?,总不可能自己去映射关联吧。这种模式是直接被淘汰了?
2015-07-22 00:30
@gzz:这种老的模式当然也支持,不过在device tree出现后已经不推荐使用了,如果的确想使用,可以参考platform_get_resource的实现。在resource的定义中有关于irq resource的类型,其start成员和end成员都是定义的irq number(virtual interrupt number),而“IRQ_EINT(irq_num)”中的irq_num是HW interrupt ID,本身arch/arm/mach-xxx/include/mach/irqs.h中定义的一大堆IRQ_XXX就是将HW interrupt ID映射到irq number的宏定义,也就是建立了irq number和HW interrupt ID之间的映射关系。
gzz 
2015-07-21 10:01
额,终于明白DT下platform device建立时的resource提取的IRQ原来是irq_of_parse_and_map后的。还一直以为就是interrupt cells里面的hw irq num,原来做了map处理。
2015-07-01 11:36
西安出差两周,终于能继续看这么美妙的文章了,真是写的好,万分膜拜,其实这些都不是我自己工作业务相关的,我就是喜欢内核,闲了就自己看源码,可惜汇编不咋地,一到硬件层面的驱动需要各种查汇编语法
2015-07-02 00:12
@SleepDeXiang:欢迎,欢迎,不论什么工作性质,只要保持一颗对linux kernel的热爱之心就OK了,把这个网站办成一个喜欢钻研linux kernel代码的集中营就是我们最大的心愿
tastier 
2015-03-24 19:16
您好,看了您的文章,对应到自己做的项目,有两个个小问题请教。 
1. 对于两个级联的8259A芯片,HW interupt ID是如何编码的?手册对两个级联芯片的中断输入管脚标有IRQ0~IRQ15,这个是编码吗? 
2. 内核里的request_irq()函数的第一个参数irq,这个是文中的IRQ number吗?
2015-03-25 11:40
@tastier:为了回答你的问题,我特地下载了8259A的datasheet来看,实际上,8259A支持8个interrupt,当两个8259A进行级联的时候,就形成了两个IRQ Domain,这两个domain的HW interrupt是可以重复的。 
回到你的具体问题:1. 对于两个级联的8259A芯片,HW interupt ID是如何编码的? 
--------------------------------- 
我认为每个8259A都是根据自己硬件引脚IR0~IR7进行HW interrupt ID的编码,对应HW interrupt ID 0~HW interrupt ID 7 

2. 内核里的request_irq()函数的第一个参数irq,这个是文中的IRQ number吗? 
------------------------------- 
是的 

我是搞ARM的,对X86的东西不是那么熟悉,上面是我的观点,请参考
tastier 
2015-03-28 22:44
@linuxer:非常感谢您的回答,在看了您的回答之后,我又经过几天的学习,产生了如下几个问题。 

1. 对于内核中的request_irq()函数,最终调用了request_threaded_irq()函数,但内核源码对于request_threaded_irq函数第一个参数irq的描述是:@irq: Interrupt line to allocate,意思是分配一个中断信号线,不是IRQ number? 
我理解Interrupt line应该是HW interrupt ID吧,不知对不对? 

2. 我看其它很多关于中断的文章都有中断向量这个概念,不知中断向量和IRQ number的区别在哪里? 
与此对应的是,中断描述符表(IDT)和中断描述符(struct irq_desc)。我看过这种描述:“当中断或异常发生时,CPU通过查中断向量号对应IDT的表项决定动作”。您在下一篇文章中这样描述:“通过IRQ number就可以获取对应的中断描述符。调用中断描述符中的highlevel irq-events handler来进行中断处理就OK了”。这里定义了一个struct irq_desc类型的数组,它和IDT的区别是什么? 

3. 在您的文章中,引用的代码和定义的概念是否针对ARM,X86不是很适用(有的适用有的不适用,尤其是一些代码,我没有接触过ARM)? 

期待您的回复。
2015-03-29 23:14
@tastier:1、我觉得这里的内核注释有些模糊,本质上request_threaded_irq函数第一个参数irq就是就是IRQ number(virtual interrupt ID),这个ID其实是和实际的interrupt request line是没有关系的,只是在irq domain的范围内有映射关系而已。我同意你的说法,interrupt line、interrupt request line或者HW interrupt ID都是类似的概念,如果我是中断子系统的维护者,我会修改这里的注释 

2、所谓向量,当然就不是标量,呵呵~~~有点像是废话。标量是一维的,或者说是一个数值量。向量是向量空间的一个元素,说起来有些抽象,我们可以以三维实数坐标空间R3(3应该是上标)为例,(x,y,z)就是R3中的一个元素,当然x,y,z都是实数。 
我们再来看中断。对于计算机系统而言,中断就是硬件或者软件向CPU发出的一种信号,希望CPU可以暂停当前程序的执行,转而处理该事件。 
根据上面对向量和中断的理解,我们可知中断向量一定就是一组数据了,这一组数据表示了在发生中断后,CPU的各个寄存器(包括PC、状态寄存器和通用寄存器)需要加载的内容。对于ARM,中断向量只有一个地址(interrupt handler的地址)需要加载给CPU的r15寄存器(对于X86,需要加载CS和IP这两个寄存器,我都有些忘记了,好像是这样吧) 
OK,应该可以进入中断向量表了,所谓中断向量表就是一个table,保存了若干的中断向量而已。中断向量表是一个通用的概念,在X86上,它具体就是体现为中断描述符表。对于ARM,中断向量表更经常的被称为异常向量表。对于ARM,代码如下: 
---------------------------------------------- 
.section .vectors, "ax", %progbits 
__vectors_start: 
    W(b)    vector_rst 
    W(b)    vector_und 
    W(ldr)    pc, __vectors_start + 0x1000 
    W(b)    vector_pabt 
    W(b)    vector_dabt 
    W(b)    vector_addrexcptn 
    W(b)    vector_irq ---------------------------IRQ Vector 
    W(b)    vector_fiq ---------------------------FRQ Vector 
------------------------------------------------------------- 
和传统意义上的中断相关的只有两项,毫无疑问,这是不够的,因此,在ARM上进行了两次的跳转,也就是说,一个外设中断发生了,首先跳转到IRQ vector,然后由它再根据当前中断控制器的状态,获取了IRQ number,最终完成了到具体interrupt handler的跳转。 
有些处理器不是这样的,对于X86,其中断向量表很大,分的很细(例如除0产生的异常也占有一个vecotr),每一个外设中断都是占用一个中断向量表的entry。 
-------------- 
我看过这种描述:“当中断或异常发生时,CPU通过查中断向量号对应IDT的表项决定动作”。 
--------------这段话应该适用于类似像X86这样的中断向量表设计的CPU,更具体的内容我明天查阅x86的中断处理再回复你 

3、没有错,我的文档都是for ARM的,我对X86不是那么熟悉。如果你是搞X86的,那么大家可以多交流互通有无。
tastier 
2015-03-30 22:48
@linuxer:谢谢您的耐心回复,虽然我的问题表述都不是很清楚。 
看了您的回复,对几个概念的理解真是如梦初醒,在阅读内核源码就明白多了。 

我目前做的小项目是基于AMD x86 cpu的,用到了中断,正在努力学习,还希望能和您这样经验丰富的大牛多多交流。
2015-03-24 15:14
hi linuxer 
hint = hwirq % nr_irqs;-------分配一个IRQ 描述符以及对应的irq number 
这里的nr_irqs需要设置吗?我看默认值是64,这个值是不是要设置成实际的硬件支持的所有的硬件中断的个数呢? 
我还是有点没有搞明白这里hint的作用,
2015-03-24 17:45
@tigger:这个nr_irqs=NR_IRQS 
那这个宏就是一个预定义的咯,根据不同的系统写死的一个中断个数咯
2015-03-24 19:04
@tigger:中断描述符(interrupt descriptor)有两种组织形式: 
1、组织成静态数组 
2、组织成Radix tree 
如果组织成静态数组,那么NR_IRQS就是系统中的total的IRQ的数目,最大的irq ID就是NR_IRQS-1了。当引入radix tree之后,事情变得复杂一些,NR_IRQS是预先分配的IRQ数目(估计是为了兼容旧的驱动)。 
在中断子系统的代码中经常需要一个当前系统中的最大的IRQ number的值,在单纯而美好的静态数组时代,NR_IRQS就够用了,在引入Radix tree后以及CONFIG_SPARSE_IRQ这样的配置项后,系统中IRQ number的最大值是一个变化的值,随着IRQ number的分配而不断变化,因此内核引入了一个变量来跟踪当前系统中IRQ number的最大值,这个变量就是nr_irqs。在初始化的时候,由于固定分配了NR_IRQS个IRQ number,因此nr_irqs被初始化成NR_IRQS
2015-03-24 19:18
@tigger:nr_irqs这个值随着中断描述符的分配(irq_alloc_descs)而扩展(当然,也有可能不增加,主要看分配的IRQ number是否大于nr_irqs),如何分配IRQ number是通过一个bitmap进行管理的,每次分配IRQ number从bitmap的的0bit开始搜索不是一个好主意,hint是一个可能是一个更合适的值。
2015-03-26 14:23
@linuxer:为什么在gic初始化的时候,已经给所有的irq都初始化了desc,我们在这里还是要再申请一次呢? 
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,  numa_node_id()); -------(e)
2015-03-27 12:15
@tigger:我没有明白你的问题,在初始化的时候,gic driver会调用irq_alloc_descs一次性的分配了若干个中断描述符,因此nr_irqs也会一次性的进行的扩展。 
irq_alloc_descs最终会调用alloc_descs函数分配若干desc,当然,对于静态数据类型的,不会真正分配,仅仅是站上那个位置而已,对于CONFIG_SPARSE_IRQ的那种,的确会分配desc的内存并插入到radix tree中。 

对于gic driver,一直都是初始化分配desc啊
2015-03-27 14:51
@linuxer:我是说 为什么irq_create_mapping 里面又会再分配一次呢?
2015-03-27 15:45
@tigger:看,沟通还是需要成本的,我的理解是这样的: 
对于目前版本的GIC driver,其实它采用了一个比较丑陋的方法: 
1、调用irq_alloc_descs一次性的分配了若干个中断描述符 
2、调用irq_domain_add_legacy一次性的创建所有的irq number到HW interrupt ID的映射。 
也就是说,gic driver初始化之后就一切搞定了,分配好了irq descriptor,创建了映射关系。 

而在具体的驱动程序中,虽然有机会调用irq_create_mapping 来创建mapping,不过由于mapping已经存在,因此不会再次分配了
gzz 
2015-07-21 14:49
@linuxer:alloc_descs这个貌似非smp下,是不重新malloc的吗,直接用的那个irq_desc全局数组。那要是用了SMP,是费用在alloc_desc一次吗?
2015-07-22 00:07
@gzz:alloc_descs这个函数和SMP没有关系,适合CONFIG_SPARSE_IRQ这个配置项有关,如果配置了CONFIG_SPARSE_IRQ,那么需要动态分配,否则使用irq_desc全局数组,静态分配
强 
2015-03-23 13:53
@linuxer 文章中有两个地方不是很清楚,能解答下吗? 
第一个: 
对于root GIC 
        hwirq_base = 16; 
        gic_irqs = 系统支持的所有的中断数目-16。之所以减去16主要是因为root GIC的0~15号HW interrupt 是for IPI的,因此要去掉。也正因为如此hwirq_base从16开始 

#这里说的IPI是什么意思?看gic的文档这里说的是SGI,IPI是什么,这两个是一个东西吗? 

第二个问题: 
index 0~15对应的IRQ无效 

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

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


#这里是说HW interrupt 和 IRQ number 从16号中断开始,是一一对应的意思吗? 

我在device tree  中申请了这样一个中断 
interrupts = <0  4 0x4> 
然后我用 
interrupt = platform_get_resource(dev->pdev, IORESOURCE_IRQ, 0); 
并将interrupt->start 发现其值是36, 

这里就有了两个疑问, 
一:申请的是4号中断,4号中断在0-15号中断区间,映射到linux IRQ number 变成了36呢?差值是32 
二:类似的,我又申请了<0 31 0x4>中断,打印出来interrupt->start 是63,差值是32, 

请linuxer有空解答下哈。
2015-03-23 15:00
@强:我先回答一下你吧 
------------------------------- 
#这里说的IPI是什么意思?看gic的文档这里说的是SGI,IPI是什么,这两个是一个东西吗? 
Inter-processor interrupts (IPI) can, for example, be used for interrupt-driven communication between CPUs in an SMP system. The GIC implements IPI using 16 software interrupts. To generate an IPI, an MPCore CPU must write to the primary GIC's Software Interrupt register specifying an interrupt ID in the range 0 to 15 and which CPU(s) will receive the interrupt. 
从上面来看,SGI跟IPI是同一个东西 
----------------------------------------------------- 

#这里是说HW interrupt 和 IRQ number 从16号中断开始,是一一对应的意思吗? 
hw interrrupt指的是硬件的中断号我的理解这里指的是 实际接到gic上面的硬件中断线,IRQ number其实指的是软件中断号。这两个应该是一一对应的。当然如果接到gic的只是一个子的中断控制器,比如gpio中断控制器,那么任意一个gpio的IRQ number,跟这个hw interrupt并不是一一对应的。 

--------------------------------------- 
3:在dt里面写的是SPI,Shared Peripheral Interrupt ,你通过dt 解出来的中断号是要+32的,因为我的理解,物理上,他是接到SPI上,SPI就是从32开始
强 
2015-03-23 15:23
@tigger:@trigger 回复的好快啊,非常感谢,基本明白了,特别是第三问题,一直没想明白,今天算是解惑了。 

支持你们。
2015-03-23 15:37
@强:my pleasure 
可以等linuxer来了再给个更详细的解释。我的理解可能有偏差。
2015-03-23 23:32
@tigger:顶,多谢tigger同学的回答,我就不补充了,呵呵
wang 
2015-05-13 18:07
@linuxer:@linuxer: 刚开始看linux的中断,这个SPI是什么?
wang 
2015-05-13 18:06
@tigger:@linuxer: 刚开始看linux的中断,这个SPI是什么东西?
2015-02-03 16:53
楼主能否分析一下irq_gic_v3.c的代码?有许多疑惑想和大神交流一下。
2015-02-05 00:00
@schedule:我正在写一个编译、链接和加载的文章,既然你有兴趣,那我先完成手头的这篇,然后再写如irq_gic_v3.c的代码分析文章好了
2014-10-23 15:10
@linuxer,文章写的真好。 
1 针对接口部分的第1篇章:是不是少了irq_domain_legacy_revmap()这个接口函数?? 
2 针对hw irq number和 gpio之间的对应关系,我有一个疑问,若hw irq number和gpio之间是一一对应的。例如hw irq number为34,对应的gpio号为5; hwirq number为35是对应的gpio号为6.这样的对应关系,可以采用irq domain的架构吗?调用哪一个接口函数比较好?? 
3 针对hwirq number和irq number的对应,我现在晕晕糊糊的。
2014-10-23 19:38
@linxi_hnh:多谢! 
1、irq_domain_legacy_revmap()函数的legacy已经说明这是一个要被废弃的接口。在最新的3.14内核中,我没有找到它 
2、irq domain是应对HW interrupt ID和IRQ number之间的关系,实际上,irq domain并不关心GPIO的编码。你说的这个场景应该是GPIO type的interrupt controller的实现,一般而言,GPIO type的interrupt controller要关注HW interrupt到GPIO之间的映射,例如HW interrupt ID=0,对应GPA中的0号GPIO,HW interrupt ID=1,对应GPA中的1号GPIO,......,使用哪一种irq domain是和你如何规划IRQ number到HW interrupt ID之间的映射相关的,一般而言,我们会采用线性映射方式的 
3、呵呵,其实很简单啊,你可以再具体说说那里比较迷糊,我可以进一步表述
阅读(2979) | 评论(0) | 转发(0) |
0

上一篇:中断的概述

下一篇:IRQ number和中断描述符

给主人留下些什么吧!~~