自学内容网 自学内容网

深入解析GIC中断处理与内核初始化:基于Linux 4.9.88内核的详细分析

往期内容

本专栏往期内容,interrtupr子系统:

  1. 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现
  2. Linux内核中IRQ Domain的结构、操作及映射机制详解
  3. 中断描述符irq_desc成员详解
  4. Linux 内核中断描述符 (irq_desc) 的初始化与动态分配机制详解
  5. 中断的硬件框架
  6. GIC介绍
  7. GIC寄存器介绍
  8. ARM架构中断与异常向量表机制解析

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    – 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有专栏内容观看顺序

img

前言

建议先看一下往期文章关于中断相关结构体的讲解:中断描述符irq_desc成员详解-CSDN博客

  • Linux 4.9.88内核源码

    • Linux-4.9.88\drivers\irqchip\irq-gic.c
    • Linux-4.9.88/arch/arm/boot/dts/imx6ull.dtsi

本文以Linux 4.9.88内核为基础,系统解析了通用中断控制器(GIC)的中断处理与初始化机制。介绍了一级和多级中断控制器的处理流程,包括中断号映射(hwirq与virq)和中断服务函数的调用。梳理了GIC驱动的关键结构体与核心函数,阐述了GIC的功能及其内核表示方法。以设备树为线索,详细剖析了GIC初始化过程,解读了IRQCHIP_DECLARE宏及其如何与设备树节点匹配并触发初始化。通过源码分析与注释,逐步讲解了GIC的域操作和分层中断处理流程。

1.GIC中断处理流程

1.1 一级中断控制器处理流程

