天天看点

中断控制器介绍及初始化源代码分析(二)

1.GIC中断控制器介绍

ARM处理器中最常用的中断控制器是GIC(Global Interrupt Controller)。GIC支持3中中断类型。

(1)SGI(Software Generated Interrupt):软件产生的中断,通常用于多核之间的通信。一个CPU可通过写GIC的寄存器给另外一个CPU产生中断(IPI_WAKEUP、IPI_TIMER等)。SGI中断通常在Linux内核里被称为IPI中断(interprocess interrupts)。

(2)PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的CPU,如CPU本地时钟中断。

(3)SPI(Shared Peripheral interrupt):共享外设中断,这类外设的中断可以发送给任何一个CPU。

GIC中断控制器主要由三部分组成,分别为distributor模块、CPU Interface模块和Virtual CPU interfaces模块。distributor模块为每个中断源维护一个状态机,支持的状态有inactive、pending、active和active and pending状态,distributor模块会记录和比较出当前优先级最高的处于pending状态的中断,然后将高优先级的中断发送给CPU Interface模块。每个CPU对应一个CPU Interface模块,CPU Interface模块决定这个中断是否可以发送给对应的CPU。这里不讨论Virtual CPU interfaces模块。

PPI和SPI的触发方式有边沿触发(Edge-triggered)和水平触发(Level-sensitive)方式。

中断控制器介绍及初始化源代码分析(二)

GIC处理中断的流程如下:

(1)当GIC检测到一个中断发生时,会将该中断标记为pending状态。

(2)对处于pending状态的中断,仲裁单元会将中断请求发送到目标CPU。

(3)对于每个CPU,仲裁单元从众多pending状态的中断中选择一个优先级最高的中断,发送到CPU Interface模块上。

(4)CPU Interface模块决定这个中断是否可以发送给CPU。如果这个中断的优先级满足要求,GIC会给CPU发送一个中断请求。

(5)当CPU收到中断请求后,会进入异常处理模式,读取GICC_IAR寄存器获取硬件中断号来响应来响应中断(通常是Linux内核中断处理程序读取寄存器)。在硬件中断处理完成之前(写GICC_EOIR寄存器表示硬件中断处理完成)GIC不会再向此cpu发送任何中断。对于SPI和PPI中断,返回的是硬件中断号,对于SGI中断,返回的是发出中断CPU的ID和硬件中断号。当GIC感知到软件读取了该寄存器后,又分为如下情况(详细的中断状态变化可参考《ARM® Generic Interrupt Controller Architecture version 2.0》第39页):

 (a.)如果该中断源是pending状态且是边缘触发,那么状态将变成active,如果该中断源是pending状态且是电平触发,那么状态将变成active and pending。对于状态为active and pending的中断,gic不会再将此中断发给任何cpu,直到状态变为inactive后,gic才会重新发送中断。

 (b.)如果该中断是active状态,将变成active and pending状态。

(6)当处理器完成中断服务后,软件写GICC_EOIR寄存器,表示中断处理完成。

下图是gic-400中断控制器的时序图,图中的中断N和M都是SPI类型的FIQ中断,高电平触发,N的优先级比M高,目标CPU相同。

(1)T1时刻:GIC distributor模块检测到中断M的电平变化。

(2)T2时刻:distributor模块设置中断M的状态为pending。

(3)T17时刻:CPU Interface模块拉低nFIQCPU[n]通知CPU发生了中断。

(4)T42时刻:GIC distributor模块检测到了另外一个优先级更高的中断N。

(5)T43时刻:GIC distributor模块用中断N替换了当前pending状态下优先级最高的中断,并设置中断N为pending状态。

(6)T58时刻:CPU Interface模块将中断N的硬件中断号更新到GICC_IAR寄存器Interrupt ID区域。

(7)T61时刻:CPU读取GICC_IAR寄存器获取了硬件中断号同时响应了中断N,distributor模块将中断N的状态由pending状态变为active and pending状态(电平触发的中断cpu响应后中断状态将会从pending直接变为active and pending)。

(8)T61-T131时刻:中断处理程序处理中断N。

 (a)CPU响应中断N 3个时钟后,CPU Interface模块拉高nFIQCPU[n]。

 (b)T126时刻cpu清除了外设的中断N,则外设的中断N引脚电平被拉低。

 (c)T128时刻distributor模块将中断的N状态由active and pending更新为active状态。

 (d)T131时刻cpu将中断N的硬件中断号写入GICC_EOIR寄存器,表示中断N处理完毕。distributor模块将中断的N状态由active更新为inactive状态。

(9)T146时刻:中断N完成后,经过tph时钟,distributor模块选择下一个最高优先级的中断,即将中断M发送给CPU Interface模块,CPU Interface模块拉低nFIQCPU[n]通知CPU发生了中断。

(10)T211时刻:CPU读取GICC_IAR寄存器获取了硬件中断号同时响应了中断M,distributor模块将中断M的状态由pending状态变为active and pending状态。

(11)T214时刻:CPU响应中断M 3个时钟后,CPU Interface模块拉高nFIQCPU[n]。

中断控制器介绍及初始化源代码分析(二)

2.zynq7k中断控制器

2.1.简介

zynq7020有两个Cortex-A9内核,采用GIC pl390(属于GIC V1版本)中断控制器。zynq7020中断控制器有16个SGI中断,硬件中断号范围0-15,5个PPI中断,硬件中断号范围16-31,其中16-26保留,没有使用,60个SPI中断,硬件中断号范围32-95,93-95保留,没有使用。SPI中断可通过读取spi_status_0和spi_status_1寄存器,获取发生中断的硬件中断号。

中断控制器介绍及初始化源代码分析(二)

2.2.设备树节点

zynq7k设备树中的中断控制器描述如下。兼容属性

compatible

arm,cortex-a9-gic

,用来与驱动程序匹配,匹配成功后,驱动会被正确加载。

#interrupt-cells = <3>

表示要用3个32位整数描述中断,第一个整数说明中断类型,0表示SPI中断,1表示PPI中断;第二个整数说明中断号;第三个整数说明中断触发类型。

interrupt-controller

属性说明此节点是一个中断控制器节点,

reg

表示中断控制器寄存器基地址及范围,

0xF8F01000

distributor

模块寄存器基地址,长度为

0x1000

0xF8F00100

CPU Interface

模块寄存器的基地址,长度为

0x100

。intc节点无

interrupt-parent

属性,说明此中断控制器为根(root)中断控制器,直接连接cpu。内核启动的时候会解析设备树节点,将设备树节点转换为

device_node

结构体,然后将这些

device_node

以树的形式组织起来,若某个驱动的属性和设备树节点中的属性一致,则驱动会被初始化并被加载到系统中。

intc: [email protected] {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0xF8F01000 0x1000>,  // ICDXX
          <0xF8F00100 0x100>;   // ICCXX
};
           

gpio0也是一个中断控制器,具有

interrupt-controller

属性,其父中断控制器为

intc

,即上面提到的zynq7k的根中断控制器。

gpio0: [email protected] {
    compatible = "xlnx,zynq-gpio-1.0";
    #gpio-cells = <2>;
    clocks = <&clkc 42>;
    gpio-controller;
    interrupt-controller;
    #interrupt-cells = <2>;
    interrupt-parent = <&intc>;  // 说明父中断控制器为intc
    interrupts = <0 20 4>;  // SPI中断,硬件中断号为20
    reg = <0xe000a000 0x1000>;
};
           

3.中断控制器的初始化过程

Linux内核使用

struct irq_desc

描述中断,每个中断都对应一个

irq_desc

结构体。

irq_desc

有两种组织形式,如果定义了

CONFIG_SPARSE_IRQ

,则采用

radix-tree

组织

irq_desc

,初始化时需要动态分配

irq_desc

,并将其插入到

irq_desc_tree

中,此种组织形式中断号不连续;反之,使用静态定义的数组

irq_desc

组织

irq_desc

,只需要初始化数组元素即可,不需要动态分配

irq_desc

,此种组织形式中断号连续。

[kernel/irq/irqdesc.c]  // 静态定义的数组,存放所有irq_desc,元素数量为NR_IRQS
    struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = 
    {
        [0 ... NR_IRQS-1] = {
            .handle_irq	= handle_bad_irq,
            .depth		= 1,
            .lock		= __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
        }
    };
    [inlcude/asm-generic/irq.h]
    #define NR_IRQS 64
    [kernel/irq/irqdesc.c]  // 定义的`radix-tree`根节点
    static RADIX_TREE(irq_desc_tree, GFP_KERNEL)
           

Linux内核启动期间,会初始化中断控制器。根据是否定义

CONFIG_SPARSE_IRQ

,初始化流程有所区别。