对于irq_desc,内核有两种分配方法:

  • 一次分配完所有的irq_desc
  • 按需分配(用到某个中断才分配它的irq_desc

现在的内核基本使用第1种方法。

img

  • 假设GIC可以向CPU发出161019号中断,这些数字被称为hwirq。015用于Process之间通信,比较特殊。

  • 假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16

  • 在GIC domain中会记录(32, 16)

  • 那么注册中断时就是:request_irq(16, ...) //虚拟中断号or软件中断号

  • 发生UART中断时

    • 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16
    • 调用irq_desc[16]中的handleA函数(GIC提供的handle函数),它的作用是屏蔽中断、调用action链表中用户注册的函数handler和thread_fn、清除中断

1.2 多级中断控制器处理流程

img

  • 假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断

  • GPIO也可以看作一个中断控制器,对于它的4个中断

  • 对于GPIO模块中0~3这四个hwirq,一般都会一下子分配四个irq_desc

  • 假设这4个irq_desc的序号为100~103,在GPIO domain中记录(0,100) (1,101)(2,102) (3,103)

  • 对于KEY,注册中断时就是:request_irq(102, ...)

  • 按下KEY时:

    • 程序从GIC中读取寄存器知道发生了33号中断,通过GIC irq_domain可以知道virq为17

    • 调用irq_desc[17]中的handleB函数

    • mask GIC33号中断

      • handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断
      • 通过GPIO irq_domain可以知道virq为102
      • 调用irq_desc[102]中的handleA函数,它的作用是mask GPIO2号中断、调用action链表中用户注册的函数、clear GPIO2号中断
      • clear GIC33 号中断

2.GIC中的重要函数和结构体

沿着中断的处理流程,GIC涉及这4个重要部分:

  • CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的

    • GIC才知道怎么判断发生的是哪个GIC中断
  • 从GIC获得hwirq后,要转换为virq:需要有GIC Domain

  • 调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供

  • 处理中断时,要屏蔽中断、清除中断等:这些函数保存在irq_chip里,由GIC驱动提供

从硬件上看,GIC的功能是什么?

  • 可以使能、屏蔽中断
  • 发生中断时,可以从GIC里判断是哪个中断

在内核里,使用gic_chip_data结构体表示GIC,gic_chip_data里有什么?

  • irq_chip:中断使能、屏蔽、清除,放在irq_chip中的各个函数里实现

在这里插入图片描述

  • irq_domain
    • 申请中断时

      • 在设备树里指定hwirq、flag,可以使用irq_domain的函数来解析设备树
      • 根据hwirq可以分配virq,把(hwirq, virq)存入irq_domain中
    • 发生中断时,从GIC读出hwirq,可以通过irq_domain找到virq,从而找到处理函数

所以,GIC用gic_chip_data来表示,gic_chip_data中重要的成员是:irq_chip、irq_domain。

3.GIC初始化过程

Linux-4.9.88\drivers\irqchip\irq-gic.c📎irq-gic.c

(将dtb文件反汇编为dts后,在里面搜索Interrupt-contorller找到中断控制器的节点,复制其compatible属性,在内核源码中grep该属性搜索一下就可以找到该文件)

3.1 内核支持多种GIC

按照设备树的套路:

  • 驱动程序注册platform_driver
  • 它的of_match_table里有多个of_device_id,表示能支持多个设备
  • 有多种版本的GIC,在内核为每一类GIC定义一个结构体of_device_id,并放在一个段里:
// drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", 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);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);

把宏IRQCHIP_DECLARE展开:

// include\linux\irqchip.h
#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(__irqchip_of_table)\
         = { .compatible = compat,\
             .data = (fn == (fn_type)NULL) ? fn : fn  }

展开示例:

IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
展开后得到:
static const struct of_device_id __of_table_cortex_a7_gic\
    __used __section(__irqchip_of_table)\
     = { .compatible = "arm,cortex-a7-gic",\
         .data = gic_of_init  }

3.2 初始化调用

// drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", 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);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);

像5.3.1中上述的宏,表示支持的GIC的种类,比如要使用IRQCHIP_DECLARE(cortex_a7_gic, “arm,cortex-a7-gic”, gic_of_init);,那它是如何去选择和初始化的?如下:

start_kernel (init\main.c)
    init_IRQ (arch\arm\kernel\irq.c)
        irqchip_init (drivers\irqchip\irqchip.c)
            of_irq_init (drivers\of\irq.c)
                desc->irq_init_cb = match->data;

                ret = desc->irq_init_cb(desc->dev,
                            desc->interrupt_parent);

如何知道of_irq_init在哪里被调用的:
在这里插入图片描述

选择某一GIC和进行初始化:

img

\Linux-4.9.88\drivers\irqchip\irq-gic.c
void __init of_irq_init(const struct of_device_id *matches)
{
    const struct of_device_id *match;         // 用于存放匹配的设备节点的指针
    struct device_node *np, *parent = NULL;   // np 指向当前中断控制器节点,parent 指向父节点
    struct of_intc_desc *desc, *temp_desc;    // 中断控制器描述符指针,用于保存控制器相关信息
    struct list_head intc_desc_list, intc_parent_list; // 初始化中断控制器列表和父节点列表

    INIT_LIST_HEAD(&intc_desc_list);          // 初始化描述符列表,存储扫描到的中断控制器
    INIT_LIST_HEAD(&intc_parent_list);        // 初始化父节点列表,存储待初始化的父节点

    // 1. 遍历设备树中与 `matches` 匹配的节点
    for_each_matching_node_and_match(np, matches, &match) {
        // 检查节点是否有 "interrupt-controller" 属性且可用
        if (!of_find_property(np, "interrupt-controller", NULL) || 
            !of_device_is_available(np))
            continue;

        // 如果匹配项的 `data` 字段为空,发出警告并跳过
        if (WARN(!match->data, "of_irq_init: no init function for %s\n", 
                 match->compatible))
            continue;

        // 2. 为每个匹配的中断控制器分配并填充一个 `of_intc_desc` 结构体
        desc = kzalloc(sizeof(*desc), GFP_KERNEL);
        if (WARN_ON(!desc)) {
            of_node_put(np);
            goto err;
        }

        desc->irq_init_cb = match->data;                 // 初始化回调函数
        desc->dev = of_node_get(np);                     // 保存节点信息
        desc->interrupt_parent = of_irq_find_parent(np); // 查找中断父节点
        if (desc->interrupt_parent == np)                // 若节点为根中断控制器,设置为 NULL
            desc->interrupt_parent = NULL;
        list_add_tail(&desc->list, &intc_desc_list);     // 添加到中断控制器描述符列表
    }

    /*
     * 3. 逐层初始化中断控制器,优先初始化根节点(即无父节点的控制器)
     */
    while (!list_empty(&intc_desc_list)) {
        list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
            int ret;

            // 跳过非当前父节点的子节点
            if (desc->interrupt_parent != parent)
                continue;

            list_del(&desc->list);                       // 从描述符列表中移除当前描述符

            of_node_set_flag(desc->dev, OF_POPULATED);   // 标记节点已初始化

            pr_debug("of_irq_init: init %s (%p), parent %p\n",
                     desc->dev->full_name,
                     desc->dev, desc->interrupt_parent);
            // 调用中断控制器的初始化回调函数
            ret = desc->irq_init_cb(desc->dev,
                                    desc->interrupt_parent);
            if (ret) {
                of_node_clear_flag(desc->dev, OF_POPULATED); // 若初始化失败,清除标记
                kfree(desc);
                continue;
            }

            /*
             * 初始化成功,将此节点加入到父节点列表中,
             * 以便后续初始化其子节点
             */
            list_add_tail(&desc->list, &intc_parent_list);
        }

        // 获取下一个待处理的父节点
        desc = list_first_entry_or_null(&intc_parent_list,
                                        typeof(*desc), list);
        if (!desc) {
            pr_err("of_irq_init: children remain, but no parents\n");
            break;
        }
        list_del(&desc->list);
        parent = desc->dev;
        kfree(desc);
    }

    // 释放未使用的描述符节点
    list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
        list_del(&desc->list);
        kfree(desc);
    }
err:
    // 清理和释放资源,释放未初始化的描述符节点
    list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
        list_del(&desc->list);
        of_node_put(desc->dev);
        kfree(desc);
    }
}
  1. 匹配中断控制器节点:遍历设备树中所有符合 matches 条件的节点,通过检查节点是否有 interrupt-controller 属性,筛选出所有中断控制器节点,并分配 of_intc_desc 结构体保存节点相关信息(包括初始化回调函数、节点指针、父节点指针等)。
  2. 初始化中断控制器:从根中断控制器(无父节点)开始,每次处理当前父节点的所有子节点,并递归初始化,确保初始化顺序由根至叶子。
  3. 清理和释放:初始化完成后,释放未使用的描述符,确保资源不被浪费。

IRQCHIP_DECLARE 宏如何完成与设备树的挂钩:

IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init); 的作用是声明并注册一个中断控制器,这样在调用 of_irq_init 时能够识别设备树中的 "arm,cortex-a9-gic" 中断控制器节点。具体过程如下:

  1. IRQCHIP_DECLARE 定义了一个 of_device_id 类型的结构体实例,其中包含 "arm,cortex-a9-gic" 兼容字符串与 gic_of_init 初始化函数的关联。
  2. of_irq_init 遍历设备树时,识别出符合 "arm,cortex-a9-gic" 的节点,匹配后调用 gic_of_init 进行中断控制器初始化,从而与设备树节点挂钩。

3.3 在设备树里指定GIC

在设备树中指定GIC,内核驱动程序根据设备树来选择、初始化GIC。

drivers\irqchip\irqchip.c中并没有定义一个platform_driver,但是套路是一样的。

img

调用过程:img

of_irq_init:

  • 内核有一个__irqchip_of_table数组,里面有多个of_device_id,表示多种GIC

  • 要使用哪类GIC?在设备树里指定

  • 根据设备树,找到__irqchip_of_table树组中对应的项,调用它的初始化函数

    • IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);