首先分析定义了

CONFIG_SPARSE_IRQ

的初始化流程。调用

early_irq_init

获取中断数量,为每一个中断动态分配一个

irq_desc

结构体,同时设置位图,将对应的bit设置为1,最后将分配的

irq_desc

插入到

irq_desc_tree

中,可以看出所有的中断都是通过

irq_desc_tree

进行管理。接着调用

init_IRQ

,将设备树中的节点与

__irqchip_of_table

段中保存的中断控制器信息进行匹配,只有兼容属性一致且设备树节点含有

interrupt-controller

属性才能匹配成功,将匹配成功的中断控制器初始化函数保存到

irq_init_cb

中,匹配完成后调用初始化函数,需要注意的是只有父控制器的初始化函数会被调用。对于zynq7k,父中断控制器始化函数是

gic_of_init

start_kernel  // 内核入口函数
      ->local_irq_disable()  // 如果没有禁止中断,则会禁止中断
      ->early_irq_init
        ->init_irq_default_affinity  // 设置中断对每个cpu的亲和度
        ->arch_probe_nr_irqs  // 获取中断数量
            // 默认从machine_desc结构体中的nr_irqs中获取,若为0则使用默认值NR_IRQS
            nr_irqs = machine_desc->nr_irqs ? machine_desc->nr_irqs : NR_IRQS
        ->alloc_desc  // 为每个中断分配一个irq_desc结构体并初始化
          ->kzalloc_node  // 分配irq_desc
          ->esc->kstat_irqs = alloc_percpu(unsigned int)  // 分配每cpu变量 
          ->desc_set_defaults  // 使用默认值初始化irq_desc
        ->set_bit(i, allocated_irqs);  // 设置位图,每一位表示一个irq_desc,将对应的bit设置为1
        ->irq_insert_desc  // 将分配的irq_desc插入到irq_desc_tree中
      ->init_IRQ
        ->irqchip_init
          // __irqchip_of_table在链接脚本中定义,存放中断控制器信息,可与设备树中的中断控制器节点匹配
          ->of_irq_init(__irqchip_of_table)
            // 遍历所有设备节点,并与__irqchip_of_table中定义的中断控制器信息进行匹配,并判断是否具有
            // interrupt-controller属性
            ->for_each_matching_node_and_match  
              kzalloc  // 满足要求,则分配一个of_intc_desc结构体,以链表的形式组织起来
              // 设置irq_init_cb指向匹配成功的中断控制器的初始化函数
              desc->irq_init_cb = match->data
            // 匹配完成开始遍历匹配成功的of_intc_desc结构体链表
            ->while (!list_empty(&intc_desc_list))
              // 调用父中断控制器的初始化函数
              desc->irq_init_cb(desc->dev, desc->interrupt_parent)
          ->acpi_probe_device_table  // 中断控制器电源相关,忽略
      ...
      ->local_irq_enable()  // 开启本地中断
           

接着分析没有定义

CONFIG_SPARSE_IRQ

的初始化流程。可以看出,是否定义

CONFIG_SPARSE_IRQ

,只在

early_irq_init

函数中不同。没有定义

CONFIG_SPARSE_IRQ

,内核会初始化静态定义的

irq_desc

数组,不会动态分配

irq_desc

结构体。

start_kernel  // 内核入口函数
      ->local_irq_disable()  // 如果没有禁止中断,则会禁止中断
      ->early_irq_init
        ->init_irq_default_affinity  // 设置中断对每个cpu的亲和度
          desc = irq_desc  // 获取保存irq_desc数组的首地址
          count = ARRAY_SIZE(irq_desc)  // 获取数组元素数量,即中断数量
        ->desc_set_defaults  // 初始化数组元素
      ->init_IRQ
        ->irqchip_init  // irqchip_init的初始化流程与定义CONFIG_SPARSE_IRQ一致
        ...
      ...
      ->local_irq_enable()  // 开启本地中断
           

中断的初始化函数最终调用到了父中断控制器的初始化函数。zynq7k的父中断控制器为

intc

,驱动程序位于irq-gic.c文件中。

IRQCHIP_DECLARE

定义了与设备树匹配的匹配表。

IRQCHIP_DECLARE

定义的内容编译器会将其放在

__irqchip_of_table

段中。gic驱动程序定义的

"arm,cortex-a9-gic"

属性和

intc

设备树节点定义的兼容属性一致,两者可以匹配,匹配成功后调用的