3.4 gic_of_init分析

请添加图片描述

先记住上面的中断处理流程,再来分析gic_of_init:

申请中断的过程(irq_domain_ops域操作结构体):

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,

};

  • 在设备树中声明要使用哪个GIC控制器下的哪个中断
  • 用gic_irq_domain_translate来解析设备树,知道其hwirq以及flag
  • 为hwirq在irq_desc数组中找到空闲的位置(数组下标就是其virq)
  • 记录hwirq和virq在domain中(建立联系)
  • 使用gic_irq_domain_alloc处理,提供handle中断函数等

img

``📎irq-gic.c📎entry-armv.S

关键词:gic_of_setup、__gic_init_bases、gic_init_chip、gic_init_bases、irq_domain_create_linear

int __init
gic_of_init(struct device_node *node, struct device_node *parent)
{
    struct gic_chip_data *gic;  // 用于存储当前GIC的配置信息
    int irq, ret;

    // 检查设备树节点的有效性,如果为空则返回错误码 -ENODEV
    if (WARN_ON(!node))
        return -ENODEV;

    // 检查当前GIC控制器数量是否已达到最大允许数量,若超限则返回错误码 -EINVAL
    if (WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))
        return -EINVAL;

    // 获取用于当前GIC的数据结构,并使用 gic_data 数组存储多实例的 GIC
    gic = &gic_data[gic_cnt];

    // 调用 gic_of_setup 函数根据设备树节点 `node` 初始化 GIC 的基本信息
    ret = gic_of_setup(gic, node);
    if (ret)
        return ret;

    /*
     * 如果 hypervisor 模式(HYP)不可用或 CPU 接口寄存器太小,禁用分离的
     * End Of Interrupt/Deactivate (EOI/Deactivate) 模式。
     */
    if (gic_cnt == 0 && !gic_check_eoimode(node, &gic->raw_cpu_base))
        static_key_slow_dec(&supports_deactivate);

    // 初始化 GIC 的寄存器基地址、GIC 中断域等基本配置
    ret = __gic_init_bases(gic, -1, &node->fwnode);
    if (ret) {
        // 若初始化失败,调用 gic_teardown 清理当前 GIC 配置
        gic_teardown(gic);
        return ret;
    }

    // 若是第一个 GIC 控制器,进行物理地址初始化并设置 KVM (Kernel-based Virtual Machine) 信息
    if (!gic_cnt) {
        gic_init_physaddr(node);
        gic_of_setup_kvm_info(node);
    }

    // 若存在父节点,则解析并映射第一个中断号并进行级联中断配置
    if (parent) {
        irq = irq_of_parse_and_map(node, 0); // 解析并映射设备树中第一个中断
        gic_cascade_irq(gic_cnt, irq);       // 配置级联中断
    }

    // 如果启用了 GIC V2M(GIC Version 2 Memory Mapped)支持,初始化 GIC V2M
    if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
        gicv2m_init(&node->fwnode, gic_data[gic_cnt].domain);

    // 增加 GIC 控制器计数器,表明一个新的 GIC 已初始化完成
    gic_cnt++;
    return 0;
}

3.4.1gic_of_setup

gic_of_init函数中调用的,主要是初始化 GIC 的基本信息,和设备树挂钩。

img

img

reg中,第一行就是分发器Distributro所对应寄存器的地址,第二行则是cpu interface所对应寄存器的地址,既然是物理地址,那就得映射,就是由该函数实现的:

static int gic_of_setup(struct gic_chip_data *gic, struct device_node *node)
{
    // 检查传入参数 gic 和 node 是否为空,以确保有效性
    if (!gic || !node)
        return -EINVAL;  // 如果为空,返回 -EINVAL 表示无效参数

    // 使用设备树节点 `node` 映射分发器(distributor)寄存器基地址
    gic->raw_dist_base = of_iomap(node, 0);
    // 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源
    if (WARN(!gic->raw_dist_base, "unable to map gic dist registers\n"))
        goto error;

    // 使用设备树节点 `node` 映射 CPU 接口(CPU interface)寄存器基地址
    gic->raw_cpu_base = of_iomap(node, 1);
    // 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源
    if (WARN(!gic->raw_cpu_base, "unable to map gic cpu registers\n"))
        goto error;

    // 读取设备树节点中的 `cpu-offset` 属性,用于定义 CPU 接口偏移量
    // 如果未定义 `cpu-offset` 属性,默认偏移量设为0
    if (of_property_read_u32(node, "cpu-offset", &gic->percpu_offset))
        gic->percpu_offset = 0;

    return 0;  // 成功初始化时返回 0 表示无错误

error:
    // 如果映射失败,调用 gic_teardown 释放已分配的 GIC 资源
    gic_teardown(gic);
    return -ENOMEM;  // 返回 -ENOMEM 表示内存分配失败
}

3.4.2 __gic_init_bases

要目的是为 GIC(Generic Interrupt Controller)初始化基础数据结构和相关属性,为后续中断处理和分发提供支持。它包含了对第一个 GIC 实例的特殊处理(主要是主 GIC 的初始化),并设置了对 SMP(对称多处理)系统的支持。

static int __init __gic_init_bases(struct gic_chip_data *gic,
   int irq_start,
   struct fwnode_handle *handle)
{
char *name;
int i, ret;

// 检查传入的 gic 是否为空或已经初始化,若是则返回 -EINVAL 表示无效参数
if (WARN_ON(!gic || gic->domain))
return -EINVAL;

// 检查当前是否为主 GIC,即 gic_data[0]
if (gic == &gic_data[0]) {
/*
 * 初始化 CPU 接口映射,将其默认设置为所有 CPU。
 * 每个 CPU 在探测其 ID 时将此映射进一步细化。
 * 仅主 GIC 需要该操作。
 */
for (i = 0; i < NR_GIC_CPU_IF; i++)
gic_cpu_map[i] = 0xff; // 0xff 表示所有 CPU

#ifdef CONFIG_SMP
// 设置跨 CPU 的中断调用函数
set_smp_cross_call(gic_raise_softirq);
#endif

// 设置 CPU 热插拔的回调状态,用于在 CPU 启动时初始化 GIC
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
  "AP_IRQ_GIC_STARTING",
  gic_starting_cpu, NULL);

// 注册 GIC 的中断处理函数
set_handle_irq(gic_handle_irq);

// 如果支持 EOI/Deactivate 模式,输出提示信息
if (static_key_true(&supports_deactivate))
pr_info("GIC: Using split EOI/Deactivate mode\n");
}

// 初始化 GIC 芯片名称和类型
if (static_key_true(&supports_deactivate) && gic == &gic_data[0]) {
// 主 GIC 使用分离的 EOI/Deactivate 模式,名称设置为 "GICv2"
name = kasprintf(GFP_KERNEL, "GICv2");
gic_init_chip(gic, NULL, name, true); // EOI/Deactivate 为 true
} else {
// 其他 GIC 使用默认名称格式 "GIC-x",x 是 GIC 索引
name = kasprintf(GFP_KERNEL, "GIC-%d", (int)(gic - &gic_data[0]));
gic_init_chip(gic, NULL, name, false); // EOI/Deactivate 为 false
}

// 调用 gic_init_bases 初始化 GIC 基础结构
ret = gic_init_bases(gic, irq_start, handle);
if (ret)
kfree(name); // 若初始化失败,释放为名称分配的内存

return ret; // 返回初始化结果
}

set_handle_irq

img

gic_init_chip

img

gic_init_bases

请添加图片描述


原文地址:https://blog.csdn.net/caiji0169/article/details/143809794

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!