irq_init_cb

函数就是

gic_of_init

函数。

[drivers/irqchip/irq-gic.c]
    // 定义中断控制器属性及入口函数,属性和设备树intc节点的属性一致
    IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
    [include/linux/irqchip.h]
    #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
    #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  }
           

下面详细分析一下

gic_of_init

函数的调用流程。

init_IRQ

传入两个

device_node

参数,第一个为中断控制器的

device_node

,第二个为中断控制器父节点的

device_node

,由于

intc

为根中断控制器,无父节点,因此第二个参数为

NULL

。Linux设备驱动程序的初始化都围绕着设备结构体进行,gic中断控制器定义了

struct gic_chip_data

设备结构体。

gic_of_init

需要初始化设备结构体中两个比较重要的成员,分别为

chip

domain

chip

指向了

gic_chip

domain

ops

指向

gic_irq_domain_hierarchy_ops

,分配的

domain

挂到了全局链表

irq_domain_list

。最后设置底层中断处理函数指针

handle_arch_irq

指向

gic_handle_irq

[drivers/irqchip/irq-gic.c]
    struct gic_chip_data {    // gic驱动定义的设备结构体,gic初始化函数都围绕此结构体展开
        struct irq_chip chip;  // gic中断控制器操作函数集合
        union gic_base dist_base;  // distributor模块映射的虚拟基地址
        union gic_base cpu_base;   // CPU Interface模块映射的虚拟基地址
        ......
        struct irq_domain *domain;  // 硬件中断号映射为软件中断号
        unsigned int gic_irqs;  // 此中断控制器支持的中断数量
        ......
    };

    // 静态定义的gic设备结构体数组,有多少个中断控制器,就会初始化多少个元素
    static struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly;

    [drivers/irqchip/irq-gic.c]    
    gic_of_init
      ->of_iomap  // 映射distributor模块的寄存器
      ->of_iomap  // 映射CPU Interface模块寄存器地址
      ->gic_check_eoimode  // 中断分步相关,gic v2可以将中断分为两步,gic v1只能是一步
      ->of_property_read_u32  // 获取cpu-offset属性,zynq7k设备树无此属性
      ->__gic_init_bases  // gic的初始化函数,重要
          gic = &gic_data[gic_nr]  // 获取gic设备结构体数组的地址,gic_nr为中断控制器序号
          gic->chip = gic_chip  // 设置chip
          gic->chip.irq_set_affinity = gic_set_affinity  // 如是SMP系统,还需要设置cpu亲和度函数
          // 获取中断控制器ICDICTR寄存器的低五位,通过低五位可计算出支持的中断数量
          // zynq7k ICDICTR寄存器的低五位默认值为0x2,有64条外部中断线,总共64个外部中断
          gic_irqs = readl_relaxed(gic_data_dist_base(gic) + GIC_DIST_CTR) & 0x1f
          // gic_irqs = (0x2 + 1) * 32 = 96
          gic_irqs = (gic_irqs + 1) * 32
          gic->gic_irqs = gic_irqs  // 设置支持的中断数量为96
          ->irq_domain_create_linear  // 初始化irq_domain
            ->__irq_domain_add  // 分配irq_domain
                ->kzalloc_node  // 分配irq_domain结构体内存
                INIT_RADIX_TREE  // 初始化revmap_tree
                domain->ops = ops  // 设置ops,ops为gic_irq_domain_hierarchy_ops
                domain->host_data = host_data  // 设置私有数据,指向中断控制器设备结构体
                domain->hwirq_max = hwirq_max  // 设置支持的最大硬件中断号,为96
                domain->revmap_size = size  // 设置线性映射中断号的数量,size为96
                domain->revmap_direct_max_irq = direct_max  // 设置为0
                ->irq_domain_check_hierarchy  // 检查中断控制器是否级联
                  // 如果ops含有alloc函数,则设置支持级联的标志
                  if (domain->ops->alloc)
                      domain->flags |= IRQ_DOMAIN_FLAG_HIERARCHY;  // 支持级联
                ->list_add  // 将irq_domain添加到全局irq_domain_list链表中
          // 在根中断控制器中,设置SMP系统的回调函数,调用gic_raise_softirq可触发IPI(SGI)中断,
          // 用于多核之间的通信
          ->set_smp_cross_call(gic_raise_softirq)  
          // 注册通知函数,当cpu状态变化时,需要调用gic_cpu_notifier通知gic
          ->register_cpu_notifier(&gic_cpu_notifier)
           // 设置中断处理函数,底层发生中断,最终要调用gic_handle_irq处理中断
           // 这就是底层handle_arch_irq指向的函数
          ->set_handle_irq(gic_handle_irq)
          ->gic_dist_init  // 初始化distributor模块
          ->gic_cpu_init  // 初始化CPU Interface模块
          ->gic_pm_init  // gic电源管理初始化
           

4.中断号的处理过程

gic_of_init

函数中设置了

irq_domain

ops

成员,指向了

gic_irq_domain_hierarchy_ops

结构体。此结构体主要用于映射中断号。

[drivers/irqchip/irq-gic.c]
    static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {
        .translate = gic_irq_domain_translate,  // 将中断引脚号转换为硬件中断号
        .alloc = gic_irq_domain_alloc,  // 完成硬件中断号向软件中断号的映射
        .free = irq_domain_free_irqs_top,  // 解除硬件中断号和软件中断号的映射关系
    };
           

4.1.中断引脚号到硬件中断号的转换过程分析

首先分析一下

gic_irq_domain_translate

函数是如何将中断引脚号转换成硬件中断号的。由于设备树中提供的是中断引脚号,没有考虑SGI和PPI中断,因此需要将中断引脚号加上SGI和PPI预留的硬件中断号范围,才能得到实际的硬件中断号。

gic_irq_domain_translate
        if (fwspec->param_count < 3)  // 如果interrupts属性值数量小于3个,返回-EINVAL
            return -EINVAL;
        *hwirq = fwspec->param[1] + 16  // 由于0-15中断号预留给SGI中断,因此需要加16
        if (!fwspec->param[0])  // 对于SPI中断,需要再加16以跳过给PPI预留的中断号
            *hwirq += 16;  // 则串口最终的硬件中断号为27+16+16=59,和手册中一致
        *type = fwspec->param[2] & IRQ_TYPE_SENSE_MASK  // 获取中断类型
           

4.2.硬件中断号到软件中断号的映射过程分析

接着分析一下

gic_irq_domain_alloc

函数是如何将硬件中断号映射为软件中断号的。每个软件中断号对应一个

irq_desc

irq_desc

中嵌入了一个和中断控制器有关的结构体

irq_data

,将硬件中断号填入

irq_data

hwirq

变量中就完成了中断号的映射工作。得到软件中断号的过程将在下一章分析。

// 传入的参数为中断控制器的指针domain、软件中断号、映射的中断号数量及参数
    gic_irq_domain_alloc
        // 将设备树中的中断引脚号转换为硬件中断号,获取中断触发类型
        ->gic_irq_domain_translate  
        ->gic_irq_domain_map  // 进行中断映射
            gic_chip_data *gic = d->host_data  // 获取gic设备结构体
            ->irq_domain_set_info
              ->irq_domain_set_hwirq_and_chip
                ->irq_domain_get_irq_data  // 获取软件中断号对应的irq_data
                irq_data->hwirq = hwirq  // 设置硬件中断号
                irq_data->chip = chip  // 设置chip,chip为gic设备结构体中的chip成员,即gic_chip结构体
                irq_data->chip_data = chip_data  // 设置gic设备结构体
            ->__irq_set_handler
              ->__irq_do_set_handler
              desc->handle_irq = handle // 设置中断处理函数为handle_fasteoi_irq
            ->irq_set_handler_data
              desc->irq_common_data.handler_data = data
        ->irq_set_probe  // 设置一些标志

    // gic chip结构体,即irq_data中chip指向的结构体,包含了gic控制器操作函数集合
    static struct irq_chip gic_chip = {
        .irq_mask		= gic_mask_irq,  // 屏蔽中断
        .irq_unmask		= gic_unmask_irq,  // 解除屏蔽
        .irq_eoi		= gic_eoi_irq,  // 中断结束时使用此接口通知gic中断控制器
        .irq_set_type		= gic_set_type,  // 设置中断触发类型
        .irq_get_irqchip_state	= gic_irq_get_irqchip_state,  // 获取中断控制器状态
        .irq_set_irqchip_state	= gic_irq_set_irqchip_state,  // 设置中断控制器状态
        // 中断控制器标志
        .flags			= IRQCHIP_SET_TYPE_MASKED |
                        IRQCHIP_SKIP_SET_WAKE |
                        IRQCHIP_MASK_ON_SUSPEND,
    };
           

4.数据结构介绍

4.1.中断描述符irq_desc

每一个外设的中断都对应一个中断描述符,Linux内核使用

irq_desc

结构体描述中断。成员

irq_data

和具体硬件相关,内部包含了

irq_chip

,是中断控制器操作函数的集合;成员

handle_irq

在中断发生时首先调用,然后根据不同的中断类型执行不同的中断处理函数,同一个中断控制器,其

handle_irq

是一样的;

action

是中断处理函数,设备驱动使用

request_irq

request_threaded_irq

注册的中断处理函数就保存在此处,如果是共享中断,则

action

是一个链表,保存了多个中断处理函数。

[include/linux/irq.h]
    struct irq_desc   // 中断描述符
    {
        struct irq_common_data	irq_common_data;
        struct irq_data		irq_data;  // 包含了irq_chip,和中断控制器相关
        unsigned int __percpu	*kstat_irqs;  // 中断的统计信息
        irq_flow_handler_t	handle_irq;  // 中断产生后,首先执行此函数指针指向的函数,然后再执行action
        ......
        struct irqaction	*action;	// 具体设备的中断处理函数,是一个链表,共享中断时有多个处理函数
        unsigned int		status_use_accessors;  // 中断描述符状态
        unsigned int		core_internal_state__do_not_mess_with_it;
        unsigned int		depth;		/* nested irq disables */
        unsigned int		wake_depth;	/* nested wake enables */
        unsigned int		irq_count;	/* For detecting broken IRQs */
        unsigned long		last_unhandled;	/* Aging timer for unhandled count */
        unsigned int		irqs_unhandled;
        atomic_t		threads_handled;
        int			threads_handled_last;
        raw_spinlock_t		lock;
        ......
        int			parent_irq;
        const char		*name;    // 产生中断的设备名称
    } ____cacheline_internodealigned_in_smp
           

struct irq_desc

在Linux内核中有两种组织方式,第一种为数组形式,应用于线性映射,另一种是基数树,应用于非线性映射。

中断控制器介绍及初始化源代码分析(二)

4.2.硬件相关的irq_data和irq_chip

irq_data

将硬件相关的内容进行了封装,主要成员有Linux软件(虚拟)中断号

irq

、硬件中断号

hwirq

、中断控制器操作函数集合

chip

及中断号映射结构体

domain

[include/linux/irq.h]
    struct irq_data     // 中断控制器对应的数据
    {
        u32			mask;
        unsigned int		irq;  // 中断号
        unsigned long		hwirq;  // 硬件中断号
        struct irq_common_data	*common;  // 所有中断控制器共享的数据
        struct irq_chip		*chip;  // 包含了底层中断控制器的操作函数
        struct irq_domain	*domain;  // 中断号映射结构体,可将硬件中断号映射成linux的软件中断号
    #ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
        struct irq_data		*parent_data;  // 指向父中断控制器的irq_data
    #endif
        void			*chip_data;  //irq_chip的私有数据 
    };
           

irq_chip

是中断控制器操作函数的集合,主要成员有启动中断控制器

irq_startup

、关闭中断控制器

irq_shutdown

、使能中断

irq_enable

、禁止中断

irq_disable

、响应中断

irq_ack

、屏蔽中断

irq_mask

、响应并屏蔽某个中断

irq_mask_ack

等。cpu调用这些函数控制中断控制器。

[include/linux/irq.h]
    struct irq_chip {  // 中断控制器操作函数集合
        const char	*name;  // 中断控制器的名称,可在/proc/interrupts中查看
        unsigned int	(*irq_startup)(struct irq_data *data);  // 启动中断,如为NULL则默认启动
        void		(*irq_shutdown)(struct irq_data *data);  // 关闭中断,如为NULL则默认关闭
        void		(*irq_enable)(struct irq_data *data);  // 使能中断
        void		(*irq_disable)(struct irq_data *data);  // 禁止中断
         // 响应中断,有的中断控制器在中断响应后(清除pending的中断)才可以重新触发中断
        void		(*irq_ack)(struct irq_data *data); 
        void		(*irq_mask)(struct irq_data *data);  // 屏蔽某个中断
        void		(*irq_mask_ack)(struct irq_data *data);  // 响应并屏蔽某个中断
        void		(*irq_unmask)(struct irq_data *data);  //  解除屏蔽中断
        void		(*irq_eoi)(struct irq_data *data);  // 中断结束后调用
        // 在smp系统中,设置中断对某个cpu的亲和度
        int		(*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
        // 重新触发中断,当丢失硬件中断后,可使用软件再次触发中断
        int		(*irq_retrigger)(struct irq_data *data); 
        // 设置中断触发方式
        int		(*irq_set_type)(struct irq_data *data, unsigned int flow_type);
        // 使能 禁止电源管理中的唤醒功能
        int		(*irq_set_wake)(struct irq_data *data, unsigned int on);
        // 使用慢速总线时,需要lock总线
        void		(*irq_bus_lock)(struct irq_data *data);
        // unlock慢速总线
        void		(*irq_bus_sync_unlock)(struct irq_data *data);
        void		(*irq_cpu_online)(struct irq_data *data);
        void		(*irq_cpu_offline)(struct irq_data *data);
        ......
        unsigned long	flags;
    };
           

4.3.中断号映射相关的irq_domain和irq_domain_ops

中断发生时,只能从中断控制器得到硬件中断号,但Linux内核使用软件(虚拟)中断号描述中断,因此需要将两者联系起来。

irq_domain

是硬件中断号和软件中断号的桥梁,可以将硬件中断号映射为软件中断号。每个中断控制器都对应一个

irq_domain

irq_domain

的主要成员有操作函数集合

ops

、最大硬件中断号

hwirq_max

、线性映射的中断号数量

revmap_size

、radix tree映射的根节点

revmap_tree

及线性映射的查找表

linear_revmap

.

[include/linux/irqdomain.h]
    struct irq_domain  // 将硬件中断号映射为Linux软件中断号,每个中断控制器都对应一个irq_domain
    {
        struct list_head link;  // 挂接到全局的irq_domain_list链表
        const char *name;  // interrupt domain的名称
        const struct irq_domain_ops *ops;  //  irq_domain的操作函数,包含映射和解除映射函数
        void *host_data;  // irq_domain私有数据
        unsigned int flags;

        struct fwnode_handle *fwnode;  // 指向和irq_domain相关联的设备树节点
        enum irq_domain_bus_token bus_token;
        struct irq_domain_chip_generic *gc;
    #ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
        struct irq_domain *parent;  // 配置级联,则指向父中断控制器的irq_domain
    #endif
        irq_hw_number_t hwirq_max;  // irq_domain中最大的硬件中断号
        unsigned int revmap_direct_max_irq; // 线性映射时可写入中断控制器的最大硬件中断号
        unsigned int revmap_size;  // 线性映射中断号的数量
        struct radix_tree_root revmap_tree;  // radix tree映射的根节点
        unsigned int linear_revmap[];  // 线性映射用到的查找表
    };

    [include/linux/irqdomain.h]
    struct irq_domain_ops   // irq_domain映射中断号时用到的操作函数
    {
        // 中断控制器设备树节点和irq_domain匹配
        int (*match)(struct irq_domain *d, struct device_node *node,
                enum irq_domain_bus_token bus_token);
        // 映射中断号
        int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
        // 取消映射
        void (*unmap)(struct irq_domain *d, unsigned int virq);
        // 将设备树提供的中断号转换为真实的硬件中断号,如zynq7k gpio设备树节点中的中断号为20,
        // 由于0-31的硬件中断号被PPI和SGI中断占用,gpio真实的硬件中断号为52,需要加上一个偏移量
        int (*xlate)(struct irq_domain *d, struct device_node *node,
                const u32 *intspec, unsigned int intsize,
                unsigned long *out_hwirq, unsigned int *out_type);
        ......
    };
    
    [kernel/irq/irqdesc.c]
    // 定义了一个位图,如果定义CONFIG_SPARSE_IRQ则大小为(NR_IRQS + 8196)bit,否则为NR_IRQS bit
    static DECLARE_BITMAP(allocated_irqs, IRQ_BITMAP_BITS);
           

5.总结

Linux中断子系统关键数据结构及其之间的联系可总结如下:

中断控制器介绍及初始化源代码分析(二)

参考资料

  1. Linux kernel V4.6版本源码
  2. https://www.cnblogs.com/LoyenWang/p/12996812.html
  3. 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
  4. 《Zynq-7000 SoC Technical Reference Manual》
  5. 《ARM® Generic Interrupt Controller Architecture version 2.0》
  6. 《CoreLink GIC-400 Generic Interrupt Controller Technical Reference Manual》